Model base class design — table name inference, primary key
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
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());
}
}