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.
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:
save(), delete(), or update() methods in the entityThe 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'
]);
The EntityManager uses a Unit of Work pattern to batch database operations:
<?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
<?php
$product = $entityManager->find(ProductEntity::class, 123);
$product->applyPriceIncrease(10);
$entityManager->persist($product);
$entityManager->flush();
persist($entity) - Marks a new entity for insertion, or tracks changes to an existing entityflush() - Executes all pending database operations (INSERT, UPDATE, DELETE) in a single transactionpersist() calls can be batched before a single flush()flush() fails, all changes are rolled backEntities 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());
}
}