Designing a Flexible URL & Content Model
Website Rebuild with Laravel
Chapter 2 · Designing a Flexible URL & Content Model
This is the third time this exact design question gets asked on this site — Website Rebuild with Next.js answered it with Prisma, Website Rebuild with Django answered it with the ORM, and both landed on the identical hybrid: a real adjacency list for structure, plus a stored materialized path for fast reads. That two independent frameworks reached the same answer wasn't a coincidence of either framework's own bias — it's a property of this specific site's own read-heavy, write-rare traffic. Watch Laravel arrive at the same place a third time, independently, for the same reason.
Where Does the Schema Actually Live? A Real Difference
Django's Page model declares its own fields directly in the class — title = models.CharField(...) — and the migration is auto-generated from that class. Prisma works the same way: schema.prisma declares the fields, and a migration is generated from the schema file. Laravel inverts this entirely.
The Migration: Hand-Written, and the Real Source of Truth
The -m flag generates a migration alongside the model in one step. ->restrictOnDelete() is Laravel's own direct parallel to Django Rebuild 2's own on_delete=PROTECT decision — explicitly refusing a deletion while referenced rows still exist, made explicit here on purpose rather than left to the database's own implicit default behavior.
The Eloquent Model: Two Relationships, Not One Field
Django's single ForeignKey('self', related_name='children') field declaration gave both directions — page.parent and page.children.all() — from one line. Eloquent needs two separate, explicitly written relationship methods to get the identical two-directional access: parent() reads upward, children() reads downward, and neither one implies the other automatically.
Recomputing full_path: A Model Event, Not an Overridden save()
Django recomputes full_path by directly overriding save() — an imperative method replacement. Laravel reaches the identical practical result through a genuinely different mechanism: model events. static::saving(...) registers a closure to run automatically every time a Page is about to be saved, without touching save() itself at all. Eloquent fires a whole family of these (creating, created, saving, saved, deleting, and more) — an event-listener pattern, not a method override.
$fillable can be set through Page::create([...]) or $page->fill([...]) — a genuinely Laravel-specific convention with no direct one-line equivalent in Django or Prisma, guarding against a real class of bug where unexpected form input silently overwrites a column nobody meant to expose.
Hands-On Exercises
Explain the real difference in "where the schema lives" between Django's Page model, Prisma's schema.prisma, and Laravel's own migration-plus-Eloquent-model pair.
Explain why Page.php needs two separate relationship methods, parent() and children(), to get the same two-directional access Django's single ForeignKey('self', related_name='children') field provided from one declaration.
Explain why full_path is recomputed inside a static::saving() closure rather than by overriding a save() method directly, and what "model event" means in this context.
Chapter 2 Quick Reference
- The same hybrid, a third time — adjacency list plus materialized path, independently reached by Prisma, Django, and now Eloquent
- The schema lives in the migration, not the Eloquent model class — a real, practical difference from Django and Prisma
php artisan make:model Page -m— generates a model and its migration together->restrictOnDelete()— Laravel's own explicit parallel to Django'son_delete=PROTECTparent()/children()— two explicit relationship methods, replacing Django's single self-referencing field- Model events (
static::saving()) — Eloquent's own mechanism for "run this before every save," genuinely different from overridingsave() $fillable— Laravel's own mass-assignment protection, with no direct Django/Prisma equivalent- Next chapter: Routing: Laravel's Own Mechanism for Arbitrary-Depth Routing