PHPackages                             duyler/orm - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. duyler/orm

ActiveLibrary[Database &amp; ORM](/categories/database)

duyler/orm
==========

Duyler Cycle ORM integration library

037PHPCI passing

Since Dec 10Pushed 5mo ago1 watchersCompare

[ Source](https://github.com/duyler/orm)[ Packagist](https://packagist.org/packages/duyler/orm)[ RSS](/packages/duyler-orm/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (1)Used By (0)

[![Quality Gate Status](https://camo.githubusercontent.com/e7ce0833a49743e5d3bc49075df8b0f9eaa67eff8cb1f665dc12189431932d02/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d6475796c65725f6f726d266d65747269633d616c6572745f737461747573)](https://sonarcloud.io/summary/new_code?id=duyler_orm)[![Coverage](https://camo.githubusercontent.com/24427a9d1c4b9c1a35b438974059186a395c5b1d87bd779048d5663f40aaf801/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d6475796c65725f6f726d266d65747269633d636f766572616765)](https://sonarcloud.io/summary/new_code?id=duyler_orm)[![type-coverage](https://camo.githubusercontent.com/d9e1b4352069ffebbe59db311ac1facbb2531b348de8b4b3de8091c191c400bf/68747470733a2f2f73686570686572642e6465762f6769746875622f6475796c65722f6f726d2f636f7665726167652e737667)](https://shepherd.dev/github/duyler/event-bus)[![psalm-level](https://camo.githubusercontent.com/9b6e1a2a1f9a3aac045c5ffc5cd854a2fd2fb97b30c152a4b781dad0a24438cf/68747470733a2f2f73686570686572642e6465762f6769746875622f6475796c65722f6f726d2f6c6576656c2e737667)](https://shepherd.dev/github/duyler/event-bus)

Duyler Cycle ORM Integration
============================

[](#duyler-cycle-orm-integration)

A declarative ORM integration package for [Duyler Framework](https://github.com/duyler/duyler) based on [CycleORM](https://cycle-orm.dev/). This package provides a clean, attribute-free approach to entity definition using a fluent Builder API.

Features
--------

[](#features)

- **Declarative Entity Builder** - Define entities without attributes in entity classes
- **Entity Behaviors** - Full support for CycleORM behaviors (timestamps, soft delete, optimistic lock, UUID generation, hooks)
- **Clean Domain Layer** - Keep your entities as pure POPOs (Plain Old PHP Objects)
- **Console Commands** - Built-in commands for migrations and fixtures
- **Type Safety** - Strict typing with 99%+ type coverage
- **Flexible Configuration** - Centralized entity configuration
- **Custom Repositories** - Easy integration of custom repository classes

Installation
------------

[](#installation)

```
composer require duyler/orm
```

Quick Start
-----------

[](#quick-start)

### 1. Define Your Entity Class

[](#1-define-your-entity-class)

Create a clean POPO without any ORM attributes:

```
namespace App\Domain\Entity;

use DateTimeInterface;
use Ramsey\Uuid\UuidInterface;

class Product
{
    private UuidInterface $id;
    private string $title;
    private int $price;
    private DateTimeInterface $createdAt;
    private DateTimeInterface $updatedAt;

    // Getters and setters...

    public function getId(): UuidInterface
    {
        return $this->id;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;
        return $this;
    }

    // ... other methods
}
```

### 2. Declare Entity Using Builder

[](#2-declare-entity-using-builder)

Configure your entity declaratively in `build/entities.php`:

```
use Duyler\ORM\Build\Entity;

Entity::declare(Product::class)
    ->database('default')
    ->table('products')
    ->primaryKey('id')

    // Entity Behaviors
    ->uuid7('id')           // Auto-generate UUID v7
    ->createdAt()           // Auto-set creation timestamp
    ->updatedAt()           // Auto-update modification timestamp

    // Column mapping
    ->columns([
        'id' => 'id',
        'title' => 'title',
        'price' => 'price',
        'createdAt' => 'created_at',
        'updatedAt' => 'updated_at',
    ])

    // Type casting
    ->typecast([
        'id' => 'uuid',
        'title' => 'string',
        'price' => 'int',
        'createdAt' => 'datetime',
        'updatedAt' => 'datetime',
    ])

    ->typecastHandler([
        UuidTypecast::class,
    ]);
```

### 3. Use in Your Application

[](#3-use-in-your-application)

```
use Cycle\ORM\ORMInterface;

// In your action handler
$product = new Product();
$product->setTitle('iPhone 15 Pro');
$product->setPrice(99900);

$orm->persist($product);
$orm->run();

// $product->id, $product->createdAt, $product->updatedAt
// are set automatically by behaviors!
```

Entity Builder API
------------------

[](#entity-builder-api)

### Basic Configuration

[](#basic-configuration)

#### declare()

[](#declare)

Start entity declaration:

```
Entity::declare(Product::class)
```

#### database()

[](#database)

Specify database connection:

```
->database('default')
```

#### table()

[](#table)

Define table name:

```
->table('products')
```

#### primaryKey()

[](#primarykey)

Set primary key column:

```
->primaryKey('id')
```

#### columns()

[](#columns)

Map entity properties to table columns:

```
->columns([
    'propertyName' => 'column_name',
    'userId' => 'user_id',
])
```

#### typecast()

[](#typecast)

Define type casting for properties:

```
->typecast([
    'id' => 'uuid',
    'price' => 'int',
    'isActive' => 'bool',
    'createdAt' => 'datetime',
])
```

#### typecastHandler()

[](#typecasthandler)

Register custom typecast handlers:

```
->typecastHandler([
    UuidTypecast::class,
    MoneyTypecast::class,
])
```

#### repository()

[](#repository)

Specify custom repository class:

```
->repository(ProductRepository::class)
```

### Relations

[](#relations)

Define relationships between entities:

```
->relations([
    'items' => [
        'type' => 'hasMany',
        'target' => OrderItem::class,
        'foreignKey' => 'order_id',
    ],
    'user' => [
        'type' => 'belongsTo',
        'target' => User::class,
    ],
])
```

Entity Behaviors
----------------

[](#entity-behaviors)

Entity Behaviors provide automatic functionality for your entities without polluting domain classes with infrastructure code.

### Timestamps

[](#timestamps)

#### createdAt()

[](#createdat)

Automatically set creation timestamp:

```
Entity::declare(Product::class)
    ->createdAt()  // Uses 'createdAt' field by default
    // or with custom field name
    ->createdAt('created', 'created_at');
```

#### updatedAt()

[](#updatedat)

Automatically update modification timestamp:

```
Entity::declare(Product::class)
    ->updatedAt()  // Uses 'updatedAt' field by default
    // or with custom field and nullable option
    ->updatedAt('updated', 'updated_at', nullable: true);
```

### Soft Delete

[](#soft-delete)

Mark records as deleted instead of removing them from database:

```
Entity::declare(Product::class)
    ->softDelete()  // Uses 'deletedAt' field by default
    // or with custom field name
    ->softDelete('removed', 'removed_at');
```

Usage:

```
$orm->delete($product);  // Sets deletedAt instead of deleting
$orm->run();

// Soft-deleted entities are automatically excluded from queries
$products = $orm->getRepository(Product::class)->findAll();
```

### Optimistic Locking

[](#optimistic-locking)

Prevent race conditions during concurrent updates:

```
Entity::declare(Order::class)
    ->optimisticLock(rule: 'increment')  // Auto-increment integer
    // Other strategies:
    // ->optimisticLock(rule: 'microtime')      // Microtime string
    // ->optimisticLock(rule: 'datetime')       // Datetime version
    // ->optimisticLock(rule: 'random-string')  // Random string
    // ->optimisticLock(rule: 'manual')         // Manual control
```

**Available lock strategies:**

- `increment` - Auto-incrementing integer (default for int fields)
- `microtime` - Microtime string (default for string fields)
- `datetime` - Datetime-based version (default for datetime fields)
- `random-string` - Random string generation
- `manual` - Manual version management

### UUID Generation

[](#uuid-generation)

Automatically generate UUIDs for primary keys:

```
Entity::declare(User::class)
    ->uuid7('id')  // UUID v7 - RECOMMENDED (time-sortable)
    // or
    ->uuid4('id')  // UUID v4 (random)
    // or
    ->uuid1('id')  // UUID v1 (time-based with MAC)
```

**Why UUID v7?**

- Time-sortable for better index performance
- No MAC address leakage
- Compatible with database indexes

### Custom Hooks

[](#custom-hooks)

Execute custom logic on entity lifecycle events:

```
use Cycle\ORM\Entity\Behavior\Event\Mapper\Command;

Entity::declare(Article::class)
    ->hook(
        callable: function (Command\OnCreate $event) {
            $entity = $event->entity;
            $entity->setSlug(Str::slug($entity->getTitle()));
        },
        events: Command\OnCreate::class,
    )
    // Multiple events
    ->hook(
        callable: fn(Command\OnUpdate $event) => Logger::log($event),
        events: [Command\OnUpdate::class, Command\OnDelete::class],
    );
```

**Available events:**

- `Command\OnCreate` - Before entity creation
- `Command\OnUpdate` - Before entity update
- `Command\OnDelete` - Before entity deletion

### Event Listeners

[](#event-listeners)

Add custom listeners with dependency injection support:

```
Entity::declare(Product::class)
    ->eventListener(ProductAuditListener::class)
    // or with arguments
    ->eventListener(CustomListener::class, ['param' => 'value']);
```

Listener example:

```
class ProductAuditListener
{
    public function __construct(
        private AuditService $auditService,
        private LoggerInterface $logger,
    ) {}

    public function __invoke(Command\OnUpdate $event): void
    {
        $this->auditService->logChanges(
            $event->entity,
            $event->state->getChanges()
        );
    }
}
```

### Generic Listeners

[](#generic-listeners)

Add any CycleORM listener classes:

```
use Cycle\ORM\Entity\Behavior\Listener;

Entity::declare(Product::class)
    ->listeners([
        Listener\CreatedAt::class,
        Listener\UpdatedAt::class,
        CustomListener::class,
    ]);
```

### Combining Behaviors

[](#combining-behaviors)

All behaviors can be combined in any order:

```
Entity::declare(Order::class)
    ->database('default')
    ->table('orders')
    ->primaryKey('id')

    // UUID primary key
    ->uuid7('id')

    // Timestamps
    ->createdAt()
    ->updatedAt()

    // Soft delete
    ->softDelete()

    // Optimistic locking
    ->optimisticLock(rule: 'increment')

    // Custom hooks
    ->hook(
        fn(Command\OnCreate $e) => $e->entity->setStatus('new'),
        Command\OnCreate::class,
    )

    // Audit listener
    ->eventListener(OrderAuditListener::class)

    // Standard configuration
    ->columns([/* ... */])
    ->typecast([/* ... */])
    ->relations([/* ... */]);
```

Working with ORM
----------------

[](#working-with-orm)

### Retrieving Entities

[](#retrieving-entities)

```
use Cycle\ORM\ORMInterface;

// Get repository
$repository = $orm->getRepository(Product::class);

// Find by primary key
$product = $repository->findByPK($id);

// Find one by criteria
$product = $repository->findOne(['slug' => 'iphone-15']);

// Find all
$products = $repository->findAll();

// Find with criteria
$products = $repository->findAll(['price' => ['>' => 1000]]);
```

### Creating Entities

[](#creating-entities)

```
$product = new Product();
$product->setTitle('iPhone 15 Pro');
$product->setPrice(99900);

$orm->persist($product);
$orm->run();
```

### Updating Entities

[](#updating-entities)

```
$product = $repository->findByPK($id);
$product->setPrice(89900);

$orm->persist($product);
$orm->run();
```

### Deleting Entities

[](#deleting-entities)

```
$product = $repository->findByPK($id);

$orm->delete($product);
$orm->run();
```

### Query Builder

[](#query-builder)

```
$products = $repository->select()
    ->where('price', '>', 1000)
    ->where('isActive', true)
    ->orderBy('createdAt', 'DESC')
    ->limit(10)
    ->fetchAll();
```

Custom Repositories
-------------------

[](#custom-repositories)

### Define Repository Interface

[](#define-repository-interface)

```
namespace App\Domain\Repository;

use App\Domain\Entity\Product;

interface ProductRepositoryInterface
{
    public function findBySlug(string $slug): ?Product;
    public function findActive(): array;
}
```

### Implement Repository

[](#implement-repository)

```
namespace App\Infrastructure\Repository;

use Cycle\ORM\Select\Repository;
use App\Domain\Repository\ProductRepositoryInterface;

class ProductRepository extends Repository implements ProductRepositoryInterface
{
    public function findBySlug(string $slug): ?Product
    {
        return $this->findOne(['slug' => $slug]);
    }

    public function findActive(): array
    {
        return $this->select()
            ->where('isActive', true)
            ->fetchAll();
    }
}
```

### Register in Entity Declaration

[](#register-in-entity-declaration)

```
Entity::declare(Product::class)
    ->repository(ProductRepository::class)
    // ... other configuration
```

Console Commands
----------------

[](#console-commands)

### Migrations

[](#migrations)

#### Create Migration

[](#create-migration)

```
./bin/do orm:migrations:generate
```

#### Apply Migrations

[](#apply-migrations)

```
./bin/do orm:migrations:up
```

#### Rollback Last Migration

[](#rollback-last-migration)

```
./bin/do orm:migrations:down
```

### Fixtures

[](#fixtures)

#### Create Fixture

[](#create-fixture)

```
namespace App\Fixtures;

use Cycle\ORM\ORMInterface;

class ProductFixture
{
    public function __construct(
        private ORMInterface $orm,
    ) {}

    public function load(): void
    {
        for ($i = 0; $i < 10; $i++) {
            $product = new Product();
            $product->setTitle("Product $i");
            $product->setPrice(rand(1000, 10000));

            $this->orm->persist($product);
        }

        $this->orm->run();
    }
}
```

#### Load Fixtures

[](#load-fixtures)

```
./bin/do orm:fixtures:load
```

Database Configuration
----------------------

[](#database-configuration)

Configure database connections in `config/db.php`:

```
use Cycle\Database\Config\DatabaseConfig;
use Cycle\Database\Config\PostgresDriverConfig;
use Cycle\Database\Driver\Postgres\PostgresDriver;

return [
    DatabaseConfig::class => [
        'databases' => [
            'default' => [
                'connection' => 'postgres',
            ],
        ],
        'connections' => [
            'postgres' => new PostgresDriverConfig(
                connection: new PostgresDriver(
                    dsn: 'pgsql:host=localhost;dbname=myapp',
                    username: 'user',
                    password: 'pass',
                ),
            ),
        ],
    ],
];
```

Real-World Example
------------------

[](#real-world-example)

Complete entity with all features:

```
use Duyler\ORM\Build\Entity;
use Cycle\ORM\Entity\Behavior\Event\Mapper\Command;

Entity::declare(Order::class)
    ->database('default')
    ->table('orders')
    ->primaryKey('id')
    ->repository(OrderRepository::class)

    // Behaviors
    ->uuid7('id')
    ->createdAt('createdAt', 'created_at')
    ->updatedAt('updatedAt', 'updated_at')
    ->softDelete('deletedAt', 'deleted_at')
    ->optimisticLock('version', rule: 'increment')

    // Business logic hooks
    ->hook(
        callable: function (Command\OnCreate $event) {
            $order = $event->entity;
            $order->setOrderNumber(
                'ORD-' . date('Y') . '-' . str_pad($order->getId(), 6, '0', STR_PAD_LEFT)
            );
            $order->setStatus(OrderStatus::New);
        },
        events: Command\OnCreate::class,
    )

    // Validation on update
    ->hook(
        callable: function (Command\OnUpdate $event) {
            $order = $event->entity;
            if ($order->getTotal() < 0) {
                throw new InvalidOrderException('Order total cannot be negative');
            }
        },
        events: Command\OnUpdate::class,
    )

    // Audit logging
    ->eventListener(OrderAuditListener::class)

    // Column mapping
    ->columns([
        'id' => 'id',
        'orderNumber' => 'order_number',
        'userId' => 'user_id',
        'status' => 'status',
        'total' => 'total',
        'version' => 'version',
        'createdAt' => 'created_at',
        'updatedAt' => 'updated_at',
        'deletedAt' => 'deleted_at',
    ])

    // Type casting
    ->typecast([
        'id' => 'uuid',
        'orderNumber' => 'string',
        'userId' => 'uuid',
        'status' => 'string',
        'total' => 'int',
        'version' => 'int',
        'createdAt' => 'datetime',
        'updatedAt' => 'datetime',
        'deletedAt' => 'datetime',
    ])

    // Relations
    ->relations([
        'user' => [
            'type' => 'belongsTo',
            'target' => User::class,
        ],
        'items' => [
            'type' => 'hasMany',
            'target' => OrderItem::class,
            'foreignKey' => 'order_id',
        ],
    ])

    ->typecastHandler([
        UuidTypecast::class,
    ]);
```

Benefits of Declarative Approach
--------------------------------

[](#benefits-of-declarative-approach)

### Clean Domain Layer

[](#clean-domain-layer)

Your entities remain pure PHP objects without infrastructure concerns:

```
// Clean POPO - no attributes, no ORM dependencies
class Product
{
    private UuidInterface $id;
    private string $title;
    private int $price;

    // Pure business logic
    public function applyDiscount(int $percent): void
    {
        $this->price = $this->price * (100 - $percent) / 100;
    }
}
```

### Centralized Configuration

[](#centralized-configuration)

All ORM configuration in one place:

```
// build/entities.php
Entity::declare(Product::class)->...
Entity::declare(Order::class)->...
Entity::declare(User::class)->...
```

### Flexibility

[](#flexibility)

Easy to change mapping without modifying entity classes:

```
// Different mapping for the same entity
Entity::declare(Product::class)
    ->database('primary')
    ->table('products');

// vs

Entity::declare(Product::class)
    ->database('analytics')
    ->table('product_snapshots');
```

### Testability

[](#testability)

Entities are easier to test without ORM dependencies:

```
// Pure unit test - no database needed
public function test_apply_discount(): void
{
    $product = new Product();
    $product->setPrice(1000);
    $product->applyDiscount(10);

    $this->assertEquals(900, $product->getPrice());
}
```

Best Practices
--------------

[](#best-practices)

### 1. Use UUID v7 for Primary Keys

[](#1-use-uuid-v7-for-primary-keys)

```
// Recommended
->uuid7('id')

// Avoid UUID v4 (bad for index performance)
->uuid4('id')
```

### 2. Always Use Timestamps

[](#2-always-use-timestamps)

```
Entity::declare(Entity::class)
    ->createdAt()
    ->updatedAt()
```

### 3. Use Optimistic Lock for Critical Data

[](#3-use-optimistic-lock-for-critical-data)

```
// For financial operations, orders, inventory
Entity::declare(Order::class)
    ->optimisticLock(rule: 'increment')
```

### 4. Soft Delete for User Data

[](#4-soft-delete-for-user-data)

```
// For GDPR compliance and data retention
Entity::declare(User::class)
    ->softDelete()
```

### 5. Keep Complex Logic in Event Listeners

[](#5-keep-complex-logic-in-event-listeners)

```
// Good: Separate class with DI support
->eventListener(ComplexAuditListener::class)

// Avoid: Complex closures in hooks
->hook(fn($e) => /* 100 lines of code */, /* ... */)
```

### 6. Behavior Order Matters

[](#6-behavior-order-matters)

```
// Recommended order
Entity::declare(Entity::class)
    ->uuid7('id')              // 1. UUID generation
    ->createdAt()              // 2. Timestamps
    ->updatedAt()
    ->softDelete()             // 3. Soft delete
    ->optimisticLock()         // 4. Concurrency control
    ->hook(/* ... */)          // 5. Custom hooks
    ->eventListener(/* ... */) // 6. Event listeners
    ->columns([/* ... */])     // 7. Standard config
    ->typecast([/* ... */])
```

Troubleshooting
---------------

[](#troubleshooting)

### Behaviors Not Working

[](#behaviors-not-working)

**Problem:** `createdAt`, `updatedAt`, or other behaviors don't set values.

**Solution:** Ensure `EventDrivenCommandGenerator` is registered in your ORM configuration. This package handles this automatically.

### Optimistic Lock Exceptions

[](#optimistic-lock-exceptions)

**Problem:** Getting `OptimisticLockException` on every update.

**Solutions:**

1. Ensure version field is in columns mapping
2. Use correct type for the lock strategy (`int` for `increment`)
3. Reload entity after save to get updated version

### UUID Not Generated

[](#uuid-not-generated)

**Problem:** UUID field is null after persist.

**Solutions:**

1. Remove manual UUID generation from entity constructor
2. Set `nullable: false` in uuid behavior
3. Ensure `UuidTypecast` is registered

Requirements
------------

[](#requirements)

- PHP 8.2+
- Duyler Framework
- CycleORM 2.x
- cycle/entity-behavior 1.7+

Testing
-------

[](#testing)

```
# Run tests
composer test

# Run static analysis
composer psalm

# Fix code style
composer cs-fix
```

License
-------

[](#license)

MIT

Links
-----

[](#links)

- [Duyler Framework](https://github.com/duyler/duyler)
- [CycleORM Documentation](https://cycle-orm.dev/)
- [Entity Behaviors](https://cycle-orm.dev/docs/entity-behaviors)

###  Health Score

19

—

LowBetter than 10% of packages

Maintenance49

Moderate activity, may be stable

Popularity7

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/69f18edde71f0f80540eda4e097854eddf8eb3390f38ff2ad241b9daaf622281?d=identicon)[milinsky](/maintainers/milinsky)

---

Top Contributors

[![milinsky](https://avatars.githubusercontent.com/u/17288321?v=4)](https://github.com/milinsky "milinsky (1 commits)")

### Embed Badge

![Health badge](/badges/duyler-orm/health.svg)

```
[![Health](https://phpackages.com/badges/duyler-orm/health.svg)](https://phpackages.com/packages/duyler-orm)
```

###  Alternatives

[doctrine/orm

Object-Relational-Mapper for PHP

10.2k285.3M6.2k](/packages/doctrine-orm)[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k115.1M102](/packages/jdorn-sql-formatter)[illuminate/database

The Illuminate Database package.

2.8k52.4M9.4k](/packages/illuminate-database)[ramsey/uuid-doctrine

Use ramsey/uuid as a Doctrine field type.

90440.3M211](/packages/ramsey-uuid-doctrine)[reliese/laravel

Reliese Components for Laravel Framework code generation.

1.7k3.4M16](/packages/reliese-laravel)[wildside/userstamps

Laravel Userstamps provides an Eloquent trait which automatically maintains `created\_by` and `updated\_by` columns on your model, populated by the currently authenticated user in your application.

7511.7M13](/packages/wildside-userstamps)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
