sirix/cycle-orm-extensions

Practical extensions for Cycle ORM: base repositories, entity traits, typecasts and more

Maintainers

Package info

github.com/sirix777/cycle-orm-extensions

pkg:composer/sirix/cycle-orm-extensions

Fund package maintenance!

sirix777

buymeacoffee.com/sirix

Statistics

Installs: 383

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 0

3.0.1 2026-03-10 06:19 UTC

This package is auto-updated.

Last update: 2026-03-10 06:20:40 UTC


README

Practical extensions for Cycle ORM: base repositories, entity traits, typecasts, and more.

Latest Stable Version Total Downloads Latest Unstable Version License PHP Version Require

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 functionality
  • HasChronosUpdateTrait - Adds update timestamp functionality
  • HasChronosDeleteTrait - Adds deletion timestamp functionality
  • HasIdIdentifierTrait - Adds integer ID primary key functionality
  • HasUuidIdentifierTrait - Adds UUID primary key functionality

Annotated Traits

Annotated versions of the traits that include Cycle ORM annotations:

  • HasChronosCreateTimestampAnnotatedTrait - Adds creation timestamp with annotations
  • HasChronosCreateDatetimeAnnotatedTrait - Adds creation datetime with annotations
  • HasChronosUpdateTimestampAnnotatedTrait - Adds update timestamp with annotations
  • HasChronosUpdateDatetimeAnnotatedTrait - Adds update datetime with annotations
  • HasChronosDeleteTimestampAnnotatedTrait - Adds deletion timestamp with annotations
  • HasChronosDeleteDatetimeAnnotatedTrait - Adds deletion datetime with annotations
  • HasIdIdentifierAnnotatedTrait - Adds integer ID primary key with annotations
  • HasUuidIdentifierAnnotatedTrait - Adds UUID primary key with annotations
  • HasUuidIdentifierStringAnnotatedTrait - Adds UUID primary key (string) with annotations
  • HasUuidIdentifierByteAnnotatedTrait - Adds UUID primary key (binary) with annotations

Repositories

Base repository implementations for common operations:

  • AbstractReadRepository - Base repository for read-only operations
  • AbstractWriteRepository - 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 MoneyCurrencyNumericCodeColumnType and MoneyMinorCurrencyNumericCodeColumnType) should continue using custom typecast handlers.

Important:

  • Native *NativeTypecast classes 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] - Converts Brick\Money\Currency to numeric code.
    • #[CurrencyCodeType] - Converts Sirix\Money\CurrencyCode (fiat/crypto) to value.
  • Money (Brick\Money):
    • #[MoneyCurrencyCodeType(currencyCode: FiatCurrencyCode::Eur)] - Converts Brick\Money\Money to string amount using specified currency.
    • #[MoneyCurrencyNumericCodeColumnType(currencyCodeEntityProperty: 'currencyCode')] - Converts Brick\Money\Money to string amount, using another entity property for currency code.
    • #[MoneyMinorCurrencyCodeType(currencyCode: FiatCurrencyCode::Eur)] - Converts Brick\Money\Money to integer (minor units).
    • #[MoneyMinorCurrencyNumericCodeColumnType(currencyCodeEntityProperty: 'currencyCode')] - Converts Brick\Money\Money to 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: true in ChronosUpdatedAt keeps updatedAt as null on 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 TypecastHandler class.
  • 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 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

License

This package is licensed under the MIT License - see the LICENSE file for details.