定制 duyler/orm 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

duyler/orm

Composer 安装命令:

composer require duyler/orm

包简介

Duyler Cycle ORM integration library

README 文档

README

Quality Gate Status Coverage type-coverage psalm-level

Duyler Cycle ORM Integration

A declarative ORM integration package for Duyler Framework based on CycleORM. This package provides a clean, attribute-free approach to entity definition using a fluent Builder API.

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

composer require duyler/orm

Quick Start

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

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

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

Basic Configuration

declare()

Start entity declaration:

Entity::declare(Product::class)

database()

Specify database connection:

->database('default')

table()

Define table name:

->table('products')

primaryKey()

Set primary key column:

->primaryKey('id')

columns()

Map entity properties to table columns:

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

typecast()

Define type casting for properties:

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

typecastHandler()

Register custom typecast handlers:

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

repository()

Specify custom repository class:

->repository(ProductRepository::class)

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 provide automatic functionality for your entities without polluting domain classes with infrastructure code.

Timestamps

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()

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

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

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

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

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

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

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

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

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

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

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

Updating Entities

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

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

Deleting Entities

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

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

Query Builder

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

Custom Repositories

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

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

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

Console Commands

Migrations

Create Migration

./bin/do orm:migrations:generate

Apply Migrations

./bin/do orm:migrations:up

Rollback Last Migration

./bin/do orm:migrations:down

Fixtures

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

./bin/do orm:fixtures:load

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

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

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

All ORM configuration in one place:

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

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

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

1. Use UUID v7 for Primary Keys

// Recommended
->uuid7('id')

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

2. Always Use Timestamps

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

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

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

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

// 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

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

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

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

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

Testing

# Run tests
composer test

# Run static analysis
composer psalm

# Fix code style
composer cs-fix

License

MIT

Links

duyler/orm 适用场景与选型建议

duyler/orm 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 37 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 05 月 12 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「database」 「orm」 「db」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 duyler/orm 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 duyler/orm 我们能提供哪些服务?
定制开发 / 二次开发

基于 duyler/orm 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 37
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 18
  • 依赖项目数: 3
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-05-12