PHPackages                             kabiroman/adaptive-entity-manager - 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. kabiroman/adaptive-entity-manager

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

kabiroman/adaptive-entity-manager
=================================

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

1.6.0(2mo ago)11381MITPHPPHP &gt;=8.1CI passing

Since May 29Pushed 1mo agoCompare

[ Source](https://github.com/kabiroman/adaptive-entity-manager)[ Packagist](https://packagist.org/packages/kabiroman/adaptive-entity-manager)[ RSS](/packages/kabiroman-adaptive-entity-manager/feed)WikiDiscussions main Synced today

READMEChangelog (10)Dependencies (20)Versions (19)Used By (1)

Adaptive Entity Manager
=======================

[](#adaptive-entity-manager)

[![CI](https://github.com/kabiroman/adaptive-entity-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/kabiroman/adaptive-entity-manager/actions/workflows/ci.yml)

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

[](#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](docs/README.md) (guides for Doctrine, DDD, hexagonal architecture, and legacy migration).

Features
--------

[](#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)
-----------------------

[](#recent-updates-v160)

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

[](#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](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](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](CHANGELOG.md) for **v1.6.0**, *Known limitations*).

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

[](#earlier-in-v150--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

[](#earlier-in-v150--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

[](#earlier-in-v140--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
------------

[](#requirements)

- PHP 8.1 or higher ([Composer platform requirement](composer.json))
- CI tests this package on **PHP 8.1, 8.2, 8.3, and 8.4** (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)), including **PHPStan** at level 5 (`composer analyse`) on each matrix version
- Composer for dependency management

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

[](#installation)

Install the package via Composer:

```
composer require kabiroman/adaptive-entity-manager
```

Dependencies
------------

[](#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
-----------

[](#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
--------------

[](#key-components)

### Entity Manager

[](#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

[](#repositories)

Custom repositories for entity-specific operations:

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

### Unit of Work

[](#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
-----------------

[](#advanced-features)

### ValueObject Support

[](#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

[](#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

[](#basic-usage-1)

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

[](#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

[](#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](docs/VALUE_OBJECTS.md).

### Custom Data Adapters

[](#custom-data-adapters)

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

### Event System

[](#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

[](#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

[](#basic-implementation)

```
