Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
3.1 KiB
PHP
81 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
/**
|
|
* What is actually known about an uploaded image.
|
|
*
|
|
* Until now an image was a string — a path, and nothing else. For a frontend
|
|
* rendering server-side that is not enough to render one properly: without
|
|
* width and height the page reflows as images arrive, and without derived
|
|
* versions every visitor downloads a 2000px original to fill a 400px slot.
|
|
*
|
|
* This table is *derived*: everything in it can be rebuilt from the files on
|
|
* disk with `php artisan media:process --all`. Nobody edits it by hand, which
|
|
* is why the human part of an image — its alt text, which depends on where the
|
|
* image is used — lives on the record using it instead.
|
|
*/
|
|
return new class extends Migration
|
|
{
|
|
public function up(): void
|
|
{
|
|
Schema::create('media', function (Blueprint $table) {
|
|
$table->id();
|
|
|
|
// Identity is (disk, path): the file is the record. Two models
|
|
// pointing at the same upload share one row and one set of
|
|
// derived versions.
|
|
$table->string('disk', 40);
|
|
$table->string('path', 500);
|
|
|
|
$table->string('mime', 100)->nullable();
|
|
$table->unsignedBigInteger('size')->nullable();
|
|
|
|
// The pair that stops the page reflowing as images load.
|
|
$table->unsignedInteger('width')->nullable();
|
|
$table->unsignedInteger('height')->nullable();
|
|
|
|
// Tiny blurred data URI, inlined by the frontend while the real
|
|
// image loads.
|
|
$table->text('placeholder')->nullable();
|
|
|
|
// [{ "format": "webp", "width": 800, "path": "...", "size": 1234 }]
|
|
$table->jsonb('variants')->nullable();
|
|
|
|
// Null means "not processed yet". The API serves the image anyway,
|
|
// just without the extras, so an unprocessed upload degrades into
|
|
// a plain <img> rather than a hole in the page.
|
|
$table->timestamp('processed_at')->nullable();
|
|
$table->text('error')->nullable();
|
|
|
|
$table->timestamps();
|
|
|
|
$table->unique(['disk', 'path']);
|
|
$table->index('processed_at');
|
|
});
|
|
|
|
// Alt text belongs to the record showing the image, not to the file:
|
|
// the same photo can need a different description in a project page
|
|
// than in a listing, and only the context knows which.
|
|
Schema::table('pages', function (Blueprint $table) {
|
|
$table->jsonb('featured_image_alt')->nullable()->after('featured_image');
|
|
});
|
|
|
|
Schema::table('projects', function (Blueprint $table) {
|
|
$table->jsonb('cover_image_alt')->nullable()->after('cover_image');
|
|
});
|
|
}
|
|
|
|
public function down(): void
|
|
{
|
|
Schema::table('projects', fn (Blueprint $table) => $table->dropColumn('cover_image_alt'));
|
|
Schema::table('pages', fn (Blueprint $table) => $table->dropColumn('featured_image_alt'));
|
|
|
|
Schema::dropIfExists('media');
|
|
}
|
|
};
|