定制 kabiroman/adaptive-entity-manager 二次开发

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

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

kabiroman/adaptive-entity-manager

Composer 安装命令:

composer require kabiroman/adaptive-entity-manager

包简介

Adaptive Entity Manager implementing Doctrine ObjectManager for flexible entity management across multiple data sources

README 文档

README

CI

A flexible PHP package implementing the Doctrine ObjectManager interface for seamlessly managing entities across different data sources: SQL databases, REST APIs, GraphQL endpoints, etc., using pluggable adapters.

Overview

Adaptive Entity Manager (AEM) is a powerful entity management system that provides a unified interface for working with entities across multiple data sources. It implements Doctrine's ObjectManager interface while offering enhanced flexibility through its adapter-based architecture.

AEM is particularly useful when you rely on Doctrine DBAL (or similar) for connections and SQL/query building, but adopting full Doctrine ORM entity mapping is impractical—for example, when integrating several external or legacy databases with uneven schemas.

In hexagonal (ports and adapters) architectures, place AEM in the infrastructure adapters behind your application or domain repositories so persistence concerns do not leak into the core model.

Documentation: docs/README.md (guides for Doctrine, DDD, hexagonal architecture, and legacy migration).

Features

  • Pluggable data source adapters
  • Unified entity management interface
  • Support for multiple data sources simultaneously
  • Value Objects support: Built-in support for immutable Value Objects with automatic conversion
  • Transaction management
  • Lazy loading through proxy objects
  • Flexible entity repository system
  • Comprehensive metadata management
  • Efficient unit of work implementation

Recent Updates (v1.6.0)

Domain value objects (metadata) and VO-aware manager API

  • Map value_object fields to domain types without ValueObjectInterface via metadata: class (preferred) or valueObjectClass, from (static factory), to (serialization). Details: docs/VALUE_OBJECTS.md.
  • ValueObjectAwareEntityManagerInterface carries getValueObjectRegistry() / hasValueObjectSupport(); the base EntityManagerInterface no longer declares them — typehint the extended interface when you rely on VO registry helpers.
  • loadAll criteria apply the same object→storage rules as flush for value_object fields; the instance must match the property’s declared VO class.
  • Stricter config: conflicting class and valueObjectClass are rejected; conversion checks the property type before calling metadata to.
  • Example: examples/ddd_domain_vo_demo.php (dev autoload Examples\Ddd\).
  • Process-wide caveat: one shared static entity factory per PHP process — several EntityManager instances with different VO/metadata setups are not fully isolated (CHANGELOG for v1.6.0, Known limitations).

Earlier in v1.5.0 — Quality, CI, and tooling

  • CI matrix for PHP 8.1, 8.2, 8.3, and 8.4 and a CI status badge in this README.
  • PHPStan at level 5 with the PHPUnit extension, a baseline quality gate, and composer analyse / composer check scripts.
  • Targeted tests added for flush rollback on insert failure, commit event order, preFlush lifecycle, flush with default optimized metadata, loadAll criteria (field-to-column and boolean source values), PersistentCollection lazy init, and EntityManager proxy helpers.
  • English documentation roadmap / RFC updates in the repo.

Earlier in v1.5.0 — API and documentation

  • Nullable parameters made explicit where needed for PHP 8.4 readiness.
  • Positioning refreshed around Doctrine DBAL, legacy databases, and hexagonal architectures.

Earlier in v1.4.0 — Boolean value mapping

  • values option for boolean fields maps non-standard source flags (Y/N, 0/1, T/F) to PHP bool; bidirectional hydration and persistence, plus criteria mapping before adapter queries.
  • ClassMetadata::getFieldOption() for reading arbitrary field options from metadata.
  • If values is omitted, behavior is unchanged.

Requirements

Installation

Install the package via Composer:

composer require kabiroman/adaptive-entity-manager

Dependencies

  • doctrine/persistence: ^3.0 || ^4.0
  • laminas/laminas-code: ^4.0
  • psr/container: ^1.1 || ^2.0
  • symfony/cache: ^6.0 || ^7.0
  • symfony/string: ^6.0 || ^7.0

