<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->morphs('mediable');
$table->string('disk')->default('public');
$table->string('path');
$table->string('filename');
$table->string('mime_type');
$table->unsignedBigInteger('size')->default(0);
$table->json('variants')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('media');
}
};
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\Storage;
class Media extends Model
{
protected $fillable = [
'disk', 'path', 'filename', 'mime_type', 'size', 'variants',
];
protected $casts = [
'variants' => 'array',
'size' => 'integer',
];
public function mediable(): MorphTo
{
return $this->morphTo();
}
public function url(?string $variant = null): string
{
$path = $variant ? ($this->variants[$variant] ?? $this->path) : $this->path;
return Storage::disk($this->disk)->url($path);
}
}
<?php
namespace App\Models\Concerns;
use App\Jobs\ResizeMediaJob;
use App\Models\Media;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Http\UploadedFile;
trait HasMedia
{
public function media(): MorphMany
{
return $this->morphMany(Media::class, 'mediable');
}
public function attachMedia(UploadedFile $file, string $disk = 'public'): Media
{
$path = $file->store('media/originals', $disk);
$media = $this->media()->create([
'disk' => $disk,
'path' => $path,
'filename' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
if (str_starts_with((string) $media->mime_type, 'image/')) {
ResizeMediaJob::dispatch($media);
}
return $media;
}
}
<?php
namespace App\Jobs;
use App\Models\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class ResizeMediaJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
protected array $sizes = [
'thumb' => 200,
'medium' => 800,
];
public function __construct(public Media $media)
{
}
public function handle(): void
{
$disk = Storage::disk($this->media->disk);
if (! $disk->exists($this->media->path)) {
return;
}
$variants = [];
$source = $disk->get($this->media->path);
$basename = pathinfo($this->media->path, PATHINFO_FILENAME);
foreach ($this->sizes as $name => $width) {
$image = Image::make($source)->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$variantPath = "media/{$name}/{$basename}.jpg";
$disk->put($variantPath, (string) $image->encode('jpg', 85));
$variants[$name] = $variantPath;
}
$this->media->update(['variants' => $variants]);
}
}
This snippet shows how a single media table can be attached to any Eloquent model through a polymorphic relationship, with the actual image resizing pushed onto a queue so uploads stay fast. The polymorphic approach avoids one join table per model type — instead of post_media, user_media, and so on, every attachment lives in one place and points back to its owner through two columns.
In create_media_table migration, the schema defines those two columns via $table->morphs('mediable'), which generates mediable_id and mediable_type plus a composite index. The row also carries a disk, the stored path, the original filename, a mime_type, and a nullable variants JSON column where resized derivatives are recorded once the job finishes. Keeping variants as JSON means new sizes can be added without a migration.
The Media model declares the inverse side with mediable() returning a morphTo relation, so $media->mediable resolves to whatever model owns it. The HasMedia trait is the reusable other half: any model that uses it gains a media() morphMany relation and an attachMedia() helper. That helper calls store() on the uploaded file to persist the original, creates the Media row, and then dispatches ResizeMediaJob — the write to storage and the database happen synchronously, but the CPU-heavy resize is deferred.
ResizeMediaJob implements ShouldQueue and receives only the Media id-bearing model, relying on Laravel's model serialization so the freshest row is loaded on the worker. It reads the original off the configured disk, generates each named size with the Intervention Image library, writes the derivative back to storage, and records the resulting paths in the variants column in one update(). Because the job is idempotent-ish — it overwrites the same variant paths — a retry simply regenerates them, which matters since queued jobs can run more than once.
The trade-off is eventual consistency: right after upload, variants is empty until the worker catches up, so views should fall back to the original path. This pattern fits any app where many model types need attachments and where resizing large images would otherwise block the request cycle.
Related snips
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
import cv2
image = cv2.imread('receipt.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresholded = cv2.adaptiveThreshold(
OpenCV image preprocessing for OCR and vision pipelines
class CreateTopSellersMv < ActiveRecord::Migration[7.0]
def up
execute <<~SQL
CREATE MATERIALIZED VIEW top_sellers AS
SELECT p.id AS product_id,
p.name AS product_name,
Cache-Friendly “Top N” with Materialized View Refresh
Share this code
Here's the card — post it anywhere.