Share
Embed
Practical Design PHPatterns
Observer, adapter, decorator, facade, composite… All these weird words refer to design patterns. Design patterns are general reusable solutions for solving common redundant problems in software architecture. Getting inspiration on some well known design patterns and implementing them in PHP will make your application code easier to understand, maintain and evolve. Based on very practical code samples, this talk will focus on the most usefull patterns in order to help you design better PHP applications.
Hugo Hamon
January 26, 2013
Transcript
104 slides
-
Practical Design PHPatterns Hugo Hamon – PHPBenelux 2013 - Antwerp
http://wwp.greenwichmeantime.com -
Definition « In software design, a design pattern is an
abstract generic solution to solve a particular redundant problem. » -
Today’s patterns o Facade o Adapter o Template Method o
Strategy o Decorator o Composite o Factory Method o Observer -
Wrapping a complicated system in order to provide a simplier
interface to use it. Goal -
$content = file_get_contents(__DIR__.'/feed.xml'); $xml = new SimpleXMLElement($content); $title = (string)
$xml->title; $entries = array(); $entry = array(); foreach ($this->xml->entry as $entry) { $entry['title'] = (string) $entry->title; $entry['summary'] = (string) $entry->summary; $entry['id'] = (string) $entry->id; if (isset($entry->author)) { // parse <author> node $author = ...; $entry['author'] = $author; } $entries[] = $entry; $entry = array(); } -
class FeedReader { public function getTitle() { [complex xml parsing
logic here] } public function getEntries() { [complex xml parsing logic here] } } -
public function getEntries() { if (null === $this->xml) { $this->loadXml();
} $entries = array(); $entry = array(); foreach ($this->xml->entry as $entry) { $entry['title'] = (string) $entry->title; $entry['summary'] = (string) $entry->summary; $entry['id'] = (string) $entry->id; if ($author = $this->getAuthor($entry)) { $entry['author'] = $author; } $entries[] = $entry; $entry = array(); } return $entries; } -
$facade = new FeedReader('feed.xml'); $title = $facade->getTitle(); $entries = $facade->getEntries();
foreach ($entries as $entry) { echo $entry['title'],"\n"; if (isset($entry['author'])) { echo $entry['author']['name']; } } -
Providing a unique API to objects that don’t share the
same API. Goal -
Fetching weather forecasts with two different weather providers, which don’t
have the same API. Practical Example -
A weather client object relies on the famous WahooWeather web
service to collect daily forecasts for a city. -
class WeatherService { private $api; public function __construct(WahooWeatherApi $api) {
$this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } } -
class WeatherService { private $api; public function __construct(WahooWeatherApi $api) {
$this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } } Concrete dependency Current API -
But a new competitor, BoogleWeather, now comes with a much
more powerful API to get daily forecasts for a city. -
We want the weather service Client supports both weather providers
although they don’t share the same API at all… -
$api = new WahooWeatherApi(); $result = $api->getCityForecasts('Paris', '2013-02-25'); $result =
array( 'city' => 'Paris', 'country' => 'France', 'date' => '2013-02-25', 'conditions' => 'snowy', 'temperature' => '-6', 'unit' => 'C', ); -
$weather = new BoogleWeather(); $weather->city = 'Paris'; $weather->date = '02/25/2013';
$json = $weather->getForecasts(); $json = '{ "location": "Paris, France", "temp": "21.2", "conditions": "snowy", "date": "02\/05\/2013” }'; -
WahooWeatherApi BoogleWeather Date in YYYY-mm-dd format Date in mm/dd/YYYY format
City is passed to the method City is set in a public property Method is getCityForecasts() Method is getForecasts() Returns an array Returns a JSON string APIs differences -
Designing an Adapter helps to provide a single public API
to support the two heterogeneous Adaptees. -
abstract class WeatherProviderAdapter { protected $date; public function setDate($date) {
$this->date = new DateTime($date); } abstract public function getForecasts($city); } -
class WahooProviderAdapter extends WeatherProviderAdapter { private $api; public function __construct(WahooWeatherApi
$api) { $this->api = $api; } public function getForecasts($city) { $date = $this->date->format('Y-m-d'); return $this->api->getCityForecasts($city, $date); } } -
class BoogleProviderAdapter extends WeatherProviderAdapter { private $api; public function __construct(BoogleWeather
$api) { $this->api = $api; } public function getForecasts($city) { $this->api->date = $this->date->format('m/d/Y'); $this->api->city = $city; $json = $this->api->getWeather(); // Convert the JSON output to an array expected by the client return $this->normalize($json); } } -
class WeatherService { private $provider; public function __construct(WeatherProviderAdapter $provider) {
$this->provider = $provider; } public function getForecasts($city, $date) { $this->provider->setDate($date); return $this->provider->getForecasts($city); } } -
class WeatherService { private $provider; public function __construct(WeatherProviderAdapter $provider) {
$this->provider = $provider; } public function getForecasts($city, $date) { $this->provider->setDate($date); return $this->provider->getForecasts($city); } } Abstraction Shared public API -
// Adapter for the Wahoo weather service $adaptee = new
WahooWeatherApi(); $adapter = new WahooProviderAdapter($adaptee); // Adapter for the Boogle weather service $adaptee = new BoogleWeather(); $adapter = new BoogleProviderAdapter($adaptee); // Client code that deals with the adapters $client = new WeatherService($adapter); $client->getForecasts('Paris', '2013-02-25'); -
Let subclasses redefine certain steps of an algorithm without changing
the algorithm’s structure. Goal -
Inserting or updating a record in a datastore like a
relational or nosql database. Practical Example -
Mapper! RelationalMapper! NoSQLMapper! + save($data) [final]! # getPrimaryKey(array $data)! #
insert(array $data)! # update($pk, array $data)! # getPrimaryKey(array $data)! # insert(array $data)! # update($pk, array $data)! # getPrimaryKey(array $data)! # insert(array $data)! # update($pk, array $data)! -
abstract class Mapper { /** * Inserts or updates a
record in a datastore. * * @param array $data The data to insert or update * @return Boolean */ final public function save(array $data) { if ($pk = $this->getPrimaryKey($data)) { return $this->update($pk, $data); } return $this->insert($data); } abstract protected function getPrimaryKey(array $data); abstract protected function update($pk, array $data); abstract protected function insert(array $data); } -
class ArticleMapper extends Mapper { private $dbh; public function __construct(PDO
$dbh) { $this->dbh = $dbh; } protected function getPrimaryKey(array $data) { return isset($data['id']) ? $data['id'] : null; } } -
class ArticleMapper extends Mapper { protected function insert(array $data) {
$stmt = $this->dbh->prepare('INSERT INTO ... '); $stmt->bindValue(':title', $data['title'], PDO::PARAM_STR); $stmt->bindValue(':body', $data['body'], PDO::PARAM_STR); $stmt->execute(); return 1 === (int) $stmt->rowCount(); } protected function update($pk, array $data) { $stmt = $this->dbh->prepare('UPDATE ... WHERE id = :id '); $stmt->bindValue(':title', $data['title'], PDO::PARAM_STR); $stmt->bindValue(':body', $data['body'], PDO::PARAM_STR); $stmt->bindValue(':id', $pk, PDO::PARAM_INT); $stmt->execute(); return 1 === (int) $stmt->rowCount(); } } -
class LogMapper extends Mapper { private $mongo; public function __construct(MongoClient
$mongo) { $this->mongo = $mongo; } protected function getPrimaryKey(array $data) { return isset($data['_id']) ? $data['_id'] : null; } } -
class LogMapper extends Mapper { protected function insert(array $data) {
$data = $this->mongo->log->insert($data); return !empty($data['_id']); } protected function update($pk, array $data) { return $this->mongo->log->update($data); } } -
The Strategy pattern encapsulates algorithms of the same nature into
dedicated classes to make them interchangeable. Goal -
Sending emails with several transport layers like SMTP or the
PHP mail function. Practical Example -
class Mailer { public function send(Message $message) { $to =
$message->getRecipients(); $subject = $message->getSubject(); $body = $message->getBody(); $headers = $message->getHeaders(); if (!mail($to, $subject, $body, $headers)) { throw new RuntimeException('...'); } } } -
What if we want to use an SMTP transport to
send the email? Practical Example -
class Mailer { private $transport; public function __construct($transport) { $this->transport
= $transport; } public function send(Message $message) { $to = $message->getRecipients(); // ... if ('smtp' === $this->transport) { // ... use SMTP transport } else { // ... use mail function } } } -
class MailTransport implements MailTransportInterface { public function send(Message $message) {
$to = $message->getRecipients(); $subject = $message->getSubject(); $body = $message->getBody(); $headers = $message->getHeaders(); if (!mail($to, $subject, $body, $headers)) { throw new MailTransportException('...'); } } } -
class SmtpTransport implements MailTransportInterface { public function send(Message $message) {
// ... use an SMTP server } } class NullTransport implements MailTransportInterface { public function send(Message $message) { // do nothing } } -
class Mailer { private $transport; public function __construct(MailTransportInterface $transport) {
$this->transport = $transport; } public function send(Message $message) { try { $this->transport->send($message); } catch (MailTransportException $e) { // ... handle error } } } Abstraction -
$transport = new MailTransport(); $transport = new NullTransport(); $transport =
new SendmailTransport(); $transport = new SmtpTransport('localhost', 'user'); $message = new Message(); $message->setTo('[email protected]'); $message->setFrom('[email protected]'); $message->setSubject('Example'); $message->setBody('Some body'); $mailer = new Mailer($transport); $mailer->send($message); -
class Connection implements ConnectionInterface { private $dbh; private $logger; private
$queryCount; public function __construct(PDO $dbh, Logger $logger = null) { $this->logger = $logger; $this->dbh = $dbh; } public function getQueryCount() { return $this->queryCount; } } -
class Connection implements ConnectionInterface { // ... public function query($query)
{ if (null !== $this->logger) { $this->logger->log('Query: '.$query); $this->queryCount++; } return $this->dbh->query($query); } } -
Testing if the logger is set in production will add
a small overhead for each executed SQL query. -
What if we want to extend the Connection class without
introducing a dependency with the logger? -
abstract class ConnectionDecorator implements ConnectionInterface { protected $connection; public function
__construct(ConnectionInterface $connection) { $this->connection = $connection; } public function query($query) { return $this->connection->query($query); } } -
class ProfiledConnection extends ConnectionDecorator { private $logger; private $queryCount; function
__construct(Connection $conn, Logger $logger) { parent::__construct($conn); $this->logger = $logger; $this->queryCount = 0; } public function getQueryCount() { return $this->queryCount; } } -
class ProfiledConnection extends ConnectionDecorator { public function query($query) { $this->logger->log('Query:
'.$query); $this->queryCount++; return parent::query($query); } } -
$logger = new Logger(__DIR__.'/logs/demo.log'); $dbh = new PDO('mysql:...', 'root'); $conn
= new Connection($dbh); $conn = new ProfiledConnection($conn, $logger); $conn->query("SET NAMES 'UTF8'"); $conn->query('SELECT * FROM users'); echo 'Queries count:'; echo $conn->getQueryCount(); // 2 -
Pros and cons + Easy way to extend an
object capabilities + No need to change the underlying code - Object construction becomes more complex - Difficulty to test the concrete object type -
Goal Composite lets clients treat individual objects and compositions
of objects uniformly. -
Representing an HTML form as a list of Field objects.
Practical Example -
A Form is a collection of Field. A Field can
also be a Form when it embeds several fields. -
$form = new Form('product'); $form->add('name', new Input('text')); $form->add('description', new Textarea());
$picture = new Form(); $picture->add('caption', new Input('text')); $picture->add('image', new Input('file')); $form->add('photo', $picture); $data = array( 'name' => 'Apple Macbook Air 11', 'description' => 'The finest laptop', 'photo' => array( 'caption' => 'The new Macbook Air.', ), ); $form->setData($data); -
abstract class Field { protected $name; protected $data; public function
setName($name) { $this->name = $name; } public function setData($data) { $this->data = $data; } public function getData() { return $this->data; } } -
class Input extends Field { private $type; public function __construct($type)
{ $this->type = $type; } public function setData($data) { if ('file' !== $this->type) { parent::setData($data); } } } -
class Form extends Field { private $fields = array(); public
function add($name, Field $field) { $this->fields[$name] = $field; $field->setName($name); } } -
class Form extends Field { public function setData($data) { foreach
($this->fields as $name => $field) { if (isset($data[$name])) { $field->setData($data[$name]); } } } public function getData() { $data = array(); foreach ($this->fields as $name => $field) { $data[$name] = $field->getData(); } return $data; } } -
Goal Define an interface for creating an object, but
let subclasses decide which class to instantiate. -
Creating several kind of Document objects with a factory (images,
pdf, text…) Practical Example -
abstract class DocumentFactory { static public function getDocument($type) { switch
($type) { case "image": return new Image(); case "pdf": return new Pdf(); case "video": return new Video(); default: throw new InvalidArgumentException(); } } } -
abstract class DocumentFactory { protected $directory; public function __construct($directory) {
$this->directory = $directory; } abstract public function createDocument($name); public function newDocument($name, $content) { $document = $this->createDocument($name); $document->setDirectory($this->directory); $document->setContent($content); $document->save(); return $document; } } -
class TextFileFactory extends DocumentFactory { public function createDocument($name) { return
new Document($name); } } -
class ImageFileFactory extends DocumentFactory { public function createDocument($name) { $image
= new Image($name); $image->setWidth(90); $image->setHeight(120); return $image; } } -
$binary = $graphic->getBinaryContent(); $factory = new ImageFileFactory(__DIR__.'/images'); $image = $factory->newDocument('lolcat.jpg',
$binary); $content = 'README CAREFULLY'; $factory = new TextFileFactory(__DIR__.'/files'); $file = $factory->newDocument('README.txt', $content); -
Goal A subject, the observable, emits a signal to
a list of modules known as observers. -
class Order { public function confirm() { $this->status = 'confirmed';
$this->save(); if ($this->logger) { $this->logger->log('New order...'); } $mail = new Email(); $mail->recipient = $this->customer->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } } -
interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface {
function attach(ObserverInterface $observer); function notify(); } -
class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface
$subject) { $reference = $subject->getReference(); $this->logger->log('New order #'. $reference); } } -
class CustomerNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface
$subject) { $mail = new Email(); $mail->recipient = $subject->customer->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); } } -
class SalesNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface
$subject) { $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } } -
class Order implements ObservableInterface { // ... private $observers; public
function attach(ObserverInterface $observer) { $this->observers[] = $observer; } public function notifyObservers() { foreach ($this->observers as $observer) { $observer->notify($this); } } } -
class Order implements ObservableInterface { public function confirm() { $this->status
= 'confirmed'; $this->save(); $this->notifyObservers(); } } -
$order = new Order(); $order->attach(new LoggerNotifier($logger)); $order->attach(new CustomerNotifier($mailer)); $order->attach(new SalesNotifier($mailer));
$order->customer = $customer; $order->amount = 150; $order->confirm();