Basic Usage

// Create configuration
$config = new Config(
    entityFolder: "your_project_dir/src/Entity",
    entityNamespace: 'App\\Entity\\',
    cacheFolder: "your_project_dir/var/cache"
);

// Initialize the Entity Manager
$entityManager = new AdaptiveEntityManager(
    $config,
    new DefaultEntityMetadataProvider(),
    new DefaultEntityDataAdapterProvider()
);

// Work with entities
$entity = $entityManager->find(YourEntity::class, $id);
$entityManager->persist($entity);
$entityManager->flush();

Key Components

Entity Manager

The core component that manages entity operations:

$entityManager->find(Entity::class, $id);      // Find an entity by ID
$entityManager->persist($entity);              // Stage an entity for persistence
$entityManager->remove($entity);               // Stage an entity for removal
$entityManager->flush();                       // Execute all staged operations

Repositories

Custom repositories for entity-specific operations:

$repository = $entityManager->getRepository(Entity::class);
$entities = $repository->findBy(['status' => 'active']);

Unit of Work

Tracks entity states and manages transactions:

$entityManager->beginTransaction();
try {
    // Perform operations
    $entityManager->flush();
    $entityManager->commit();
} catch (\Exception $e) {
    $entityManager->rollback();
    throw $e;
}

Advanced Features

ValueObject Support

Adaptive Entity Manager provides built-in support for immutable Value Objects, allowing you to work with domain-specific types instead of primitive values.

Built-in ValueObjects

  • Email: Email validation with domain/local part extraction
  • Money: Currency-aware monetary values with arithmetic operations
  • UserId: Type-safe user identifiers with validation

Basic Usage

use Kabiroman\AEM\ValueObject\Common\Email;
use Kabiroman\AEM\ValueObject\Common\Money;
use Kabiroman\AEM\ValueObject\Converter\ValueObjectConverterRegistry;

// Enable ValueObject support
$registry = new ValueObjectConverterRegistry();
$entityManager = new AdaptiveEntityManager(
    $config,
    $metadataProvider,
    $dataAdapterProvider,
    valueObjectRegistry: $registry
);

// Using Email ValueObject
$user = new User();
$user->setEmail(new Email('user@example.com'));

// Automatic conversion during persistence
$entityManager->persist($user);  // Email converts to string
$entityManager->flush();

// Automatic conversion during hydration
$loadedUser = $entityManager->find(User::class, 1);
$email = $loadedUser->getEmail();  // Returns Email ValueObject
echo $email->getDomain();  // "example.com"

Entity Configuration

Configure your entity metadata to use ValueObjects:

// In your entity metadata class
use Kabiroman\AEM\Constant\FieldTypeEnum;
use Kabiroman\AEM\ValueObject\Common\Email;

$metadata->addField('email', FieldTypeEnum::ValueObject, Email::class);

Custom ValueObjects

Create your own ValueObjects by implementing ValueObjectInterface:

use Kabiroman\AEM\ValueObject\ValueObjectInterface;

class ProductCode implements ValueObjectInterface
{
    public function __construct(private readonly string $code)
    {
        if (!preg_match('/^[A-Z]{2}\d{4}$/', $code)) {
            throw new \InvalidArgumentException('Invalid product code format');
        }
    }

    public function toPrimitive(): string
    {
        return $this->code;
    }

    public static function fromPrimitive($value): self
    {
        return new self((string) $value);
    }

    public function equals(ValueObjectInterface $other): bool
    {
        return $other instanceof self && $this->code === $other->code;
    }

    public function __toString(): string
    {
        return $this->code;
    }
}

For complete ValueObject documentation, see docs/VALUE_OBJECTS.md.

Custom Data Adapters

Create custom adapters for different data sources by implementing the appropriate interfaces in the DataAdapter namespace.

Event System

