Attribute-driven ORM for the Semitexa Framework. Defines database schema directly in PHP 8.4 attributes, with Swoole connection pooling and MySQL 8.0+ support.
The package is included as a path repository in the root composer.json:
composer require semitexa/orm:"*"Add to .env:
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=semitexa
DB_USERNAME=root
DB_PASSWORD=
DB_POOL_SIZE=10
DB_CHARSET=utf8mb4use Semitexa\Orm\Attribute\FromTable;
use Semitexa\Orm\Attribute\Column;
use Semitexa\Orm\Attribute\PrimaryKey;
use Semitexa\Orm\Attribute\Index;
use Semitexa\Orm\Adapter\MySqlType;
use Semitexa\Orm\Trait\HasTimestamps;
#[FromTable(name: 'users')]
#[Index(columns: ['email'], unique: true)]
class UserResource
{
use HasTimestamps;
#[PrimaryKey(strategy: 'auto')]
#[Column(type: MySqlType::Int)]
public int $id;
#[Column(type: MySqlType::Varchar, length: 255)]
public string $email;
#[Column(type: MySqlType::Varchar, length: 255)]
public string $name;
}| Attribute | Target | Description |
|---|---|---|
#[FromTable] |
Class | Maps class to a database table |
#[Column] |
Property | Defines column type, length, precision, default |
#[PrimaryKey] |
Property | Marks primary key with strategy (auto, uuid, manual) |
#[Index] |
Class | Defines table index (repeatable, supports unique) |
#[Filterable] |
Property | Enables filterByX() and auto-index for this column |
#[Deprecated] |
Property | Marks column for future removal |
#[BelongsTo] |
Property | Many-to-one relation |
#[HasMany] |
Property | One-to-many relation |
#[OneToOne] |
Property | One-to-one relation |
#[ManyToMany] |
Property | Many-to-many with pivot table |
#[Aggregate] |
Property | Virtual aggregated field (COUNT, SUM, etc.) |
HasTimestamps— addscreated_at,updated_atSoftDeletes— adds nullabledeleted_atHasUuid— addsuuid(varchar 36, v4) as a non-PK fieldHasUuidV7— UUID v7 asBINARY(16)primary key (chronologically sortable, optimal for InnoDB)FilterableTrait— addsfilterByX($value)for main-table properties with#[Filterable], andfilterBy{Relation}{Column}($value)for related models; implementFilterableResourceInterfacewhen using withRepository::find(object)
Typed factory (DI) creates a clean resource instance; the resource declares filters via #[Filterable] and exposes typed filterByX() methods; the repository accepts the prepared resource and runs the query.
Layer responsibilities:
- Resource factory (DI) — e.g.
UserResourceFactory: creates a clean resource instance with no data ($userFactory->create()). - Resource model — declares filterable fields with
#[Filterable], usesFilterableTraitandFilterableResourceInterface;filterByX($value)sets criteria. - Repository —
find($resource)/findOne($resource)accept only the repository’s resource type and execute the query from the resource’s criteria.
Rules:
filterByX()is available only for properties with#[Filterable]. CallingfilterByX()for a non-filterable property throwsBadMethodCallException.- For relation filters, the related model’s column must be marked with
#[Filterable](e.g. filter byuser.emailrequiresemailto be filterable on the User resource). - A DB index is created automatically for every
#[Filterable]column. Repository::find($resource)andfindOne($resource)accept only a resource of the repository’s type (otherwiseInvalidArgumentException).
Example:
use Semitexa\Orm\Attribute\FromTable;
use Semitexa\Orm\Attribute\Column;
use Semitexa\Orm\Attribute\PrimaryKey;
use Semitexa\Orm\Attribute\Filterable;
use Semitexa\Orm\Adapter\MySqlType;
use Semitexa\Orm\Trait\HasTimestamps;
use Semitexa\Orm\Trait\FilterableTrait;
use Semitexa\Orm\Contract\FilterableResourceInterface;
#[FromTable(name: 'users')]
class UserResource implements FilterableResourceInterface
{
use HasTimestamps;
use FilterableTrait;
#[PrimaryKey(strategy: 'auto')]
#[Column(type: MySqlType::Int)]
public int $id;
#[Column(type: MySqlType::Varchar, length: 255)]
#[Filterable]
public string $name;
#[Column(type: MySqlType::Varchar, length: 255)]
#[Filterable]
public string $email;
}
// In a handler or service (factory and repository injected via DI):
$userResource = $userFactory->create();
$userResource->filterByName('Rita');
$users = $userRepository->find($userResource);
// Or find first match:
$user = $userRepository->findOne($userResource);You can restrict results using conditions on related resource models. Use filterBy{Relation}{Column}($value) where the relation is the property name (e.g. user) and the column is a filterable property on the related model (e.g. email). Same value semantics as main table: null → IS NULL, array → IN, scalar → =.
Example (BelongsTo): find orders where the related user has a given email:
#[FromTable(name: 'orders')]
class OrderResource implements FilterableResourceInterface
{
use FilterableTrait;
#[Column(type: MySqlType::Bigint)]
public int $id;
#[Column(type: MySqlType::Bigint)]
public int $user_id;
#[BelongsTo(target: UserResource::class, foreignKey: 'user_id')]
public UserResource $user;
}
$orderResource = $orderFactory->create();
$orderResource->filterByUserEmail('admin@example.com');
$orders = $orderRepository->find($orderResource);Example (HasMany): find users that have at least one order with status paid (use a filterable column on the related Order resource):
$userResource = $userFactory->create();
$userResource->filterByOrdersStatus('paid');
$users = $userRepository->find($userResource);In repository methods you can also use the query builder: $this->select()->whereRelation('user', 'email', '=', 'x')->fetchAll().
Resource factory (DI): Implement a per-resource factory interface and bind it to ResourceFactory:
namespace App\Resource\Factory;
use Semitexa\Orm\Factory\ResourceFactoryInterface;
interface UserResourceFactory extends ResourceFactoryInterface
{
public function create(): \App\Resource\UserResource;
}Register in your container/bootstrap: UserResourceFactory → new \Semitexa\Orm\Factory\ResourceFactory(\App\Resource\UserResource::class).
- Nullable FK —
ON DELETE SET NULL,ON UPDATE SET NULL - Not-null FK —
ON DELETE RESTRICT,ON UPDATE RESTRICT - Override — set explicitly on the relation, e.g.
#[BelongsTo(User::class, foreignKey: 'owner_id', onDelete: \Semitexa\Orm\Schema\ForeignKeyAction::CASCADE)]
Use HasUuidV7 trait for resources where a chronologically sortable UUID is the primary key, stored as BINARY(16) for optimal InnoDB performance:
use Semitexa\Orm\Attribute\FromTable;
use Semitexa\Orm\Trait\HasUuidV7;
use Semitexa\Orm\Trait\HasTimestamps;
#[FromTable(name: 'documents')]
class DocumentResource
{
use HasUuidV7;
use HasTimestamps;
#[Column(type: MySqlType::Varchar, length: 255)]
public string $title;
}The id property holds a canonical UUID string (0192d4e0-7b3a-7xxx-...). Conversion to/from BINARY(16) is handled transparently by the ORM. UUID v7 (RFC 9562) embeds a millisecond timestamp, ensuring chronological ordering and efficient B-tree indexing.
You can also use Uuid7 directly:
use Semitexa\Orm\Uuid\Uuid7;
$uuid = Uuid7::generate(); // "0192d4e0-7b3a-7..."
$bytes = Uuid7::toBytes($uuid); // 16-byte binary
$back = Uuid7::fromBytes($bytes); // canonical stringImplement DomainMappable when #[FromTable(mapTo: ...)] is used:
#[FromTable(name: 'users', mapTo: User::class)]
class UserResource implements DomainMappable
{
public function toDomain(): User { /* ... */ }
public static function fromDomain(object $entity): static { /* ... */ }
}Where to put ORM-related classes inside a Semitexa module (see project docs/MODULE_STRUCTURE.md for the full layout):
| What | Folder | Notes |
|---|---|---|
ORM model (class with #[FromTable]) |
Application/Db/MySQL/Model/ |
Table → domain mapping. Use #[FromTable(mapTo: ...)] and DomainMappable to tie to a domain entity. The Resource folder is reserved for response DTOs only; put DB mapping classes in Db/MySQL/Model/ and refer to them in docs as "ORM model" or "table mapping". |
| Domain entity | Domain/Model/ |
e.g. User.php, readonly value object. Lives at module root level, not under Application/. |
| Repository interface | Domain/Repository/ |
e.g. UserRepositoryInterface.php. |
| Repository implementation | Application/Db/MySQL/Repository/ |
e.g. UserRepository implements UserRepositoryInterface. |
Namespaces: ...\Application\Db\MySQL\Model\, ...\Domain\Model\, ...\Domain\Repository\, ...\Application\Db\MySQL\Repository\.
- SchemaCollector — discovers
#[FromTable]classes viaClassDiscovery, buildsTableDefinitionobjects, validates schema - ConnectionPool — Swoole
Channel-based pool with lazy creation - MysqlAdapter — MySQL 8.0+ with version detection and capability checks
- OrmManager — orchestrator that ties pool, adapter, and schema collector together
- PHP 8.4+
- MySQL 8.0+
- Swoole 5.x
semitexa/core ^1.0