Data Mapper Architecture

The Data Mapper pattern separates your domain objects from database persistence. Entities contain only business logic and data - no database operations, no SQL knowledge. A separate mapper layer (the EntityManager) handles all persistence while keeping entities completely independent. This differs from Active Record patterns where entities inherit database methods like save() and delete(). With Data Mapper, entities are plain PHP objects that can be instantiated and tested without any database connection.

Pure Domain Entities

Entities contain business logic and data, with no persistence concerns:

<?php
namespace App\Entity;

use Quellabs\ObjectQuel\Annotations\Orm;

/**
 * @Orm\Table(name="products")
 */
class ProductEntity {

    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    private ?int $productId = null;

    /**
     * @Orm\Column(name="name", type="string", limit=255)
     */
    private string $name;

    /**
     * @Orm\Column(name="price", type="decimal", limit="10,2")
     */
    private float $price;

    /**
     * @Orm\Column(name="category_id", type="integer")
     */
    private int $categoryId;

    // Pure business logic - no database operations
    public function calculateDiscountedPrice(float $discountPercent): float {
        return $this->price * (1 - $discountPercent / 100);
    }

    public function isExpensive(): bool {
        return $this->price > 100.00;
    }

    public function applyPriceIncrease(float $percentage): void {
        $this->price = $this->price * (1 + $percentage / 100);
    }

    // Simple getters and setters - no save() or delete() methods
    public function getProductId(): ?int { return $this->productId; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
    public function getPrice(): float { return $this->price; }
    public function setPrice(float $price): void { $this->price = $price; }
    public function getCategoryId(): int { return $this->categoryId; }
    public function setCategoryId(int $categoryId): void { $this->categoryId = $categoryId; }
}

Key Points:

  • No save(), delete(), or update() methods in the entity
  • No database connection or SQL knowledge
  • Focus purely on business logic and data validation
  • Annotations only provide metadata - they don't create dependencies

Querying Entities

The EntityManager provides methods to load entities from the database:

<?php
$entityManager = new EntityManager($config);

// Load single entity by primary key
$product = $entityManager->find(ProductEntity::class, 1);

// Load multiple entities by generic properties
$products = $entityManager->findBy(ProductEntity::class, ['active' => true]);

// Complex queries using ObjectQuel
$discountedProducts = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    range of c is App\\Entity\\CategoryEntity via p.categoryId = c.categoryId
    retrieve (p) where p.price > :minPrice and c.name = :category
    sort by p.price desc
", [
    'minPrice' => 50.00,
    'category' => 'Electronics'
]);

Persisting Entities

The EntityManager uses a Unit of Work pattern to batch database operations:

Creating New Entities

<?php
$product = new ProductEntity();
$product->setName('Wireless Mouse');
$product->setPrice(29.99);
$product->setCategoryId(5);

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

// After flush, the entity has its database-generated ID
echo $product->getProductId(); // e.g., 123

Updating Existing Entities

<?php
$product = $entityManager->find(ProductEntity::class, 123);
$product->applyPriceIncrease(10);

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

How It Works

  • persist($entity) - Marks a new entity for insertion, or tracks changes to an existing entity
  • flush() - Executes all pending database operations (INSERT, UPDATE, DELETE) in a single transaction
  • Multiple persist() calls can be batched before a single flush()
  • If flush() fails, all changes are rolled back

Unit Testing Benefits

Entities have no external dependencies, making unit testing straightforward:

<?php
use PHPUnit\Framework\TestCase;

class ProductEntityTest extends TestCase {

    public function testCalculateDiscountedPrice() {
        $product = new ProductEntity();
        $product->setPrice(100.00);

        $discountedPrice = $product->calculateDiscountedPrice(10);
        $this->assertEquals(90.00, $discountedPrice);
    }

    public function testIsExpensive() {
        $product = new ProductEntity();

        $product->setPrice(150.00);
        $this->assertTrue($product->isExpensive());

        $product->setPrice(50.00);
        $this->assertFalse($product->isExpensive());
    }

    public function testApplyPriceIncrease() {
        $product = new ProductEntity();
        $product->setPrice(100.00);

        $product->applyPriceIncrease(10);
        $this->assertEquals(110.00, $product->getPrice());
    }
}

Benefits

  • Testability: Entities can be unit tested without database setup
  • Flexibility: Change persistence strategy without affecting business logic
  • Separation of Concerns: Clear boundaries between domain and infrastructure
  • Performance: Control exactly when and how database operations occur
  • Maintainability: Business logic changes don't affect persistence code
  • Domain Focus: Entities represent business concepts, not database tables