The Adaptive Entity Manager provides a flexible event system based on PSR-14, allowing you to hook into various stages of the entity lifecycle. This enables powerful extensibility and modularity, letting you execute custom logic before or after core entity operations.

Key Events:

  • PrePersistEvent: Dispatched before an entity is persisted.
  • PostPersistEvent: Dispatched after an entity has been persisted.
  • PreUpdateEvent: Dispatched before an entity is updated.
  • PostUpdateEvent: Dispatched after an entity has been updated.
  • PreRemoveEvent: Dispatched before an entity is removed.
  • PostRemoveEvent: Dispatched after an entity has been removed.

Usage Example (Conceptual):

To listen to events, you would implement a PSR-14 compatible event listener. For example, using a simple event dispatcher (or your framework's own, like Symfony's EventDispatcher):

use Kabiroman\AEM\Event\PrePersistEvent;
use Psr\EventDispatcher\EventDispatcherInterface;

class MyEventListener
{
    public function onPrePersist(PrePersistEvent $event): void
    {
        $entity = $event->getEntity();
        // Perform custom logic before persistence, e.g., set creation date
        if (method_exists($entity, 'setCreatedAt') && $entity->getCreatedAt() === null) {
            $entity->setCreatedAt(new \DateTimeImmutable());
        }
        // You can stop propagation of the event if needed
        // $event->stopPropagation();
    }
}

// Assuming you have a PSR-14 EventDispatcher instance
/** @var EventDispatcherInterface $eventDispatcher */
$eventDispatcher = /* ... your event dispatcher instance ... */;

// In a non-Symfony project, you might need a ListenerProvider:
// $listenerProvider = new \League\Event\ListenerProvider();
// $listenerProvider->addListener(PrePersistEvent::class, [new MyEventListener(), 'onPrePersist']);
// $eventDispatcher = new \League\Event\EventDispatcher($listenerProvider);

// Add your listener to the dispatcher
// The exact method depends on your PSR-14 implementation
$eventDispatcher->addListener(PrePersistEvent::class, [new MyEventListener(), 'onPrePersist']);

// When AdaptiveEntityManager (specifically UnitOfWork) dispatches PrePersistEvent,
// MyEventListener::onPrePersist will be called.

Data Adapter Example

The Adaptive Entity Manager allows you to create custom data adapters for different data sources. Here's a simple example of implementing a REST API data adapter for a User entity.

Basic Implementation

<?php

namespace Example\DataAdapter;

use Kabiroman\AEM\DataAdapter\AbstractDataAdapter;
use GuzzleHttp\ClientInterface;

class UserApiAdapter extends AbstractDataAdapter
{
    private const API_ENDPOINT = 'https://api.example.com/users';

    public function __construct(
        private readonly ClientInterface $httpClient
    ) {}

    public function loadById(array $identifier): ?array
    {
        $response = $this->httpClient->request('GET', self::API_ENDPOINT . '/' . $identifier['id']);
        $data = json_decode($response->getBody()->getContents(), true);
        
        if (!$data) {
            return null;
        }

        // Convert response fields from snake_case to camelCase
        $this->toCamelCaseParams($data);
        
        return $data;
    }

    public function insert(array $row): array
    {
        // Convert entity fields from camelCase to snake_case
        $this->toSnakeCaseParams($row);
        
        $response = $this->httpClient->request('POST', self::API_ENDPOINT, [
            'json' => $row
        ]);
        
        $data = json_decode($response->getBody()->getContents(), true);
        $this->toCamelCaseParams($data);
        
        return $data;
    }

    public function update(array $identifier, array $row)
    {
        $this->toSnakeCaseParams($row);
        
        $this->httpClient->request('PUT', self::API_ENDPOINT . '/' . $identifier['id'], [
            'json' => $row
        ]);
    }

    public function delete(array $identifier)
    {
        $this->httpClient->request('DELETE', self::API_ENDPOINT . '/' . $identifier['id']);
    }

