When CRUD Tables Are No Longer Enough

Published: (January 9, 2026 at 04:43 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Cover image for “When CRUD Tables Are No Longer Enough”

giuliopanda

Anyone who has built an admin panel in PHP knows how this story usually ends. At the beginning, everything is simple: a table, a form, a couple of filters. Then real requirements arrive — relationships between data, more complex flows, partial updates — and the classic “table + form” CRUD starts to crack.

It’s not CRUD’s fault. The problem is that admin interfaces are not just CRUD.

A real admin needs to:

  • see data in different contexts,
  • navigate relationships without losing state, and
  • edit related entities quickly.

These reflections led me to explore different approaches, until I decided to build a system from scratch, outside of the most popular frameworks. This is where I ended up.

A Different Way to Think About the Problem

Instead of thinking “I need to build a page”, I started thinking “I need to describe a view of this data.”

That led me to builders: PHP objects that describe what to display and how. They are not frontend components, not magic widgets — just PHP structures that generate HTML, handle queries, and return either HTML or JSON responses.

The code stays PHP. The flow stays request → processing → response.

A Concrete Example: Posts

Let’s start with the simplest case: a module to manage posts with a title and content.

The Model defines the data structure

class PostsModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule->table('#__posts')
            ->id()
            ->string('title')->index()
            ->text('content')->formType('editor');
    }

    #[Validate('title')]
    public function validateTitle($current_record_obj): string
    {
        $value = $current_record_obj->title;
        if (strlen($value) model, 'idTablePosts')
            ->field('content')->truncate(50)
            ->field('title')->link('?page=posts&action=edit&id=%id%')
            ->setDefaultActions();

        $response = array_merge($this->getCommonData(), $tableBuilder->getResponse());
        Response::render(MILK_DIR . '/Theme/SharedViews/list_page.php', $response);
    }

    #[RequestAction('edit')]
    public function postEdit()
    {
        $response = $this->getCommonData();
        $response['form'] = FormBuilder::create($this->model, $this->page)->getForm();
        Response::render(MILK_DIR . '/Theme/SharedViews/edit_page.php', $response);
    }
}

TableBuilder already knows how to handle search, pagination, and sorting. FormBuilder knows which fields to show and how to validate them. You describe what you want — the builders generate the rest.

Screenshots

Posts list view

Post edit view

Views are plain PHP templates — no templating engines, no special syntax. If you need to change something, you open the file and edit the HTML directly.

Where Things Get Complicated: Relationships

Simple CRUD works everywhere. Problems start when entities are related, e.g.:

Recipes
 └─ Comments

When relationships become interactive, the usual options are either:

  • splitting everything into multiple pages (losing fluidity), or
  • relying on heavy frameworks that manage state for you (losing transparency and control).

Tools like Laravel Nova or Filament are powerful and work well in structured teams. But when flows become very specific, it can be hard to understand what is really happening between backend and frontend.

I wanted a different approach: keep everything in PHP, use fetch to avoid page reloads, but without hiding what’s going on.

Recipes: CRUD with Relationships, Fully in Fetch

This is the full example: recipes with comments, two interface levels, no page reloads.

Recipe model

class RecipeModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule->table('#__recipes')
            ->id()
            ->hasMany('comments', RecipeCommentsModel::class, 'recipe_id')
            ->image('image');
    }
}
title('name')->index()
    ->text('ingredients')->formType('textarea')
    ->select('difficulty', ['Easy', 'Medium', 'Hard']);
}

Comments model

class RecipeCommentsModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule->table('#__recipe_comments')
            ->id()
            ->int('recipe_id')->formType('hidden')
            ->text('comment');
    }
}

Everything else — lists, off‑canvas forms, modals, automatic refresh — is built using the same builders and the same mental model.

MilkAdmin demo

Why This Approach

This is not another framework. It’s a way of thinking about admin panels.

  • Builders reduce boilerplate, but the code remains readable PHP.
  • If a builder doesn’t do what you need, you can always drop down to plain PHP.
  • JSON as a contract between backend and frontend is easy to debug – no hidden state, no complex lifecycles. PHP sends instructions, the browser executes them.
  • Most importantly, relationships are handled with the same tools as basic CRUD. There’s no separate system to learn just for relations.

Who This Is For

If you work with Laravel and you’re happy with it, this is probably not for you.

But if you find yourself in one of these situations:

  • You maintain existing PHP projects that can’t be migrated.
  • You work on internal tools, CRMs, or management systems.
  • You need to understand the code quickly, even months later.
  • You don’t want to chase complex ecosystems.
  • You want PHP that looks and feels like PHP.

Then it might be worth rethinking how we build admin interfaces.

Admin panels are not public websites. They are work tools. The best tools do their job well, remain understandable over time, and don’t force you to rewrite everything when requirements change.

These reflections come from the development of MilkAdmin, a PHP admin panel system I actively maintain. Code, demos, and documentation are available at milkadmin.org. MIT licensed.

https://github.com/giuliopanda/milk-admin

Back to Blog

Related posts

Read more »

Laravel FAQs (Beginner to Advanced)

Why Laravel still matters and what this FAQ solves Laravel remains one of the fastest ways to ship secure, maintainable PHP applications — from simple sites to...