Models, Migrations and Media

How the data layer is organised, which traits give your model translations, media, slugs, search and cache-safety, and how migrations, seeders and factories fit into a plugin.

The model toolbox

You need…UseExample in the code base
Public pages never show stale dataExtend App\Models\BaseModel (ClearCache trait)Page, Post, Menu, Advertisement, the Testimonials example
Translated columnsAstrotomic\Translatable + a {table}_translations tablePage, Post, Booking's Location
Images and filesHasMedia + InteractsWithMedia (Spatie) and HandlesMediaUploadsPost (featured-image), User (avatar)
A public URLHasSlug + ResolvableSlugPage, Post, Booking Service, TruCommerce Product
Admin and front searchNicolaslopezj\Searchable\SearchableTrait with $searchablePost, User, Advertisement
View countersCyrildeWit\EloquentViewable\InteractsWithViews + ViewablePage, Post
Soft deletes / trashSoftDeletesPage, Post, User
Sitemap inclusionSpatie\Sitemap\Contracts\SitemapablePage, Post (only core models are collected today)
CommentsCommentable / ICommentablePost

BaseModel and cache invalidation

BaseModel uses HasFactory and boots ClearCache: on created, updated and deleted it forgets the model's cache keys and flushes the response cache. Customise per model by overriding protected methods:

class Testimonial extends BaseModel
{
    /** Cache keys to forget. Default: ['cache_testimonial', 'cache_testimonial_{id}'] */
    protected function cacheKeys(): array
    {
        return [...parent::cacheKeys(), 'testimonials.homepage'];
    }

    /** Keys that depend on the OLD attribute values (see Menu::dynamicCacheKeys). */
    protected function dynamicCacheKeys(): array
    {
        return ['testimonials.slug.' . $this->getOriginal('slug')];
    }

    /** Return false for models written constantly by activity that visitors never see (User does). */
    protected function shouldClearResponseCache(): bool
    {
        return false;
    }
}

If you cannot extend BaseModel (the model already has another base class, or comes from a framework like Vanilo/Konekt) use ResponseCacheInvalidator::flushWhenChanged([Model::class, ...]), which registers saved/deleted/restored listeners. Eloquent events are per concrete class: register subclasses (Staff) and the configured media model (config('media-library.media_model')), not their parents.

Translatable models

Translations live in a sibling table with {model}_id, locale and the translated columns. Booking's Location is a compact example:

migration

Schema::create('booking_location_translations', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('location_id');
    $table->string('locale')->index();
    $table->string('name')->nullable();
    $table->text('address')->nullable();
    $table->unique(['location_id', 'locale'], 'bl_trans_unique');
    $table->foreign('location_id', 'bl_trans_location_fk')->references('id')->on('booking_locations')->cascadeOnDelete();
});

models

class Location extends Model implements TranslatableContract
{
    use Translatable;

    public $translatedAttributes = ['name', 'address', 'description'];
    public $translationModel = LocationTranslation::class;
}

class LocationTranslation extends Model
{
    public $timestamps = false;
    protected $fillable = ['name', 'address', 'description'];
    protected $table = 'booking_location_translations';
}

Media

php (pattern from Post)

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Gallery extends BaseModel implements HasMedia
{
    use HandlesMediaUploads, InteractsWithMedia;

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('cover')
            ->singleFile()
            ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp'])
            ->withResponsiveImages();
    }
}

// controller
$upload = $gallery->replaceMediaFromRequest($request, 'cover_image', 'cover');   // trait helper
if (! $upload['success']) {
    return back()->with('error', $upload['message'])->withInput();
}

// view
<img src="{{ $gallery->getFirstMediaUrl('cover') }}" alt="">

Migrations

Seeders and factories

Money, dates and units

Querying safely