    public function loadAll(
        array $criteria = [],
        ?array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null
    ): array {
        $query = http_build_query(array_filter([
            'filter' => $criteria,
            'sort' => $orderBy,
            'limit' => $limit,
            'offset' => $offset,
        ]));

        $response = $this->httpClient->request('GET', self::API_ENDPOINT . '?' . $query);
        $data = json_decode($response->getBody()->getContents(), true);

        foreach ($data as &$row) {
            $this->toCamelCaseParams($row);
        }

        return $data;
    }

    public function refresh(array $identifier): array
    {
        return $this->loadById($identifier) ?? 
            throw new \RuntimeException('Entity not found');
    }
}

Usage Example

Here's how to use the custom data adapter with the Adaptive Entity Manager:

use Kabiroman\AEM\AdaptiveEntityManager;
use Kabiroman\AEM\Config;
use Example\Entity\User;
use Example\DataAdapter\UserApiAdapter;

// Create HTTP client
$httpClient = new \GuzzleHttp\Client();

// Create data adapter
$userAdapter = new UserApiAdapter($httpClient);

// Configure entity manager
$config = new Config([
    'dataAdapters' => [
        User::class => $userAdapter
    ]
]);

// Create entity manager
$entityManager = new AdaptiveEntityManager($config);

// Use the entity manager
$user = $entityManager->find(User::class, 1);
$user->setEmail('new@example.com');

$entityManager->persist($user);
$entityManager->flush();

Key Features

  • REST API Integration: Simple implementation for RESTful APIs
  • Automatic Case Conversion: Handles conversion between snake_case (API) and camelCase (entities)
  • CRUD Operations: Complete set of Create, Read, Update, and Delete operations
  • Query Support: Built-in support for filtering, sorting, and pagination
  • Clean Implementation: Extends AbstractDataAdapter for common functionality

Available Methods

Method Description
loadById() Fetches a single entity by its identifier
insert() Creates a new entity
update() Updates an existing entity
delete() Removes an entity
loadAll() Retrieves multiple entities with optional filtering
refresh() Reloads entity data from the data source

Helper Methods

The AbstractDataAdapter provides useful case conversion methods:

  • toCamelCaseParams(): Converts array keys to camelCase
  • toSnakeCaseParams(): Converts array keys to snake_case

Tips

  1. Always handle data transformation between your storage format and entity format
  2. Implement proper error handling for API responses
  3. Use type hints and return types for better code reliability
  4. Consider implementing caching for frequently accessed data
  5. Follow RESTful conventions for API endpoints

Next Steps

  • Implement custom query builders for complex filtering
  • Add caching layer for better performance
  • Implement batch operations for multiple entities
  • Add logging for debugging purposes
  • Implement retry mechanisms for failed API calls

Metadata Management

Comprehensive metadata handling for entity mapping and relationship management.

Metadata example

<?php

namespace App\Metadata;

use Kabiroman\AEM\Metadata\AbstractClassMetadata;
use App\Entity\User;
use App\Entity\Role;
use App\Entity\Post;
use App\DataAdapter\UserDataAdapter;

class UserMetadata extends AbstractClassMetadata
{
    public function __construct()
    {
        $this->metadata = [
            User::class => [
                // Specify the data adapter for this entity
                'dataAdapterClass' => UserDataAdapter::class,
                
                // Define identifier fields
                'id' => [
                    'id' => [
                        'type' => 'integer',
                        'column' => 'user_id',
                        'generator' => 'AUTO'
                    ]
                ],
                
                // Define regular fields
                'fields' => [
                    'email' => [
                        'type' => 'string',
                        'column' => 'email_address',
                        'nullable' => false
                    ],
                    'username' => [
                        'type' => 'string',
                        'column' => 'username',
                        'nullable' => false
                    ],
                    'createdAt' => [
                        'type' => 'datetime',
                        'column' => 'created_at',
                        'nullable' => false
                    ]
                ],
                
                // Define relationships
                'hasOne' => [
                    'role' => [
                        'targetEntity' => Role::class,
                        'joinColumn' => [
                            'name' => 'role_id',
                            'referencedColumnName' => 'id'
                        ],
                        'fetch' => 'LAZY'
                    ]
                ],
                'hasMany' => [
                    'posts' => [
                        'targetEntity' => Post::class,
                        'mappedBy' => 'author',
                        'fetch' => 'LAZY'
                    ]
                ],
                
                // Define lifecycle callbacks
                'lifecycleCallbacks' => [
                    'prePersist' => ['setCreatedAt']
                ]
            ]
        ];
    }
}

