kabiroman / adaptive-entity-manager
Adaptive Entity Manager implementing Doctrine ObjectManager for flexible entity management across multiple data sources
Requires
- php: >=8.1
- doctrine/persistence: ^3.0 || ^4.0
- laminas/laminas-code: ^4.0
- psr/container: ^1.1 || ^2.0
- psr/event-dispatcher: ^1.0
- symfony/cache: ^6.0 || ^7.0
- symfony/string: ^6.0 || ^7.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.0
README
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.
Features
- Pluggable data source adapters
- Unified entity management interface
- Support for multiple data sources simultaneously
- Transaction management
- Lazy loading through proxy objects
- Flexible entity repository system
- Comprehensive metadata management
- Efficient unit of work implementation
Requirements
- PHP 8.1 or higher
- Composer for dependency management
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
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 camelCasetoSnakeCaseParams()
: Converts array keys to snake_case
Tips
- Always handle data transformation between your storage format and entity format
- Implement proper error handling for API responses
- Use type hints and return types for better code reliability
- Consider implementing caching for frequently accessed data
- 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
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.
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
- Ruslan Kabirov - kabirovruslan@gmail.com
Support
For issues, questions, or contributions, please use the GitHub issue tracker.