sirix / cycle-orm-extensions
Practical extensions for Cycle ORM: base repositories, entity traits, typecasts and more
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- cycle/orm: ^2.10
- ramsey/uuid: ^4.7
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.8
- cakephp/chronos: ^3.1
- cycle/annotated: ^4.2
- cycle/entity-behavior: ^1.4
- phpunit/phpunit: ^10.5
- sirix/money: ^1.2
Suggests
- cakephp/chronos: Required for Chronos datetime support
- cycle/annotated: Required for annotated entity support
- cycle/entity-behavior: Required for entity behaviors and lifecycle hooks support
- sirix/money: Required for Money and Currency typecast support
README
Practical extensions for Cycle ORM: base repositories, entity traits, typecasts, and more.
Overview
This package provides a collection of practical extensions for Cycle ORM, including:
- Entity traits for common functionality (timestamps, UUIDs, etc.)
- Base repository implementations
- Custom typecasts
- Event listeners
- And more
Requirements
- PHP 8.2, 8.3, 8.4, or 8.5
- Cycle ORM 2.10+
- Ramsey UUID 4.7+
Installation
Install the package via composer:
composer require sirix/cycle-orm-extensions
Features
Entity Traits
The package provides several traits for common entity functionality:
Standard Traits
HasChronosCreateTrait- Adds creation timestamp functionalityHasChronosUpdateTrait- Adds update timestamp functionalityHasChronosDeleteTrait- Adds deletion timestamp functionalityHasIdIdentifierTrait- Adds integer ID primary key functionalityHasUuidIdentifierTrait- Adds UUID primary key functionality
Annotated Traits
Annotated versions of the traits that include Cycle ORM annotations:
HasChronosCreateTimestampAnnotatedTrait- Adds creation timestamp with annotationsHasChronosCreateDatetimeAnnotatedTrait- Adds creation datetime with annotationsHasChronosUpdateTimestampAnnotatedTrait- Adds update timestamp with annotationsHasChronosUpdateDatetimeAnnotatedTrait- Adds update datetime with annotationsHasChronosDeleteTimestampAnnotatedTrait- Adds deletion timestamp with annotationsHasChronosDeleteDatetimeAnnotatedTrait- Adds deletion datetime with annotationsHasIdIdentifierAnnotatedTrait- Adds integer ID primary key with annotationsHasUuidIdentifierAnnotatedTrait- Adds UUID primary key with annotationsHasUuidIdentifierStringAnnotatedTrait- Adds UUID primary key (string) with annotationsHasUuidIdentifierByteAnnotatedTrait- Adds UUID primary key (binary) with annotations
Repositories
Base repository implementations for common operations:
AbstractReadRepository- Base repository for read-only operationsAbstractWriteRepository- Base repository for read-write operations
SelectFactory
The SelectFactory is a utility class used by repositories to create Cycle\ORM\Select instances. It ensures that the role passed to the select is a valid entity class implementing EntityInterface.
In your application, you should register SelectFactory in your dependency injection container. Registration example:
<?php use Sirix\Cycle\Extension\Factory\SelectFactory; use Cycle\ORM\ORMInterface; /** @var ORMInterface $orm */ $selectFactory = new SelectFactory($orm);
Repositories provided by this package require SelectFactory in their constructor:
<?php use Sirix\Cycle\Extension\Repository\AbstractReadRepository; use Sirix\Cycle\Extension\Factory\SelectFactory; class MyRepository extends AbstractReadRepository { public function __construct(SelectFactory $selectFactory) { parent::__construct($selectFactory); } protected function getEntityClass(): string { return MyEntity::class; } }
Typecasts
Custom typecasts for various data types, implemented as internal package types. The architecture is inspired by vjik/cycle-typecast.
Annotation-based Typecasting
The package supports typecasting via PHP 8 attributes. This allows you to define typecasting logic directly on entity properties.
To use this feature, you need to configure Sirix\Cycle\Extension\Typecast\Handler\AttributeTypecastHandler for your entity and use the provided type attributes:
<?php use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Extension\Typecast\Uuid\UuidToStringType; use Sirix\Cycle\Extension\Typecast\Handler\AttributeTypecastHandler; use Ramsey\Uuid\UuidInterface; #[Entity( typecast: AttributeTypecastHandler::class, )] class User { #[UuidToStringType] private UuidInterface $uuid; }
This mode uses property-level typecast attributes (like #[UuidToStringType]) together with
Sirix\Cycle\Extension\Typecast\Handler\AttributeTypecastHandler.
Available type attributes:
- Arrays:
#[ArrayToDelimitedStringType(delimiter: ',')]- Converts array to string and vice versa.#[EnumArrayToDelimitedStringType(enumClass: MyEnum::class, delimiter: ',')]- Converts array of backed enums to string.#[ArrayToJsonType]- Converts array to JSON string.
- Boolean:
#[BooleanToIntType]- Converts boolean to integer (0 or 1).
- Enums (BackedEnum):
#[IntegerEnumType(enumClass: MyIntEnum::class)]- Converts int-backed enum to integer value and back.#[StringEnumType(enumClass: MyStringEnum::class)]- Converts string-backed enum to string value and back.
- Chronos (CakePHP Chronos):
#[ChronosToTimestampType]- Converts Chronos to UNIX timestamp (string/int).#[ChronosToDateTimeStringType]- Converts Chronos to 'Y-m-d H:i:s' string.#[ChronosToDateStringType]- Converts Chronos to 'Y-m-d' string.
Native Cycle Field Typecast for Chronos
For schema-builder configurations, you can use native Cycle field-level typecast callbacks:
<?php use Sirix\Cycle\Extension\Typecast\Chronos\ChronosNativeTypecast; $entity->getFields()->set( 'updatedAt', (new Field()) ->setType('datetime') ->setColumn('updated_at') ->setTypecast([ChronosNativeTypecast::class, 'toChronos']), );
For timestamp columns:
<?php use Sirix\Cycle\Extension\Typecast\Chronos\ChronosNativeTypecast; $entity->getFields()->set( 'updatedAt', (new Field()) ->setType('int') ->setColumn('updated_at') ->setTypecast([ChronosNativeTypecast::class, 'toChronosFromTimestamp']), );
Native Typecaster in Annotated Entity
You can also configure native Cycle typecast rules on an annotated entity:
<?php use Cycle\Annotated\Annotation\Column; use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Extension\Typecast\Chronos\ChronosNativeTypecast; #[Entity(typecast: [ 'createdAt' => [ChronosNativeTypecast::class, 'toChronos'], 'updatedAt' => [ChronosNativeTypecast::class, 'toChronos'], 'deletedAt' => [ChronosNativeTypecast::class, 'toChronos'], ])] final class User { #[Column(type: 'datetime')] private \Cake\Chronos\Chronos $createdAt; #[Column(type: 'datetime', nullable: true)] private ?\Cake\Chronos\Chronos $updatedAt = null; #[Column(type: 'datetime', nullable: true)] private ?\Cake\Chronos\Chronos $deletedAt = null; }
You can configure native rules directly at #[Column(..., typecast: ...)] level as well:
<?php use Cake\Chronos\Chronos; use Cycle\Annotated\Annotation\Column; use Sirix\Cycle\Extension\Typecast\Chronos\ChronosNativeTypecast; final class User { #[Column(type: 'datetime', typecast: [ChronosNativeTypecast::class, 'toChronos'])] private Chronos $createdAt; }
Native Cycle Field Typecast for UUID
For UUID fields in schema-builder, use native field-level callbacks:
<?php use Sirix\Cycle\Extension\Typecast\Uuid\UuidNativeTypecast; $entity->getFields()->set( 'uuid', (new Field()) ->setType('uuid') ->setColumn('uuid') ->setTypecast([UuidNativeTypecast::class, 'toUuidFromString']), );
For binary UUID(16) storage:
<?php use Sirix\Cycle\Extension\Typecast\Uuid\UuidNativeTypecast; $entity->getFields()->set( 'uuid', (new Field()) ->setType('binary') ->setColumn('uuid') ->setTypecast([UuidNativeTypecast::class, 'toUuidFromBytes']), );
Native Cycle Field Typecast for Other Types
<?php use Sirix\Cycle\Extension\Typecast\Array\ArrayNativeTypecast; use Sirix\Cycle\Extension\Typecast\Boolean\BooleanNativeTypecast; use Sirix\Cycle\Extension\Typecast\Currency\CurrencyNativeTypecast; use Sirix\Cycle\Extension\Typecast\CurrencyCode\CurrencyCodeNativeTypecast; use Sirix\Cycle\Extension\Typecast\Money\MoneyNativeTypecast; // bool $field->setTypecast([BooleanNativeTypecast::class, 'toBool']); // array from JSON $field->setTypecast([ArrayNativeTypecast::class, 'toArrayFromJson']); // array from delimited string $field->setTypecast([ArrayNativeTypecast::class, 'toArrayFromDelimitedString', ['|']]); // currency (numeric code -> Brick\Money\Currency) $field->setTypecast([CurrencyNativeTypecast::class, 'toCurrency']); // currency code (numeric code -> FiatCurrencyCode|CryptoCurrencyCode) $field->setTypecast([CurrencyCodeNativeTypecast::class, 'toCurrencyCode']); // money with fixed currency code $field->setTypecast([MoneyNativeTypecast::class, 'toMoneyByCurrencyCode', ['USD']]); // money with fixed numeric currency code $field->setTypecast([MoneyNativeTypecast::class, 'toMoneyByNumericCode', [840]]);
Limitation:
- Native Cycle callbacks do not have row-level context, so column-dependent money conversions
(analogues of
MoneyCurrencyNumericCodeColumnTypeandMoneyMinorCurrencyNumericCodeColumnType) should continue using custom typecast handlers.
Important:
- Native
*NativeTypecastclasses are callback providers for Cycle rules. - They are not property attributes and should not be used as
#[SomeNativeTypecast].
Decision Matrix: Native vs Handler
Use this matrix when selecting typecast approach:
| Type / Scenario | Preferred | Notes |
|---|---|---|
Chronos (datetime / timestamp) |
Native | Use ChronosNativeTypecast::* callbacks. |
UUID (uuid / binary(16)) |
Native | Use UuidNativeTypecast::toUuidFromString or toUuidFromBytes. |
| Boolean | Native | Use BooleanNativeTypecast::toBool. |
| Array / JSON / Delimited array | Native | Use ArrayNativeTypecast::*. |
| Currency / CurrencyCode | Native | Use CurrencyNativeTypecast::toCurrency, CurrencyCodeNativeTypecast::toCurrencyCode. |
| Money with fixed currency (known in config) | Native | Use MoneyNativeTypecast::* with fixed code argument. |
| Money dependent on another entity field (e.g. currency column) | Handler | Requires row-level context ($context->data). |
| Any conversion requiring access to multiple fields | Handler | Use TypecastHandler / AttributeTypecastHandler. |
Property-level attribute style (#[SomeType]) |
Handler | Requires AttributeTypecastHandler. |
Practical rule:
- Start with native callbacks.
- Switch to handler when conversion needs context, bidirectional custom behavior, or property attributes.
- Currency (Brick\Money):
#[CurrencyType]- ConvertsBrick\Money\Currencyto numeric code.#[CurrencyCodeType]- ConvertsSirix\Money\CurrencyCode(fiat/crypto) to value.
- Money (Brick\Money):
#[MoneyCurrencyCodeType(currencyCode: FiatCurrencyCode::Eur)]- ConvertsBrick\Money\Moneyto string amount using specified currency.#[MoneyCurrencyNumericCodeColumnType(currencyCodeEntityProperty: 'currencyCode')]- ConvertsBrick\Money\Moneyto string amount, using another entity property for currency code.#[MoneyMinorCurrencyCodeType(currencyCode: FiatCurrencyCode::Eur)]- ConvertsBrick\Money\Moneyto integer (minor units).#[MoneyMinorCurrencyNumericCodeColumnType(currencyCodeEntityProperty: 'currencyCode')]- ConvertsBrick\Money\Moneyto integer (minor units), using another entity property for currency code.
- UUID (Ramsey\Uuid):
#[UuidToBytesType]- Converts UUID to binary.#[UuidToStringType]- Converts UUID to string.
Typecast Traits
For common scenarios, the package provides traits that combine property definition, Cycle ORM annotations, and typecasting:
HasUuidIdentifierTypecastTrait- UUID primary key (string 36).HasUuidIdentifierStringTypecastTrait- UUID primary key (explicit string 36).HasUuidIdentifierByteTypecastTrait- UUID primary key (binary 16).HasChronosCreateTimestampTypecastTrait,HasChronosCreateDatetimeTypecastTrait- Creation timestamps.HasChronosUpdateTimestampTypecastTrait,HasChronosUpdateDatetimeTypecastTrait- Update timestamps.HasChronosDeleteTimestampTypecastTrait,HasChronosDeleteDatetimeTypecastTrait- Soft delete timestamps.
Event Listeners
The EventListeners attribute lets you declare Cycle ORM entity listeners directly on the entity class. It injects the configured listeners into the entity schema under SchemaInterface::LISTENERS.
Note: To use listeners, you must install the Cycle Entity Behavior package:
composer require cycle/entity-behavior
You can provide listeners as simple class-strings or as [class, args] tuples. You may also mix both forms in one array. Passing an empty args array [] is equivalent to specifying the class directly; the attribute normalizes this for you.
Simple listeners (class-strings only)
<?php use Sirix\Cycle\Extension\Behavior\EventListeners; use Sirix\Cycle\Extension\Listener\ChronosCreateListener; use Sirix\Cycle\Extension\Listener\ChronosUpdateListener; #[EventListeners(listeners: [ ChronosCreateListener::class, ChronosUpdateListener::class, ])] final class User {}
Listeners with constructor arguments (mixed usage)
<?php use Sirix\Cycle\Extension\Behavior\EventListeners; use Sirix\Cycle\Extension\Listener\ChronosSoftDeleteListener; use App\Cycle\Listener\AuditListener; #[EventListeners(listeners: [ ChronosSoftDeleteListener::class, [AuditListener::class, ['timezone' => 'UTC', 'captureOldValues' => true]], ])] final class User {}
Example listener class consuming the named arguments:
<?php namespace App\Cycle\Listener; final class AuditListener { public function __construct( private readonly string $timezone = 'UTC', private readonly bool $captureOldValues = false, ) {} // Implement handle methods as required by Cycle ORM's listener contracts }
For a complete entity example that uses listeners, see the annotated entity example below.
Chronos Schema Modifiers (Cycle-style)
For schema-builder configuration (Cycle\Schema\Definition\Entity), the package provides Cycle-style modifiers that wrap Chronos listeners and listener args:
ChronosCreatedAt(field: 'createdAt', column: 'created_at')ChronosUpdatedAt(field: 'updatedAt', column: 'updated_at', nullable: true)ChronosSoftDelete(field: 'deletedAt', column: 'deleted_at')
<?php use Sirix\Cycle\Extension\Behavior\ChronosCreatedAt; use Sirix\Cycle\Extension\Behavior\ChronosSoftDelete; use Sirix\Cycle\Extension\Behavior\ChronosUpdatedAt; $entity->addSchemaModifier(new ChronosCreatedAt(field: 'createdAt', column: 'created_at')); $entity->addSchemaModifier(new ChronosUpdatedAt(field: 'updatedAt', column: 'updated_at', nullable: true)); $entity->addSchemaModifier(new ChronosSoftDelete(field: 'deletedAt', column: 'deleted_at'));
Notes:
nullable: trueinChronosUpdatedAtkeepsupdatedAtasnullon create and sets it only on real update.- These modifiers are recommended instead of manual
new EventListener(Chronos*Listener::class, ...)wiring in schema-builder code.
Annotated Entity Example
The same modifiers can be used as attributes on an annotated entity:
<?php use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Extension\Behavior\ChronosCreatedAt; use Sirix\Cycle\Extension\Behavior\ChronosSoftDelete; use Sirix\Cycle\Extension\Behavior\ChronosUpdatedAt; #[Entity] #[ChronosCreatedAt(field: 'createdAt', column: 'created_at')] #[ChronosUpdatedAt(field: 'updatedAt', column: 'updated_at', nullable: true)] #[ChronosSoftDelete(field: 'deletedAt', column: 'deleted_at')] final class User { // Entity fields... }
Usage Examples
The package includes several example files in the src/Example directory that demonstrate how to use its features:
Example Files
- AnnotatedEntityWithAttributesExample.php: Demonstrates how to create an entity with attributes for typecasting, using traits for UUID identifier and Chronos timestamps.
- AnnotatedEntityWithTypecastHandlerExample.php: Demonstrates how to create an entity using a custom
TypecastHandlerclass. - ReadRepositoryExample.php: Demonstrates how to create a read-only repository.
- WriteRepositoryExample.php: Shows how to create a repository with writing capabilities.
Creating an Annotated Entity with Attribute Typecast
<?php declare(strict_types=1); namespace Sirix\Cycle\Extension\Example; use Cycle\Annotated\Annotation\Column; use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Extension\Behavior\EventListeners; use Sirix\Cycle\Extension\Domain\Contract\EntityInterface; use Sirix\Cycle\Extension\Entity\Trait\Annotated\Typecast\HasChronosCreateTimestampTypecastTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\Typecast\HasChronosDeleteTimestampTypecastTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\Typecast\HasChronosUpdateTimestampTypecastTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\Typecast\HasUuidIdentifierTypecastTrait; use Sirix\Cycle\Extension\Listener\ChronosCreateListener; use Sirix\Cycle\Extension\Listener\ChronosSoftDeleteListener; use Sirix\Cycle\Extension\Listener\ChronosUpdateListener; use Sirix\Cycle\Extension\Typecast\Handler\AttributeTypecastHandler; #[Entity( repository: WriteRepositoryExample::class, table: 'users', database: 'default', typecast: AttributeTypecastHandler::class, )] #[EventListeners( listeners: [ ChronosCreateListener::class, ChronosUpdateListener::class, ChronosSoftDeleteListener::class, ], )] class AnnotatedEntityWithAttributesExample implements EntityInterface { // Include the annotated traits (with attributes for typecasting) use HasUuidIdentifierTypecastTrait; use HasChronosCreateTimestampTypecastTrait; use HasChronosUpdateTimestampTypecastTrait; use HasChronosDeleteTimestampTypecastTrait; // Define additional properties with annotations #[Column(type: 'string')] private string $name; #[Column(type: 'string')] private string $email; public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } public function getEmail(): string { return $this->email; } public function setEmail(string $email): void { $this->email = $email; } }
Creating an Annotated Entity with Typecast Handler
<?php declare(strict_types=1); namespace Sirix\Cycle\Extension\Example; use Cycle\Annotated\Annotation\Column; use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Extension\Behavior\EventListeners; use Sirix\Cycle\Extension\Domain\Contract\EntityInterface; use Sirix\Cycle\Extension\Entity\Trait\Annotated\HasChronosCreateTimestampAnnotatedTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\HasChronosDeleteTimestampAnnotatedTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\HasChronosUpdateTimestampAnnotatedTrait; use Sirix\Cycle\Extension\Entity\Trait\Annotated\HasUuidIdentifierAnnotatedTrait; use Sirix\Cycle\Extension\Listener\ChronosCreateListener; use Sirix\Cycle\Extension\Listener\ChronosSoftDeleteListener; use Sirix\Cycle\Extension\Listener\ChronosUpdateListener; #[Entity( repository: WriteRepositoryExample::class, table: 'users_with_handler', database: 'default', typecast: AnnotatedEntityExampleTypecastHandler::class, )] #[EventListeners( listeners: [ ChronosCreateListener::class, ChronosUpdateListener::class, ChronosSoftDeleteListener::class, ], )] class AnnotatedEntityWithTypecastHandlerExample implements EntityInterface { // Include the annotated traits (without attributes for typecasting) use HasUuidIdentifierAnnotatedTrait; use HasChronosCreateTimestampAnnotatedTrait; use HasChronosUpdateTimestampAnnotatedTrait; use HasChronosDeleteTimestampAnnotatedTrait; // Define additional properties with annotations #[Column(type: 'string')] private string $name; #[Column(type: 'string')] private string $email; public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } public function getEmail(): string { return $this->email; } public function setEmail(string $email): void { $this->email = $email; } }
Creating a Write Repository
<?php declare(strict_types=1); namespace Sirix\Cycle\Extension\Example; use Cycle\ORM\ORMInterface; use DateTimeInterface; use Sirix\Cycle\Extension\Factory\SelectFactory; use Sirix\Cycle\Extension\Repository\AbstractWriteRepository; class WriteRepositoryExample extends AbstractWriteRepository { public function __construct(ORMInterface $orm, SelectFactory $selectFactory) { parent::__construct($orm, $selectFactory); } /** * Example of finding entities created after a certain date. */ public function findUsersCreatedAfter(DateTimeInterface $date): array { $select = $this->select() ->where('createdAt', '>', $date->getTimestamp()); return $select->fetchAll(); } protected function getEntityClass(): string { return AnnotatedEntityWithAttributesExample::class; } }
Optional Dependencies
The package suggests the following dependencies for additional functionality:
cakephp/chronos: Required for Chronos datetime supportcycle/annotated: Required for annotated entity supportcycle/entity-behavior: Required for entity behaviors and lifecycle hooks supportsirix/money: Required for Money and Currency typecast support
License
This package is licensed under the MIT License - see the LICENSE file for details.