This metadata configuration provides:

  • Clear mapping between database columns and entity properties
  • Relationship management (One-to-One, One-to-Many)
  • Automatic lifecycle event handling
  • Type safety through explicit type definitions
  • Flexible data adapter integration

Boolean value mapping

If your data source uses flags like Y|N, 0|1, or T|F, you can normalize them to PHP bool using the values option on a field with type => 'boolean' in your metadata:

'fields' => [
    'isActive' => [
        'type' => 'boolean',
        'column' => 'ACTIVE',
        'nullable' => false,
        'values' => [
            'Y' => true,
            'N' => false,
        ],
    ],
]

Notes:

  • Keys in values are matched case-insensitively for strings; exact match takes precedence.
  • If values is not specified, the source value is used as-is (no implicit Y/N handling).
  • Repository criteria on boolean fields also honor values when querying: passing ['isActive' => true] will be converted to the corresponding source value (e.g., 'Y'). Arrays in criteria (e.g., ['isActive' => [true, false]]) are mapped element-wise.
  • During persistence (object → row), boolean properties are converted to the configured source representation using values (e.g., true'Y').

User entity class that this metadata would describe:

<?php

namespace App\Entity;

use DateTime;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;

class User
{
    private ?int $id = null;
    private string $email;
    private string $username;
    private string $password;
    private DateTime $createdAt;
    private bool $isActive = true;
    
    private Role $role;
    private Collection $posts;
    
    public function __construct()
    {
        $this->posts = new ArrayCollection();
    }
    
    // Lifecycle callback
    public function setCreatedAt(): void
    {
        $this->createdAt = new DateTime();
    }
    
    // Getters and setters...
}

Entity Proxies

The system supports lazy loading through proxy objects, automatically generating proxy classes when needed.

Value Objects

The Adaptive Entity Manager provides powerful Value Object support for better domain modeling and type safety. Value Objects are immutable objects that are defined by their values rather than identity.

use Kabiroman\AEM\ValueObject\Common\Email;
use Kabiroman\AEM\ValueObject\Common\Money;
use Kabiroman\AEM\ValueObject\Converter\ValueObjectConverterRegistry;

// Enable Value Object support
$valueObjectRegistry = new ValueObjectConverterRegistry();
$entityManager = new AdaptiveEntityManager(
    config: $config,
    classMetadataProvider: $metadataProvider,
    entityDataAdapterProvider: $adapterProvider,
    valueObjectRegistry: $valueObjectRegistry
);

// Use Value Objects in entities
class Product
{
    private Email $contactEmail;
    private Money $price;
    
    public function setContactEmail(Email $email): void
    {
        $this->contactEmail = $email;
    }
    
    public function setPrice(Money $price): void
    {
        $this->price = $price;
    }
}

// Create and use Value Objects
$product = new Product();
$product->setContactEmail(Email::fromPrimitive('contact@example.com'));
$product->setPrice(Money::fromPrimitive(['amount' => 12500, 'currency' => 'USD']));

$entityManager->persist($product);
$entityManager->flush();

For detailed documentation on Value Objects, see docs/VALUE_OBJECTS.md.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This package is open-sourced software licensed under the MIT license.

Author

Support

For issues, questions, or contributions, please use the GitHub issue tracker.

kabiroman/adaptive-entity-manager 适用场景与选型建议

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

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

围绕 kabiroman/adaptive-entity-manager 我们能提供哪些服务?
定制开发 / 二次开发

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

  • 总下载量: 137
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 16
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

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

其他信息

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