Core Classes

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.

EntityManager - Your Main Interface

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:

  • Executes find operations and ObjectQuel queries
  • Schedules entities for persistence (INSERT, UPDATE, DELETE)
  • Triggers flush operations that synchronize changes to the database
  • Delegates to UnitOfWork, EntityStore, and EntityLifecycleManager

UnitOfWork - Change Detection and Transactions

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:

  • Maintains the Identity Map - ensures one entity instance per database row
  • Detects changes by comparing current state with original snapshot
  • Manages database transactions (begin, commit, rollback)
  • Sorts operations to respect foreign key dependencies
  • Executes batched SQL operations efficiently

EntityStore - Metadata Repository

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:

  • Parses entity class annotations on first use
  • Caches metadata for performance
  • Maps entity properties to database columns
  • Stores relationship definitions (OneToMany, ManyToOne, etc.)
  • Provides metadata queries to EntityManager and UnitOfWork

Configuration - System Settings

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:

  • Defines where to find entity classes (namespace and filesystem path)
  • Configures proxy generation for lazy loading
  • Enables metadata caching to avoid re-parsing annotations

How the Classes Collaborate

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

Dependency Structure

EntityManager

  • Uses: Configuration, UnitOfWork, EntityStore
  • Role: Public API facade that coordinates all operations

UnitOfWork

  • Uses: EntityManager, EntityStore
  • Role: Tracks entity state, detects changes, manages transactions

EntityStore

  • Uses: Configuration, AnnotationReader
  • Role: Metadata repository for entity structure and relationships

Configuration

  • Uses: Nothing (leaf dependency)
  • Role: Centralized settings container

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.