This session introduces most well known design patterns to build PHP classes and objects that need to store and fetch data from a relational databases. The session will describe the difference between of the Active Record, the Table and Row Data Gateway and the Data Mapper pattern. We will also examine some technical advantages and drawbacks of these implementations. This talk will expose some of the best PHP tools, which ease database interactions and are built on top of these patterns.
3. By Martin Fowler
§ Table Module
§ Transaction Script
§ Row Data Gateway
§ Table Data Gateway
§ Active Record
§ Data Mapper
§ Unit of Work
§ Identity Map
§ Data Transfer Object
§ …
15. class OrderGateway
{
public function findAll()
{
$query = 'SELECT * FROM orders';
return $this->conn->fetchAll($query);
}
public function find($pk)
{
$rs = $this->conn->findBy(array('id' => $pk));
return 1 === count($rs) ? $rs[0] : false;
}
}
16. public function findBy(array $criteria)
{
$where = array();
foreach ($criteria as $field => $value) {
$where[] = sprintf('%s = ?');
}
$q = sprintf(
'SELECT * FROM orders WHERE %s',
implode(' AND ', $where)
);
return $this->conn->fetchAll($q, array_values($criteria));
}
17. public function findPaidOrders()
{
return $this->findBy(array('status' => 'paid'));
}
public function findUnpaidOrders()
{
return $this->findBy(array('status' => 'unpaid'));
}
37. class Order
{
private $id;
private $reference;
private $amount;
private $vat;
private $vatRate;
private $total;
private $createdAt;
private $status;
private $isPaid;
// Getters and setters for each property
// ...
}
38. class Order
{
public function __construct($id = null)
{
if (null !== $id) {
$this->id = $id;
}
$this->vatRate = 0.00;
$this->vat = 0.00;
$this->amount = 0.00;
$this->total = 0.00;
$this->isPaid = false;
$this->status = 'processing';
$this->createdAt = new DateTime();
}
}
39. $conn = new Connection('...');
$order = new Order();
$order->setReference('XX12345678');
$order->setAmount(300.00);
$order->setVatRate(0.196);
$order->applyDiscount(20.00);
$order->updateTotal();
$order->save($conn);
40. class Order
{
public function applyDiscount($discount)
{
$this->amount -= $discount;
}
public function updateTotal()
{
if ($this->vatRate) {
$this->vat = $this->amount * $this->vatRate;
}
$this->total = $this->amount + $this->vat;
}
}
41. class Order
{
public function isPaid()
{
return $this->isPaid;
}
public function setPaid()
{
$this->isPaid = true;
}
}
42. class Order
{
public function isReadyForShipment()
{
return $this->isPaid() && 'complete' == $this->status;
}
public function ship($address)
{
$this->doShipment($address);
$this->status = 'shipped';
}
}
43. class OrderController
{
public function confirmAction($reference)
{
$conn = $this->getDatabaseConnection();
$order = ...;
$order->setPaid();
$order->save($conn);
if ($order->isReadyForShipment()) {
$order->ship();
return $this->view->render('ship.php', array('order' => $order));
}
return $this->view->render('pending.php', array('order' => $order));
}
}
45. abstract class ActiveRecord
{
protected $fields = array();
abstract public function getTableName();
public function save(Connection $conn)
{
// insert or update $fields in the database
}
public function delete(Connection $conn)
{
// delete the object from the database
}
}
46. class Order extends ActiveRecord
{
private $amount;
abstract public function getTableName()
{
return 'tbl_orders';
}
public function setAmount($amount)
{
$this->amount = $amount;
$this->fields['amount'] = $amount;
}
}
49. « A layer of Mappers that moves data
between objects and a database
while keeping them independent of
each other and the mapper itself. »
Martin Fowler
52. class OrderMapper
{
private $conn;
public function __construct(Connection $conn) {
$this->conn = $conn;
}
public function store(Order $order) {
// Execute the query to persist the object to the DB
}
public function remove(Order $order) {
// Executes the query to remove the object to the DB
}
}
53. $order = new Order();
$order->setReference('XX12345678');
$order->setAmount(300.00);
$order->setVatRate(0.196);
$order->updateTotal();
$conn = new Connection('mysql:host=localhost ...');
$mapper = new OrderMapper($conn);
$mapper->store($order);
54. class OrderMapper
{
public function findAll()
{
$objects = array();
$query = 'SELECT id, reference, vat ... FROM orders';
foreach ($this->conn->fetchAll($query) as $data) {
$object = new Order($data['id']);
$object->load($data);
$objects[] = $object;
}
return $objects;
}
}
55. class OrderMapper
{
public function find($pk)
{
$query = 'SELECT id, vat ... FROM orders WHERE id = ?';
$object = false;
if (false !== $data = conn->fetch($query, array($pk))) {
$object = new Order($data['id']);
$object->load($data);
}
return $object;
}
}
56. $conn = new Connection('mysql:host=localhost ...');
$mapper = new OrderMapper($conn);
$order = $mapper->find(42);
$order->setAmount(399.00);
$order->updateTotal();
$mapper->store($order);
72. $query = Query::create()
->select(array('id', 'reference', 'amount', 'status'))
->from('orders')
->where(Criteria::equals('status', 'paid'))
->where(Criteria::greaterThan('amount', 2000))
->where(Criteria::like('reference', 'XX123%'))
->orderBy('amount', 'desc')
->getSql()
;
// SELECT id, reference, amount, status
// WHERE status = ? AND amount > ? AND reference LIKE ?
// ORDER BY amount DESC
73. class Criteria
{
private $field;
private $operator;
private $parameters;
public function __construct($field, $operator, $value)
{
$this->field = $field;
$this->operator = $operator;
$this->parameters[] = $value;
}
}
74. class Criteria
{
static public function equal($field, $value, $vars)
{
return new Criteria($field, '=', $vars);
}
static public function notEqual($field, $value, $vars)
{
return new Criteria($field, '<>', $vars);
}
}
86. By Martin Fowler
§ Table Module
§ Transaction Script
§ Row Data Gateway
§ Table Data Gateway
§ Active Record
§ Data Mapper
§ Unit of Work
§ Identity Map
§ Data Transfer Object
§ …