0

Model base class design — table name inference, primary key

Intermediate5 min read·fw-08-001
sql

Concept

Model base class design defines the foundation that all Eloquent-style models inherit from. The base class handles table name inference, primary key management, attribute storage, and delegates to the Query Builder.

Table name inference: Convention over configuration. If the model class is User, the table is users. If it's BlogPost, the table is blog_posts. Use a tableize() utility: snake_case the class name, pluralize. Override with protected string $table = 'custom_table'.

Primary key convention: Default primary key is id. Override with protected string $primaryKey = 'uuid'. Some models use composite keys (not common in Active Record pattern).

Attribute storage: Models store attributes in protected array $attributes = []. $model->name reads from this array via __get(). $model->name = 'Alice' writes via __set(). This magic property access is what makes $user->email work.

Connection: Model needs a Connection (or QueryBuilder) to execute queries. Injected via a static setConnection() or resolved from a container. Laravel uses a static $resolver on the base model.

New vs existing records: $model->exists = false when the model is freshly instantiated. save() INSERTs if exists === false, UPDATEs if exists === true. After INSERT, exists = true.

The fill() method: Mass assign attributes from an array. Respects $fillable and $guarded.

Code Example

php
<?php
namespace Framework\Orm;

use Framework\Database\Connection;
use Framework\Database\QueryBuilder;

abstract class Model
{
    protected static ?Connection $connection = null;
    protected string $table      = '';
    protected string $primaryKey = 'id';
    protected array  $attributes = [];
    public    bool   $exists     = false;

    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
    }

    // --- Static setup ---

    public static function setConnection(Connection $connection): void
    {
        static::$connection = $connection;
    }

    protected static function getTable(): string
    {
        if (property_exists(static::class, 'table') && static::$table !== '') {
            return static::$table;
        }
        return static::inferTableName();
    }

    private static function inferTableName(): string
    {
        $class    = (new \ReflectionClass(static::class))->getShortName();
        // PascalCase → snake_case → plural
        $snake    = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($class)));
        return $snake . 's'; // Naive pluralization — use an Inflector library
    }

    // --- Attribute access ---

    public function __get(string $name): mixed
    {
        return $this->getAttribute($name);
    }

    public function __set(string $name, mixed $value): void
    {
        $this->setAttribute($name, $value);
    }

    public function __isset(string $name): bool
    {
        return isset($this->attributes[$name]);
    }

    public function getAttribute(string $key): mixed
    {
        return $this->attributes[$key] ?? null;
    }

    public function setAttribute(string $key, mixed $value): void
    {
        $this->attributes[$key] = $value;
    }

    public function fill(array $attributes): static
    {
        foreach ($attributes as $key => $value) {
            $this->setAttribute($key, $value);
        }
        return $this;
    }

    public function getAttributes(): array { return $this->attributes; }
    public function getKey(): mixed        { return $this->getAttribute($this->primaryKey); }

    protected static function newQuery(): QueryBuilder
    {
        return new QueryBuilder(static::$connection, static::getTable());
    }
}