An attribute-driven model layer that sits on top of reflexive/query (and reflexive/core).
It combines:
- schema inference from PHP attributes
- active-record style CRUD helpers
- model hydration and identity caching
- lazy or cached result collections
- SQL schema export from the inferred metadata
- PHP
^8.4 ext-reflectionreflexive/corereflexive/querypsr/simple-cache
composer require reflexive/modelModels extend Reflexive\Model\Model and are described with PHP attributes.
use Reflexive\Model\Model;
use Reflexive\Model\ModelId;
use Reflexive\Model\Table;
use Reflexive\Model\Property;
use Reflexive\Model\Column;
#[Table('users')]
final class User extends Model
{
use ModelId;
#[Property]
#[Column('email_address', type: 'VARCHAR(255)')]
protected string $email;
#[Property]
#[Column] // using defaults from type and default value
protected bool $active = true;
}Key attributes and helpers:
#[Table(...)]defines table-level metadata, inheritance rules, and super/sub-type flags.#[Property(...)]marks protected properties as managed, optionally generating getters/setters.#[Column(...)]maps a property to a database column and can declare id, type, nullability, defaults, and auto-increment.#[Reference(...)]describes object relationships.ModelIdadds a conventional auto-incrementidproperty.ModelEnumadapts PHP enums to the same CRUD API shape.
Managed properties are accessed through __get(), __set(), and generated methods created from #[Property].
By default:
#[Property]creates a getter and setter- setting a different value marks the property as modified
- read-only properties are enforced by the model layer
The write path is then used by create() and update() to decide which columns should be sent to SQL.
Every model gets static builders:
::create(Model $model)::read(...)::search(...)::update(Model $model)::delete(Model $model)::count()
Example:
use Reflexive\Core\Condition;
$user = new User();
$user->setEmail('person@example.com');
User::create($user)->execute($pdo);
$loaded = User::read()->where(Condition::EQUAL('id', $user->id))->execute($pdo);
$matches = User::search()->where(Condition::EQUAL('active', true))
->limit(50)
->execute($pdo);The read/search/count builders translate model property names into real table and column names using Schema.
#[Reference] metadata lets the model layer turn object-level conditions into SQL joins or foreign-key predicates.
Supported cardinalities:
OneToOneOneToManyManyToOneManyToMany
At runtime, references are hydrated into:
- a single related model (or enum)
- a lazily-backed
ModelCollection
For many-to-many updates, Push builds extra insert/update/delete statements for the join table based on collection changes.
Hydrator is responsible for turning rows into model instances and caching them by model id.
Notable behavior:
- repeated reads of the same id can reuse cached objects
- sub-types can be resolved through the internal
reflexive_subTypefield - references may be loaded lazily using PHP lazy ghosts/proxies
ModelCollection wraps either a PDOStatement or a Reflexive\Query\Composed query and implements:
IteratorArrayAccessCountableJsonSerializable
It can auto-execute, cache fetched objects, and track added/modified/removed keys for relationship syncing.
Schema::initFromAttributes() reflects a model class and builds the runtime mapping used by the whole package.
Useful entry points:
Schema::getSchema(FQCN)to fetch the inferred schemaSchema::dumpSQL()to generate SQL for all discovered modelsSchema::exportSQL()to print that SQL directly
The generated SQL is aimed at MySQL/MariaDB-style schemas and includes foreign-key creation statements.
- Some inheritance and reference flows are implemented, but they are opinionated and tightly coupled to the inferred schema layout.