Slide 1

Slide 1 text

Practical Design PHPatterns Hugo Hamon – PHPBenelux 2013 - Antwerp http://wwp.greenwichmeantime.com

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

What are design patterns?

Slide 4

Slide 4 text

Definition « In software design, a design pattern is an abstract generic solution to solve a particular redundant problem. »

Slide 5

Slide 5 text

Patterns categories Creation Structure Behavior

Slide 6

Slide 6 text

Want to learn?

Slide 7

Slide 7 text

Disclaimer Patterns are not the holly grail!

Slide 8

Slide 8 text

Today’s patterns o  Facade o  Adapter o  Template Method o  Strategy o  Decorator o  Composite o  Factory Method o  Observer

Slide 9

Slide 9 text

Facade http://www.flickr.com/photos/br1dotcom/5930994659

Slide 10

Slide 10 text

Wrapping a complicated system in order to provide a simplier interface to use it. Goal  

Slide 11

Slide 11 text

Parsing an Atom XML feed to extract some information. Practical Example  

Slide 12

Slide 12 text

$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 node $author = ...; $entry['author'] = $author; } $entries[] = $entry; $entry = array(); }

Slide 13

Slide 13 text

Encapsulating the complex xml parsing logic into a facade object. Solution  

Slide 14

Slide 14 text

class FeedReader { public function getTitle() { [complex xml parsing logic here] } public function getEntries() { [complex xml parsing logic here] } }

Slide 15

Slide 15 text

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; }

Slide 16

Slide 16 text

$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']; } }

Slide 17

Slide 17 text

Adapter

Slide 18

Slide 18 text

Providing a unique API to objects that don’t share the same API. Goal  

Slide 19

Slide 19 text

Fetching weather forecasts with two different weather providers, which don’t have the same API. Practical Example  

Slide 20

Slide 20 text

A weather client object relies on the famous WahooWeather web service to collect daily forecasts for a city.

Slide 21

Slide 21 text

class WeatherService { private $api; public function __construct(WahooWeatherApi $api) { $this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } }

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

But a new competitor, BoogleWeather, now comes with a much more powerful API to get daily forecasts for a city.

Slide 24

Slide 24 text

We want the weather service Client supports both weather providers although they don’t share the same API at all…

Slide 25

Slide 25 text

$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', );

Slide 26

Slide 26 text

$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” }';

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Designing an Adapter helps to provide a single public API to support the two heterogeneous Adaptees.

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

abstract class WeatherProviderAdapter { protected $date; public function setDate($date) { $this->date = new DateTime($date); } abstract public function getForecasts($city); }

Slide 31

Slide 31 text

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); } }

Slide 32

Slide 32 text

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); } }

Slide 33

Slide 33 text

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); } }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

// 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');

Slide 36

Slide 36 text

Template Method http://www.flickr.com/photos/jmgeorges/4849023808

Slide 37

Slide 37 text

Let subclasses redefine certain steps of an algorithm without changing the algorithm’s structure. Goal  

Slide 38

Slide 38 text

Inserting or updating a record in a datastore like a relational or nosql database. Practical Example  

Slide 39

Slide 39 text

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)!

Slide 40

Slide 40 text

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); }

Slide 41

Slide 41 text

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; } }

Slide 42

Slide 42 text

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(); } }

Slide 43

Slide 43 text

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; } }

Slide 44

Slide 44 text

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); } }

Slide 45

Slide 45 text

Strategy http://redmtncom.com/uploads/chess_pieces_photo.jpg

Slide 46

Slide 46 text

The Strategy pattern encapsulates algorithms of the same nature into dedicated classes to make them interchangeable. Goal  

Slide 47

Slide 47 text

Sending emails with several transport layers like SMTP or the PHP mail function. Practical Example  

Slide 48

Slide 48 text

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('...'); } } }

Slide 49

Slide 49 text

What if we want to use an SMTP transport to send the email? Practical Example  

Slide 50

Slide 50 text

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 } } }

Slide 51

Slide 51 text

interface MailTransportInterface { function send(Message $message); } Clean strategy interface

Slide 52

Slide 52 text

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('...'); } } }

Slide 53

Slide 53 text

class SmtpTransport implements MailTransportInterface { public function send(Message $message) { // ... use an SMTP server } } class NullTransport implements MailTransportInterface { public function send(Message $message) { // do nothing } }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

$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);

Slide 56

Slide 56 text

Decorator http://soyou.wordpress.com/tag/poupees-russes/

Slide 57

Slide 57 text

Adding responsibilities to objects without subclassing their classes. Goal  

Slide 58

Slide 58 text

Adding new responsabilities to a database connection object. Practical Example  

Slide 59

Slide 59 text

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; } }

