Admin CRUD Interface
Website Rebuild with Laravel
Chapter 10 · Admin CRUD Interface
Django Rebuild 10 got a working CRUD interface almost for free — a single admin.site.register(Page) call, thanks to Django's own admin app introspecting the model and generating list, edit, and delete views automatically. Laravel ships nothing like that. This is a real, honest philosophical difference, not just a missing convenience method — and it's where this chapter finally pays off Chapter 2's own deferred cost: recomputing full_path for an entire subtree when a page is reparented.
No Free Admin — A Real Philosophical Difference
Laravel's own official ecosystem does offer admin-panel packages — Nova (paid, first-party) and Filament (free, community-maintained) both generate CRUD interfaces from model definitions, genuinely comparable in spirit to Django's own built-in admin. But neither ships with the framework itself the way Django's admin app does out of INSTALLED_APPS — installing one is an explicit, separate decision, not a default. This chapter builds the interface by hand instead, matching Next.js Rebuild 10's own approach and, more importantly, because hand-building it is the only way to actually teach the reparenting logic this chapter needs to cover.
Listing Pages: The Same Searchable Flat List
Next.js Rebuild 10 solved its own parent-picker problem — choosing a new parent for a page at arbitrary depth — with a searchable flat list rather than a nested tree widget, for an explicit, scale-justified reason: at this site's realistic page count, a flat list with client-side filtering is simpler to build and just as fast to use as a tree view, and a tree view's own advantage (seeing hierarchy at a glance) matters less than being able to type a few letters and jump straight to a page. That reasoning carries over unchanged here.
The page itself is excluded from its own candidate-parent list — reparenting a page under itself, or under one of its own descendants, would corrupt the tree. A simple client-side <input> filters the <select> options by full_path substring, giving fast lookup without a full tree-rendering component.
Paying Off Chapter 2's Deferred Cost: movePage()
save() once, then re-triggering every descendant's own save() in breadth-first order, so each node recomputes its own full_path from its own parent without any code explicitly walking the tree. Laravel's Eloquent models don't have an equivalent built-in cascade for a computed field like this, so updateDescendantPaths() walks the subtree explicitly and recursively instead. Same real cost, same result, genuinely different mechanism — Django's cascade lives inside the model's own save() override; Laravel's lives in a dedicated method that's called on purpose.
Refusing, Not Cascading, on Delete
Chapter 2's own restrictOnDelete() constraint means the database itself refuses to delete a page with existing children — the raw failure is a QueryException from a foreign-key violation. Catching it here and returning a plain, readable error message turns that low-level database refusal into an honest, expected UI outcome, rather than a raw stack trace reaching the admin. Deliberately no automatic cascading delete: reparenting or removing an entire subtree is left as a conscious, separate action.
Three Admin Interfaces, Compared
| Next.js | Django | Laravel | |
|---|---|---|---|
| Free admin panel | None — built by hand | admin.site.register(), generated automatically | None by default — Nova/Filament exist as separate packages |
| Reparent cascade | Explicit movePage() walk | Implicit — re-triggered save() cascade | Explicit movePage() + recursive updateDescendantPaths() |
| Delete-with-children | Refused, error surfaced in the UI | Refused via on_delete=PROTECT, surfaced in the admin | Refused via restrictOnDelete(), caught and surfaced |
Hands-On Exercises
Explain the real, honest philosophical difference between Django's free automatic admin and Laravel's total absence of one, and why this chapter builds the interface by hand rather than reaching for a package like Filament.
📄 View solutionExplain how Page::movePage() recomputes full_path for the moved page and all of its descendants, and how this differs mechanically from Django Rebuild 10's own breadth-first save() re-trigger approach.
Explain why deleting a page with children fails with a friendly error instead of silently cascading, and which earlier chapter's decision this behavior traces back to.
📄 View solutionChapter 10 Quick Reference
- No free admin by default — Nova (paid) and Filament (free) exist as separate packages, not a framework default like Django's
adminapp - Searchable flat list — same parent-picker approach as Next.js Rebuild 10, for the same scale-justified reason
movePage()— updates the moved page's ownfull_path, then callsupdateDescendantPaths()updateDescendantPaths()— explicit recursive walk, Laravel's own answer to Django's implicitsave()-cascade mechanism- Delete refusal — Chapter 2's
restrictOnDelete()throws aQueryException, caught here and turned into a readable UI error - Next chapter: Deployment