ez-php / orm
ORM module for the ez-php framework — Active Record style models with a fluent query builder and schema builder
1.8.0
2026-04-19 10:18 UTC
Requires
- php: ^8.5
- ez-php/cache: ^1.0
- ez-php/console: ^1.0
- ez-php/contracts: ^1.0
Requires (Dev)
- ez-php/docker: ^1.0
- ez-php/testing-application: ^1.0
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
This package is auto-updated.
Last update: 2026-04-19 10:21:38 UTC
README
ORM module for the ez-php framework — Data Mapper pattern with Entity, AbstractRepository, a fluent QueryBuilder, and a Schema builder.
Requirements
- PHP 8.5+
- ext-pdo
- ez-php/framework 0.*
Installation
composer require ez-php/orm
Setup
Register the service providers:
$app->register(\EzPhp\Orm\EntityServiceProvider::class); $app->register(\EzPhp\Orm\Schema\SchemaServiceProvider::class);
Usage
Defining an entity
use EzPhp\Orm\Entity; class User extends Entity { protected static string $table = 'users'; protected static bool $timestamps = true; protected static array $fillable = ['name', 'email']; protected static array $casts = ['age' => 'int']; }
Defining a repository
use EzPhp\Orm\AbstractRepository; /** * @extends AbstractRepository<User> */ class UserRepository extends AbstractRepository { protected function entityClass(): string { return User::class; } public function findByEmail(string $email): ?User { return $this->findOneBy('email', $email); } public function activeUsers(): array { return $this->query()->where('active', true)->orderBy('name')->get(); } }
Persisting
$repo = $app->make(UserRepository::class); // INSERT $user = new User(['name' => 'Alice', 'email' => 'alice@example.com']); $repo->save($user); // UPDATE (only dirty columns) $user->name = 'Bob'; $repo->save($user); // DELETE $repo->delete($user);
Querying
$user = $repo->find(1); $all = $repo->findAll(); $alice = $repo->findByEmail('alice@example.com'); $page = $repo->query()->where('active', true)->paginate(perPage: 15, page: 1);
Soft deletes
class Post extends Entity { protected static string $table = 'posts'; protected static bool $softDeletes = true; }
$repo->delete($post); // sets deleted_at — row stays in the DB $post->trashed(); // true after soft delete // Include soft-deleted rows $all = $repo->query()->withTrashed()->get(); $deleted = $repo->query()->onlyTrashed()->get();
Relations
Define relation helpers on the repository, then call them on an entity:
class PostRepository extends AbstractRepository { protected function entityClass(): string { return Post::class; } public function author(Post $post): EntityBelongsTo { return $this->belongsTo(UserRepository::class, 'user_id', 'id'); } } // Lazy load $author = $postRepo->author($post)->getResult(); // Eager load (avoids N+1) $posts = $postRepo->query()->with('author')->get();
Custom casts
use EzPhp\Orm\CastableInterface; class Money implements CastableInterface { public function __construct(private readonly int $cents) {} public static function castFrom(mixed $value): static { return new self((int) $value); } public function castTo(): mixed { return $this->cents; } } class Product extends Entity { protected static array $casts = ['price' => Money::class]; }
Entity lifecycle observers
Attach observers to a repository to react to create/update/delete events:
use EzPhp\Orm\EntityObserverInterface; use EzPhp\Orm\ObservableRepositoryTrait; class AuditObserver implements EntityObserverInterface { public function creating(object $entity): void {} public function created(object $entity): void { /* log insert */ } public function updating(object $entity): void {} public function updated(object $entity): void { /* log update */ } public function deleting(object $entity): void {} public function deleted(object $entity): void { /* log delete */ } } class UserRepository extends AbstractRepository { use ObservableRepositoryTrait; // ... } $repo->observe(new AuditObserver());
The *ing hooks fire before the DB operation; *ed hooks fire after.
Schema builder
use EzPhp\Orm\Schema\Schema; Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamps(); }); Schema::table('users', function (Blueprint $table) { $table->string('phone')->nullable(); }); Schema::drop('old_table');
Console commands
| Command | Description |
|---|---|
make:entity |
Scaffolds an Entity subclass in src/Entities/ |
make:repository |
Scaffolds an AbstractRepository subclass in src/Repositories/ |
Classes
| Class | Description |
|---|---|
Entity |
Abstract Data Mapper entity base; attributes, casts, fillable guards, relation storage |
AbstractRepository |
Abstract repository base; INSERT/UPDATE/DELETE, dirty tracking, relations, eager-load |
EntityObserverInterface |
Lifecycle hook contract: creating/created/updating/updated/deleting/deleted |
ObservableRepositoryTrait |
Adds observer support to a repository; fires hooks around save() and delete() |
EntityQueryBuilder |
Typed fluent query builder for entities; with(), withCount(), paginate() |
EntityServiceProvider |
Calls Entity::setDatabase($db) in boot() |
Hydrator |
Converts raw DB rows → Entity instances and Entity attributes → storage arrays |
CastableInterface |
Interface for custom value-object casts: castFrom()/castTo() |
DuplicateKeyException |
Thrown by save() on duplicate-key violations |
Paginator |
Immutable page-of-results value object |
QueryBuilder |
Fluent SQL builder for raw rows; all WHERE/JOIN/ORDER/LIMIT/aggregates/paginate/chunk/cache |
EntityHasMany |
One-to-many relation (FK on related entity) |
EntityHasOne |
One-to-one relation (FK on related entity) |
EntityBelongsTo |
Inverse of HasMany/HasOne (FK on owning entity) |
EntityBelongsToMany |
Many-to-many relation via pivot table |
Schema |
DDL façade: create(), table(), drop(), dropIfExists(), hasTable() |
Blueprint |
Column and constraint builder for CREATE TABLE and ALTER TABLE |
License
MIT — Andreas Uretschnig