Usage

Defining Models

Every model extends the abstract Model class and declares at minimum a table name:

<?php

declare(strict_types=1);

namespace App\Models;

use WPZylos\Framework\Model\Model;

class Order extends Model
{
    /**
     * The table name without WordPress prefix.
     */
    protected static string $table = 'orders';

    /**
     * Mass-assignable attributes.
     */
    protected array $fillable = ['customer_id', 'total', 'status', 'notes'];

    /**
     * Attribute casts.
     */
    protected array $casts = [
        'customer_id' => 'integer',
        'total'       => 'float',
        'is_paid'     => 'bool',
        'metadata'    => 'json',
    ];

    /**
     * Attributes hidden from serialization.
     */
    protected array $hidden = ['internal_notes'];
}

Model Properties

PropertyTypeDefaultDescription
$tablestring''Table name without WP prefix
$primaryKeystring'id'Primary key column
$fillablearray[]Mass-assignable attributes
$guardedarray['id']Non-mass-assignable attributes
$castsarray[]Attribute type casts
$hiddenarray[]Hidden from serialization
$timestampsbooltrueAuto-manage timestamps
$createdAtColumn?string'created_at'Created timestamp column
$updatedAtColumn?string'updated_at'Updated timestamp column

CRUD Operations

Create

// Method 1: Create and persist in one step
$order = Order::create([
    'customer_id' => 42,
    'total'       => 149.99,
    'status'      => 'pending',
]);

// Method 2: Create instance, then save
$order = new Order(['customer_id' => 42, 'total' => 149.99]);
$order->status = 'pending';
$order->save();

Read

// Find by primary key
$order = Order::find(1);

// Find or throw RuntimeException
$order = Order::findOrFail(1);

// Get all records
$orders = Order::all(); // Returns ModelCollection

// Query with conditions
$pending = Order::where('status', 'pending')->get();

Update

// Method 1: Update with array
$order->update(['status' => 'shipped', 'total' => 199.99]);

// Method 2: Set attributes and save
$order->status = 'shipped';
$order->save();

Delete

$order->delete();

Attribute Casting

Define $casts to automatically convert attributes on get:

protected array $casts = [
    'quantity'   => 'int',       // also: 'integer'
    'price'      => 'float',     // also: 'double', 'real'
    'is_active'  => 'bool',      // also: 'boolean'
    'code'       => 'string',
    'tags'       => 'array',     // JSON string → array
    'metadata'   => 'json',      // JSON string → array
    'created_at' => 'datetime',  // String → DateTimeImmutable
];

Casting also works in reverse when serializing for database storage:

  • bool0 or 1
  • array/json → JSON string
  • datetimeY-m-d H:i:s string

Accessors & Mutators

Accessors

Define a getXxxAttribute method to transform a value when reading:

public function getFullNameAttribute($value): string
{
    return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}

// Usage
echo $user->full_name; // "John Doe"

Mutators

Define a setXxxAttribute method to transform a value when writing:

public function setEmailAttribute($value): void
{
    $this->attributes['email'] = strtolower(trim($value));
}

// Usage
$user->email = '  [email protected]  ';
// Stored as: "[email protected]"

Relationships

Has One

class User extends Model
{
    protected static string $table = 'users';

    public function profile()
    {
        return $this->hasOne(Profile::class);
        // Looks for: profiles.user_id = users.id
    }
}

$profile = $user->profile; // Lazy-loaded, cached

Has Many

class User extends Model
{
    protected static string $table = 'users';

    public function posts()
    {
        return $this->hasMany(Post::class);
        // Looks for: posts.user_id = users.id
    }
}

$posts = $user->posts; // ModelCollection

Belongs To

class Post extends Model
{
    protected static string $table = 'posts';

    public function author()
    {
        return $this->belongsTo(User::class, 'author_id');
        // Looks for: users.id = posts.author_id
    }
}

$author = $post->author;

Custom Foreign Keys

// Has Many with custom keys
$this->hasMany(Comment::class, 'post_id', 'id');

// Belongs To with custom keys
$this->belongsTo(User::class, 'author_id', 'id');

Soft Deletes

Add the SoftDeletes trait to enable soft deletes:

use WPZylos\Framework\Model\Concerns\SoftDeletes;

class Article extends Model
{
    use SoftDeletes;

    protected static string $table = 'articles';
    protected array $fillable = ['title', 'body'];

    // Optionally customize the column name
    // protected string $deletedAtColumn = 'deleted_at';
}

Usage

$article = Article::find(1);

// Soft delete
$article->delete();         // Sets deleted_at timestamp
$article->trashed();        // true

// Restore
$article->restore();        // Clears deleted_at
$article->trashed();        // false

// Force delete (permanent)
$article->forceDelete();    // Actually removes from DB

// Query scopes
Article::onlyTrashed()->get();  // Only trashed records
Article::withTrashed()->get();  // All records including trashed

Model Events

Register lifecycle event callbacks in the static boot() method:

class Order extends Model
{
    protected static string $table = 'orders';
    protected array $fillable = ['customer_id', 'total', 'status'];

    protected static function boot(): void
    {
        // Before events can cancel by returning false
        static::creating(function (Order $order) {
            if ($order->total <= 0) {
                return false; // Cancel creation
            }
        });

        static::created(function (Order $order) {
            // Send notification, log, etc.
        });

        static::updating(function (Order $order) {
            // Validate before update
        });

        static::deleting(function (Order $order) {
            if ($order->status === 'processing') {
                return false; // Prevent deletion of processing orders
            }
        });
    }
}

Available events: creating, created, updating, updated, deleting, deleted, saving, saved.

Query Scopes

Define reusable query constraints as scopeXxx methods:

class Order extends Model
{
    protected static string $table = 'orders';
    protected array $fillable = ['customer_id', 'total', 'status'];

    public function scopePending($query)
    {
        return $query->where('status', 'pending');
    }

    public function scopeHighValue($query, float $min = 100.0)
    {
        return $query->where('total', '>=', $min);
    }
}

// Usage — called as static methods
$pending = Order::pending()->get();
$bigOrders = Order::highValue(500)->get();

Timestamps

By default, models auto-manage created_at and updated_at:

// Disable timestamps
protected bool $timestamps = false;

// Customize column names
protected ?string $createdAtColumn = 'date_created';
protected ?string $updatedAtColumn = 'date_modified';

// Disable one column
protected ?string $createdAtColumn = null; // No created_at

// Touch updated_at manually
$model->touch();

Mass Assignment

Using $fillable (whitelist)

protected array $fillable = ['name', 'email', 'role'];

// Only name, email, role will be set
$user = User::create($request);

Using $guarded (blacklist)

protected array $guarded = ['id', 'is_admin'];

// Everything except id, is_admin can be set
$user = User::create($request);

Force Fill (bypass protection)

$user->forceFill(['is_admin' => true]);

Serialization

$user = User::find(1);

// To array (excludes $hidden attributes)
$array = $user->toArray();

// To JSON
$json = $user->toJson();
$json = (string) $user; // Same as toJson()

// JSON serializable
echo json_encode($user);

// Collection serialization
$users = User::all();
$array = $users->toArray();
$json  = $users->toJson();

Dirty Tracking

$user = User::find(1);

$user->isDirty();         // false
$user->name = 'Updated';
$user->isDirty();         // true
$user->isDirty('name');   // true
$user->isDirty('email');  // false

$user->getDirty();        // ['name' => 'Updated']
$user->getOriginal();     // Original attributes snapshot