Slide 60

Slide 60 text

class Connection implements ConnectionInterface { // ... public function query($query) { if (null !== $this->logger) { $this->logger->log('Query: '.$query); $this->queryCount++; } return $this->dbh->query($query); } }

Slide 61

Slide 61 text

Testing if the logger is set in production will add a small overhead for each executed SQL query.

Slide 62

Slide 62 text

What if we want to extend the Connection class without introducing a dependency with the logger?

Slide 63

Slide 63 text

Connection! ConnectionDecorator! MasterSlaveConnection! ProfiledConnection! query($query)! query($query)! query($query)! connection! query($query)!

Slide 64

Slide 64 text

abstract class ConnectionDecorator implements ConnectionInterface { protected $connection; public function __construct(ConnectionInterface $connection) { $this->connection = $connection; } public function query($query) { return $this->connection->query($query); } }

Slide 65

Slide 65 text

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; } }

Slide 66

Slide 66 text

class ProfiledConnection extends ConnectionDecorator { public function query($query) { $this->logger->log('Query: '.$query); $this->queryCount++; return parent::query($query); } }

Slide 67

Slide 67 text

$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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Composite http://www.flickr.com/photos/xtinalamb/59989462

Slide 70

Slide 70 text

Goal   Composite lets clients treat individual objects and compositions of objects uniformly.

Slide 71

Slide 71 text

Representing an HTML form as a list of Field objects. Practical Example  

Slide 72

Slide 72 text

input[type=text] textarea input[type=text] input[type=file] Form Form

Slide 73

Slide 73 text

A Form is a collection of Field. A Field can also be a Form when it embeds several fields.

Slide 74

Slide 74 text

Field! setData($data)! Input! setData($name)! Form! setData($name)! add($name, Field $field)! remove($name)! getFields()!

Slide 75

Slide 75 text

$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);

Slide 76

Slide 76 text

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; } }

Slide 77

Slide 77 text

class Input extends Field { private $type; public function __construct($type) { $this->type = $type; } public function setData($data) { if ('file' !== $this->type) { parent::setData($data); } } }

Slide 78

Slide 78 text

class Form extends Field { private $fields = array(); public function add($name, Field $field) { $this->fields[$name] = $field; $field->setName($name); } }

Slide 79

Slide 79 text

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; } }

Slide 80

Slide 80 text

Factory Method http://www.flickr.com/photos/jerryms/6704324441

Slide 81

Slide 81 text

Goal   Define an interface for creating an object, but let subclasses decide which class to instantiate.

Slide 82

Slide 82 text

Creating several kind of Document objects with a factory (images, pdf, text…) Practical Example  

Slide 83

Slide 83 text

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(); } } }

Slide 84

Slide 84 text

Goal   Factory Method lets a class defer instantiation to subclasses.

Slide 85

Slide 85 text

DocumentFactory + createDocument($name) + newDocument($name, $content) Document Image ImageFileFactory + createDocument($name)

Slide 86

Slide 86 text

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; } }

Slide 87

Slide 87 text

class TextFileFactory extends DocumentFactory { public function createDocument($name) { return new Document($name); } }

Slide 88

Slide 88 text

class ImageFileFactory extends DocumentFactory { public function createDocument($name) { $image = new Image($name); $image->setWidth(90); $image->setHeight(120); return $image; } }

Slide 89

Slide 89 text

$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);

Slide 90

Slide 90 text

Observer

Slide 91

Slide 91 text

Goal   A subject, the observable, emits a signal to a list of modules known as observers.

Slide 92

Slide 92 text

Decoupling the dependencies of a domain object. Practical Example  

Slide 93

Slide 93 text

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); } }

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface { function attach(ObserverInterface $observer); function notify(); }

Slide 96

Slide 96 text

class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface $subject) { $reference = $subject->getReference(); $this->logger->log('New order #'. $reference); } }

Slide 97

Slide 97 text

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); } }

Slide 98

Slide 98 text

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); } }

Slide 99

Slide 99 text

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); } } }

Slide 100

Slide 100 text

class Order implements ObservableInterface { public function confirm() { $this->status = 'confirmed'; $this->save(); $this->notifyObservers(); } }

Slide 101

Slide 101 text

$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();

Slide 102

Slide 102 text

SplObserver SplSubject What’s next?  

Slide 103

Slide 103 text

Conclusion…  

Slide 104

Slide 104 text

No content