Skip to content

Data ​

Introduction ​

kitloom keeps its data where WordPress keeps it: posts, meta, terms, options, users — the same tables, the same keys. A module follows the same rule for its own data: a custom field is post meta, a setting is an option, and a module that replaces a WordPress plugin reads and writes that plugin's tables and keys.

Models ​

The WordPress tables are Eloquent models in kitloom/wp-schema, looked up by key rather than by class:

php
use Kitloom\WpSchema\WpSchema;

WpSchema::query('post')->where('post_type', 'book')->get();   // Builder<Post>
WpSchema::model('term_taxonomy');                              // the class
WpSchema::make('post_meta');                                   // a new instance
KeyTablePackage model
post, attachmentpostsModels\Post, Models\Attachment
post_metapostmetaModels\PostMeta
term, term_taxonomy, term_metaterms, term_taxonomy, termmetaModels\Term, Models\TermTaxonomy, Models\TermMeta
comment, comment_metacomments, commentmetaModels\Comment, Models\CommentMeta
user, user_metausers, usermetathe application's user, Models\UserMeta
optionoptionsModels\Option

Why by key: an application almost always extends the package models and adds its own behaviour. Relations inside the packages ask the registry for the class, so $post->meta returns the application's meta model and nothing it added is lost.

The ModelRegistry ​

Kitloom\WpSchema\ModelRegistry holds the class for each key. Three parties register classes — the packages, the application's config('kitloom-wp-schema.models'), a module or a theme with keys of its own — and they must form one line of inheritance:

  • the most specific class wins, whatever order they register in;
  • two classes where neither extends the other are an error, not a silent loss of one of them;
  • a class for a WordPress key must extend the package model (App\Models\Post extends Kitloom\WpSchema\Models\Post). user is free: the application's user extends Authenticatable and takes WordPress's behaviour from the IsWordPressUser trait.
php
// config/kitloom-wp-schema.php in the application
'models' => [
    'post' => App\Models\Post::class,
],

A module's own models ​

A module puts its models in the registry under keys of its own, so an application can swap them, and reads the class back from the registry everywhere — in its admin resources too:

php
private function registerModels(): void
{
    $register = static function (ModelRegistry $registry): void {
        if (! $registry->has(Review::KEY)) {
            $registry->use(Review::KEY, Review::class);
        }
    };

    $this->app->resolving(ModelRegistry::class, $register);

    if ($this->app->resolved(ModelRegistry::class)) {
        $register($this->app->make(ModelRegistry::class));
    }
}
php
// Filament resource
public static function getModel(): string
{
    return WpSchema::model(Review::KEY);
}
php
// Nova provider: the resource's model follows the registry
$resource::$model = $this->app->make(ModelRegistry::class)->class(Review::KEY);

A module's records that are posts of its own type are a model extending Post with a global scope on post_type — what kitloom:make-module --post-type generates — declared to the site with PostTypeObject(model: Review::class).

Meta ​

Reading ​

php
$post->getMetaValue('rating');          // ?string — null when there is no such meta
$term->getMetaValue('color');           // term_taxonomy rows have meta too
$post->meta;                            // all rows, through the registry's post_meta model

getMetaValue() reads from the loaded meta relation. Eager-load it for a list — WpSchema::query('post')->with('meta') — or each post runs a query of its own.

Meta values are strings, as WordPress keeps them. Turn them into types on the model, with an accessor:

php
protected function rating(): Attribute
{
    return Attribute::get(fn (): int => (int) $this->getMetaValue('rating'));
}

Custom fields made with kitloom/custom-fields are read with CustomFields::get($post, 'subtitle') or $post->acf_subtitle, which decode them by their field type.

Writing ​

In a Filament form, bind the field to a meta key — it reads, writes, and deletes the meta when the field is emptied:

php
MetaField::bind(TextInput::make('rating')->numeric(), 'rating');

// with conversions
MetaField::bind(
    Toggle::make('featured'),
    'featured',
    toState: fn (?string $value): bool => $value === '1',
    toStored: fn (mixed $value): string => $value ? '1' : '0',
);

In a Nova post resource, list the meta in $postMeta (request key => meta key) and the resource writes it after the post is saved. Elsewhere:

php
use Kitloom\WpSchema\Filament\Forms\MetaField;

MetaField::write($post, 'rating', '4');     // null deletes it

or the meta model itself:

php
WpSchema::query('post_meta')->updateOrCreate(
    ['post_id' => $post->ID, 'meta_key' => 'rating'],
    ['meta_value' => '4'],
);

Naming meta keys ​

  • A key WordPress or a plugin already uses — use it as is, so migrated data keeps working: _thumbnail_id, rank_math_title, _price.
  • A key of your own: short and plain (rating). Start it with _ when editors should not see it as a custom field — WordPress hides _-prefixed meta from its Custom Fields box.
  • Name it once, as a constant.

Options ​

Site settings are rows of the options table — wp_options on a site moved from WordPress.

php
use Kitloom\Settings\Facades\Settings;

Settings::get('blogname');
Settings::get('reviews_out_of', 5);       // the stored value, then a declared default, then this one
Settings::array('reviews_sources');        // an array, whether stored as JSON or serialized
Settings::has('reviews_out_of');

Options are read in one query and cached; Settings::refresh() drops the cache, and the writer below refreshes it for you. Read options at the moment you need them, not in a constructor of a singleton — under Octane the singleton outlives the request.

Writing:

php
use Kitloom\Settings\SettingWriter;

app(SettingWriter::class)->put('reviews_out_of', 10);
app(SettingWriter::class)->put('reviews_sources', ['imdb', 'goodreads']);   // stored as JSON
app(SettingWriter::class)->forget('reviews_out_of');

A settings page in either admin does this for you: each field writes the option named by the field.

Name options after the module (reviews_out_of, kitloom_inactive_modules), never with the table prefix — that is the connection's.

How arrays are stored ​

WordPress stores arrays with PHP's serialize(). kitloom reads that format and writes JSON:

ReadsWrites
Arrays and mapsJSON, or a:2:{…} as WordPress wrote itJSON
Booleans1/01/0

So a site moved from WordPress works as it is, and a value becomes JSON the next time it is saved. Serialized data is read with allowed_classes => false: a stored value cannot create objects.

Use the helpers instead of unserialize()/json_decode():

php
use Kitloom\WpSchema\Support\StoredArray;

StoredArray::decode($stored);    // ?array — null when it holds no array
StoredArray::toArray($stored);   // array — empty when it holds none
StoredArray::encode($array);     // JSON

Settings::array() and SettingWriter do the same for options. A module that keeps a plugin's data — Rank Math's redirect sources, ACF's field settings — reads both formats and writes JSON like the rest.

The one exception is php artisan wordpress:install, which writes WordPress's own format on purpose: it prepares a database for WordPress itself.

Tables of your own ​

When a module's data is not posts or meta, give it a table — a migration that creates it only when it is missing, so a site that had the plugin keeps its rows:

php
if (! Schema::hasTable('rank_math_redirections')) {
    Schema::create('rank_math_redirections', function (Blueprint $table): void {
        // the plugin's columns, as the plugin creates them
    });
}
  • Name it like the plugin it replaces, or after the module. The connection adds the site's prefix.
  • Put the model in the registry under a key of your own.
  • Store arrays in it as JSON, like everywhere else.