nwidart/laravel-videoable
Laravel Videoable provides a Videoable trait and polymorphic video model to associate videos with any Eloquent model, making it easy to store and retrieve video records for posts, products, and other Laravel resources.
Check the constraints before you composer require. This package's last release (3.0.0, Oct 2018) targets Laravel 5.7 and PHP ^7.1.3. On Laravel 8+ you will hit a Composer solve failure or, worse, a silent install with an unmaintained dependency. Verify with:
composer show nwidart/laravel-videoable --all
# inspect require: illuminate/support, php
If you are on 5.7, install and publish:
composer require nwidart/laravel-videoable
php artisan vendor:publish --provider="Nwidart\Videoable\VideoableServiceProvider"
php artisan migrate
Where to look first: the published migration (database/migrations/*_create_videos_table.php). It defines the actual schema — the polymorphic keys (videoable_type, videoable_id) plus whatever metadata columns the package expects. Read it before writing any code; the column names are the API contract you will be writing against. Then read the Videoable trait to confirm the relation name (conventionally videos()).
First use case — attach videos to a model:
use Nwidart\Videoable\Traits\Videoable;
class Product extends Model
{
use Videoable;
}
Then in a controller or seeder:
$product->videos()->create([
'url' => 'https://cdn.example.com/trailer.mp4',
// other metadata columns per the published migration
]);
The day-one win is that you never touch a product's schema to add media. $product->videos just works, and so does it for Post, User, or anything else — one trait, one table.
Trait-per-model, one table. The package's whole value is that video storage is decoupled from domain models. Do not add video_url columns to products; use the relation. This is the pattern that pays off when a fourth model needs media.
Creation via the relation, not the model directly. Always go through $model->videos()->create([...]) so the morph keys are populated for you. Creating Video::create([...]) manually means you own videoable_type/videoable_id, and you will get them wrong (App\Models\Post vs. a morph alias).
Ordering and "primary" video. The description advertises query/order support. If the migration has an order/position column, set it on create and query deterministically:
$product->videos()->orderBy('position')->get();
$product->videos()->orderBy('position')->first(); // "the" trailer
If there is no order column, add one in your own migration — do not rely on insertion order.
Eager loading for index pages. A product list that renders a thumbnail per row is the classic N+1:
Product::with('videos')->paginate();
Morph map integration. If your app uses Relation::morphMap(), the stored videoable_type values become aliases ('product'), not FQCNs. Register the map before writing any videos, or you will have a mixed-data migration on your hands.
API surface. Wrap the relation in a resource class rather than leaking the model, so a later swap (or replacing this abandoned package) doesn't change your JSON contract:
class VideoResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'url' => $this->url,
'title' => $this->title,
];
}
}
Integration tip — validation lives with you. The package stores metadata; it does not validate URLs, MIME types, or ownership. Put an StoreVideoRequest in front of any endpoint that creates videos, and authorize against the parent model ($this->authorize('update', $product)).
Tip — wrap creation in a small service. Even a 20-line AttachVideo action means the day this package is replaced, you change one file, not every controller.
Abandonment is the headline risk, not an implementation detail. Last release 2018-10-01, 18 stars, no Laravel 6/7/8+ support. Plan for this package to be your code, not upstream's. If the project has a multi-year horizon, budget for vendoring or replacing it; the trait + morphMany relation is not hard to reimplement in a day.
Major-version semantics are misleading. 3.0.0 is a major bump whose only note is "added Laravel 5.7 support" — that almost certainly means 5.5/5.6 constraints were dropped. Pin explicitly (^3.0, not a floating range) and commit the lock file so an unattended composer update cannot cross the boundary in production.
Read the published migration for the real column names. Do not guess field names from the README. The migration is the source of truth for what create() accepts; mass-assignment failures ($fillable) on the package's Video model show up as silently-dropped fields, not exceptions. Use Model::preventSilentlyDiscardingAttributes() in local dev to catch this.
No cascade deletes out of the box. Deleting a Product leaves orphaned videos rows with dangling morph keys. Add a deleted model event or a FK with onDelete('cascade') in your own migration; the package will not do it for you.
Morph aliases break queries if introduced late. If you add Relation::morphMap() to an existing install, historical rows still carry FQCNs. Either backfill the videoable_type column or leave the map off. Mixed values make $product->videos return nothing — a confusing, silent failure.
Debugging ordering/N+1: DB::listen() or Laravel Debugbar will show a query per product if you forget with('videos'). This is the most common performance bug with this package and it will not surface in small test fixtures.
Storage is metadata, not ingestion. The package stores what you give it; it does not upload, transcode, or generate thumbnails. Pair it with your own disk/queue pipeline (or a service like Mux/Cloudflare Stream) and store the resulting URL plus IDs.
Extension point — the Video model. If you need extra behaviour (accessors, soft deletes, casts), check whether the package lets you swap the model via config or a container binding before you start subclassing. If it doesn't, that is another argument for a small in-house replacement.
Upgrade path when it breaks. Because the relation is a plain morphMany, a replacement package only needs the same trait name and method signature. Keep a one-page decision note: why you pinned ^3.0, what Laravel version you are on, and what the exit path is. That note is worth more than the package's changelog.
How can I help you explore Laravel packages today?