Skip to content

What Is WordPress Here, What Is Laravel ​

Introduction ​

kitloom keeps its content in the WordPress database and handles a request the way WordPress does. Everything else — the code, the models, the admin, the API — is Laravel. This page draws the line, concept by concept, with the price of each WordPress-shaped part and the way around it when it is in your way.

The short version: the storage is WordPress's; the code never is. There is no PHP from WordPress, no hooks, no wp-admin. Where WordPress names show, they are the column or the option a site brought over, and the public API wraps them in Laravel names.

The map ​

ConceptWhere it livesWhat is WordPressWhat is LaravelThe priceThe way around
A record of contentwp_poststhe table, its columns, the statuses (publish, draft, trash…)an Eloquent model, Post, with aliases (title, slug, status, published_at), writers, events, global scopesa type's model must extend Post; a record that is not a row of wp_posts gets no address from the main query — only from a route of your ownkeep the row, put your fields in tables of your own
Fields of a recordwp_postmetaone row per key, the value a string, ACF's layout for field groupsgetMeta(), getMetaInt(), setMeta(), $post->field(), the Fields facade; all keys of a record read in one query, with('meta') to load them with the listwrites are two queries a key; filtering and sorting by a field joins an unindexed text columna hasOne details table with real columns; meta for what came from WordPress
Categories, tags, taxonomieswp_terms, wp_term_taxonomy, wp_term_relationshipsthe three-table split, term_id and term_taxonomy_idTerm and TermTaxonomy models, TermWriter, $post->category, $post->termsIn('genre'), $post->termTaxonomiestwo ids for one thing; a join for every term lookupTermTaxonomy reads name and slug through its term, and the post's taxonomy attributes hide the rest; nothing to do
Optionswp_optionsthe table, option_name, every array as serialize() writes it — WordPress's own and its plugins'Settings::get(), Settings::array(), SettingWriter, SettingSaved; the autoloaded options in one query, cached, the rest by name; arrays written as JSONtwo formats to read; an array kitloom has saved is JSON, which a WordPress still running on the same database no longer readsread as arrays only, never as objects; the site moves one way
Users and sign-inwp_users, wp_usermetathe table, WordPress's password hashesthe application's User with the IsWordPressUser trait, the wordpress auth driver and hasher (phpass and WordPress 6.8's bcrypt), the Two-Factor modulethe users table is not Laravel's usersa user signing in keeps their password; nothing to do
Who may do whatwp_usermeta ({prefix}capabilities) for the user's role, mirrored for WordPress; the permission tables for what the role may dothe capability names: edit_posts, manage_options, edit_theme_options; map_meta_cap()Laravel's Gate, a roles engine on spatie/laravel-permission behind its own contracts, Permission providers per module, the Roles screenWordPress names in every can()constants on the modules' classes (Shop::CAPABILITY_ORDERS), admin sections asked by class; a module declares its own permissions with any name
Arrays in the databaseevery tableserialize() for every array, WordPress's own and its plugins'; 1/0 flagsJSON, StoredArraytwo formats to readwritten as JSON once touched
Table namesevery tablethe wp_ prefixthe connection prefix, also in raw SQLthe prefix in every raw querythe connection prefix; the models never see it
An address—permalink settings, rewrite rules, query vars, the canonical 301Route::fallback(), events (RewriteRulesBuilding, MainQueryResolved), Site::resolve()a route of your own wins over the fallback, nothing else to learnyour routes first, the site answers the rest
What a page shows—the main query and its conditional tags, the template hierarchy (single-book, archive, index)page parts, typed objects with toArray() and a JSON Schema (kitloom:schema writes TypeScript from them), Blade or Vue through Inertia, JSON or Markdown for every pagetemplate names follow WordPress's lista TemplateCandidates listener adds yours
Widgets and menussidebars_widgets, widget_*, nav_menu termsthe options and the term tree a WordPress site broughtthe menus, menuLocations and widgets page parts, zones and locations added in the adminthe widget settings are option rowsthe parts give them drawn; nothing to do
The REST API—wp/v2, application passwords, X-WP-TotalLaravel routes and controllers under /wp-json/, RestNamespace per module, WordPress's error shapeWordPress's JSON for the core routesa namespace of your own with any shape
Plugin datathe plugin's tables and metaACF field groups, Rank Math meta and redirects, Contact Form 7 forms and Flamingo messages, WooCommerce products and orders, Slide Anything, Nextend Social Logina module per format, with models, writers and admins of its ownthe module's storage is the plugin'sthat is the point: the site moves with its data
The code—nothingservice providers, contracts and container tags, Eloquent, events, queues, Horizon, Pulse, Octane-ready, Pint, PHPStan, PHPUnit——

Why a type's model extends Post ​

A post type of your own — a theme's recipe, a module's review — is read with a model you name, and that model extends Kitloom\WpSchema\Models\Post. The site refuses one that does not (PostTypeObject). The reason is not WordPress; it is where the site keeps everything a record needs:

  • meta and custom fields — getMeta(), field(), the Custom Fields module;
  • extensions — what modules add to every post with WpSchema::extend('post', …): attributes, relations, scopes, observers — Languages' translated title and text, the events' own observers;
  • capabilities — edit_post, read_post, delete_post of this record, mapped as WordPress maps them;
  • the main query — statuses, visibility, the preview of a draft, the canonical address, the template hierarchy;
  • writers and events — PostWriter, PostSaved, PostTrashed, PostDeleted, everything that listens to them — search, the activity log, SEO, import and export — and everything that writes through them: the REST API, the AI tools.

A model that is not a Post would have to carry all of that itself, and every module would have to know about it. Extending Post is the price of getting it for free.

What it does not mean: that your fields belong in wp_postmeta.

What it costs, and why it stays for now ​

The base class has costs, and they were weighed rather than missed:

  • One base class. A model that already extends another class — a package's model, a base model of your application — cannot be a type's model as it is. It becomes a Post, or it sits beside one: the post is the record, your model a relation of it.
  • A large parent. Post brings meta, fields, extensions, the featured image, column defaults and the model registry with it. A type inherits all of it, whether it uses it or not.
  • wp_posts underneath. A type's record is always a row of that table, with its columns. What the type adds goes into tables of its own (below), not instead of the row.

The alternative is a contract: an interface, with the behaviour in traits that any Eloquent model over wp_posts could take, and Post as the default implementation. It is not built now, on purpose:

  • Tables of your own already give a type its own shape. A type's data lives in its own tables, on relations, with indexes, while the record keeps its address, permissions, comments, REST API and agent tools (below). That is the freedom most types need.
  • The change reaches everything that reads a post. That is every package that shows, writes, searches, translates or exports content. It also includes the parts most likely to break: the visibility scopes, the capability mapping, per-request state under Octane. Each would need tests on a model that is not a Post before anything moves.
  • Waiting closes no door. Adding a contract later breaks nothing written today: Post would implement it, so every model that extends Post would keep passing every check.

What would change the decision: a case a relation cannot cover — a model that must keep another base class and still be read by the main query itself.

The way around meta ​

Meta is the right place for what came from WordPress and what WordPress-format modules expect there: the featured image, SEO's title and description, ACF field groups a site brought over. It is the wrong place for the fields of a type you are building now and will filter and sort by. For those, keep the row in wp_posts and put the fields in tables of the theme or module, on Eloquent relations:

php
final class Recipe extends Post
{
    /** One row per recipe, real columns, indexed: servings, minutes, difficulty. */
    public function details(): HasOne
    {
        return $this->hasOne(RecipeDetails::class, 'post_id');
    }

    public function ingredients(): HasMany
    {
        return $this->hasMany(Ingredient::class, 'post_id')->orderBy('sort');
    }

    public function equipment(): BelongsToMany
    {
        return $this->belongsToMany(Equipment::class, 'recipe_equipment', 'post_id', 'equipment_id');
    }
}

The post keeps its title, text, slug, address, status, author, terms and permissions; the archive filters and sorts on recipe_details with indexes; the page parts load the relations with the post. Saffron does this for a whole type: a recipe's card is one row of typed, indexed columns, its ingredients and steps a row each, and the archive filters and sorts on them; Game Reviews keeps its player ratings the same way. A theme of your own gets the same shape from the generator with --storage=tables. The migrations, the model registry and the rules are on Data & Models → Tables of your own.

What reads meta does not see these tables: Custom Fields, the WXR export, the REST API, Search & Replace (unless the theme declares its tables to it), the generic AI tools. A type on tables of its own gives the REST API its own fields and agents its own tool, the way a module does; the export of those rows is a command of the theme's own.

Names ​

The rule the packages follow, and the one for code you write on them: WordPress names stay in the storage layer. The public API speaks Laravel: $post->title, not post_title; Settings::get(), not get_option(); Menus::get('main'), not wp_nav_menu(); $post->status, with the constants of PostContract, not the string 'publish' typed out. Queries, raw rows and import/export keep the columns, because that is what they are.

Next ​

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]