ObjectQuel implements the Data Mapper pattern through four core classes that separate your domain objects from database persistence logic. Each class has a distinct responsibility in managing the object-relational mapping lifecycle.
The EntityManager is your primary access point for all database operations. It coordinates the other core classes to keep your entities ignorant of persistence details:
<?php
// Create CakePHP database connection
$connection = new \Cake\Database\Connection([
'driver' => \Cake\Database\Driver\Mysql::class,
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'database' => 'database'
]);
$entityManager = new EntityManager($configuration, $connection);
// Retrieving entities
$product = $entityManager->find(ProductEntity::class, 1);
$products = $entityManager->findBy(ProductEntity::class, ['active' => true]);
// ObjectQuel queries for complex retrieval
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.price > :price
", ['price' => 50.00]);
// Persistence operations
$entityManager->persist($product); // Schedule for INSERT
$entityManager->remove($product); // Schedule for DELETE
$entityManager->flush(); // Execute all pending operations
What it does:
The UnitOfWork tracks every loaded entity and detects changes automatically. It ensures that only one instance of each entity exists in memory (Identity Map pattern) and manages database transactions:
<?php
// Load an entity - UnitOfWork stores it in the Identity Map
$product = $entityManager->find(ProductEntity::class, 1);
// Modify the entity - UnitOfWork tracks the change
$product->setPrice(99.99);
// Retrieve the same entity - UnitOfWork returns the existing instance
$sameProduct = $entityManager->find(ProductEntity::class, 1);
var_dump($product === $sameProduct); // true - same object reference
// Flush triggers UnitOfWork to:
// 1. Compare current entity state with original database values
// 2. Generate appropriate SQL (INSERT/UPDATE/DELETE)
// 3. Start a database transaction
// 4. Execute SQL in correct order to maintain foreign key constraints
// 5. Commit the transaction
$entityManager->flush();
What it does:
The EntityStore reads and caches metadata from your entity annotations. Other components query it to understand entity structure, table mappings, and relationships:
<?php
$entityStore = $entityManager->getEntityStore();
// Table mapping
$tableName = $entityStore->getOwningTable(ProductEntity::class);
// Returns: "products"
// Primary key information
$primaryKeys = $entityStore->getIdentifierKeys(ProductEntity::class);
// Returns: ['id']
$columnNames = $entityStore->getIdentifierColumnNames(ProductEntity::class);
// Returns: ['product_id']
// Property-to-column mapping
$columnMap = $entityStore->getColumnMap(ProductEntity::class);
// Returns: ['name' => 'product_name', 'price' => 'unit_price', ...]
// Relationship metadata
$oneToMany = $entityStore->getOneToManyDependencies(ProductEntity::class);
$manyToOne = $entityStore->getManyToOneDependencies(ProductEntity::class);
What it does:
The Configuration object holds entity discovery paths and performance options. Database connections are now managed separately through CakePHP's Connection class:
<?php
$config = new Configuration();
// Entity discovery - where to find your entity classes
$config->setEntityNamespace('App\\Entity');
$config->setEntityPath(__DIR__ . '/src/Entity');
// Performance options
$config->setProxyDir(__DIR__ . '/var/cache/proxies');
$config->setProxyNamespace('App\\Proxies');
$config->setUseMetadataCache(true);
$config->setMetadataCachePath(__DIR__ . '/var/cache/metadata');
// Create CakePHP database connection
$connection = new \Cake\Database\Connection([
'driver' => \Cake\Database\Driver\Mysql::class,
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'database' => 'database'
]);
// Pass configuration and connection to EntityManager
$entityManager = new EntityManager($config, $connection);
What it does:
Understanding the interaction between these classes clarifies how ObjectQuel processes your operations:
<?php
// Setup phase
$config = new Configuration();
$connection = new \Cake\Database\Connection([
'driver' => \Cake\Database\Driver\Mysql::class,
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'database' => 'db'
]);
$entityManager = new EntityManager($config, $connection);
// Find operation
$product = $entityManager->find(ProductEntity::class, 1);
// 1. EntityManager asks EntityStore for ProductEntity metadata
// 2. EntityStore parses annotations (or returns cached metadata)
// 3. EntityManager generates SELECT query using metadata
// 4. UnitOfWork receives the entity and stores it in Identity Map
// 5. UnitOfWork saves a snapshot of original values for change detection
// Modification
$product->setPrice(199.99);
// No database interaction - UnitOfWork will detect this change later
// Flush operation
$entityManager->flush();
// 1. UnitOfWork compares each tracked entity with its original snapshot
// 2. UnitOfWork identifies that $product.price changed
// 3. UnitOfWork starts a database transaction
// 4. UnitOfWork generates UPDATE SQL using EntityStore metadata
// 5. UnitOfWork executes the UPDATE
// 6. UnitOfWork commits the transaction
// 7. UnitOfWork updates its snapshot with new values
Architecture principle: This separation ensures that entity classes remain plain PHP objects with no knowledge of the database. The EntityManager and supporting classes handle all persistence concerns, implementing the Data Mapper pattern cleanly.