Practical PHP Patterns: Mapper
Join the DZone community and get the full member experience.
Join For FreeThe Mapper pattern is used to establish a communication between two subsystems, when the integration must be managed in a way to avoid creating mutual dependencies or even one-way dependencies between them.
The classic example of this pattern is its specialization for data storage, the Data Mapper, which as you can see from the relative article translates data between an object graph and an external storage such as a relational database (most advanced ORMs like Doctrine 2 are Data Mappers), but also a document-oriented database or any other kind of persistent storage.
The Mapper component is naturally dependent on both the subsystems it connect, but they are not aware at all of its existence. No additional hard dependencies are introduced from the subsystems towards other components.
Following our example, Doctrine 2 borrows the Hibernate's approach to mapping: it parses annotations or unobtrusive XML files for specifying metadata on the object-oriented side, and uses a wrapped version of PDO (with ordinary database drivers) on the database side. Neither the database tables are aware of the existence of an object model, nor the objects are ideally aware of them being persisted into a tabular form.
Client code
The main issue in wiriting a Mapper, in all its specializations, is how to invoke it, since the connected subsystems do not know anything about it, nor they can instance it without a dependency being created.
The most common solution is to use an higher-level layer that drives the three components at the same time. In PHP applications, this is usually a controller layer, which by default starts a transaction and commit it following the lifecycle of the served HTTP request. The domain objects are then passed to the Data Mapper's Facade or they depend on a set of Repository interfaces implemented with the aid of the Data Mapper.
Another possible solution is to implement an Observer pattern, so that interfaces or abstractions in the two subsystems break the dependency. The Mapper implementation classes would realize these interfaces, but there is a trade-off produced by the introduction of interfaces in the subsystems.
Mapper vs. Gateway
In comparison, a Gateway is simpler to implement than a Mapper, although your mileage may vary depending on the domain and your requirements.
Basically, a Mapper is more complex but by definition it has the benefit that neither of the two components depend on it. Thus the Mapper is even more decoupled than one of the two subsystems dependent on an interface or abstraction of a Gateway: in fact, you can simply throw it out the Mapper and the two subsystems will work as long as they can to fulfill their own responsibilities. Whereas, you can't remove a Gateway from an application without specifying an alternate implementation.
This pattern is also different from a Mediator: no one of the objects at the same or at a lower layer of the Mapper is aware of it.
Example
The code sample is a simple Data Mapper that shuffles data between database and Twitter, just to shake the notion that all Data Mappers are ORMs.
The code presented here is only a one-way implementation, since a bidirectional one would involve authentication and would increase too much the size of this article.
The first side of the Mapper is a domain model built over the database (not represented here). The second side is Twitter's own web services instead, which we can be sure does not depend on our little in-memory database.
<?php class TwitterMapper { private $connection; /** * The Mapper composes one side of the computation (the database) * in the form of its Facade (the connection). * The other side is Twitter's web service, so it isn't injected * for simplicity. */ public function __construct(PDO $connection) { $this->connection = $connection; } /** * the only functionality I need from the feed */ public function synchronizeLastTweets(array $usernames) { foreach ($usernames as $username) { $endPoint = "http://twitter.com/statuses/user_timeline/{$username}.xml?count=1"; $buffer = file_get_contents($endPoint); $xml = new SimpleXMLElement($buffer); $this->_addTweet($xml->id, $username, $xml->status->text); } } public function _addTweet($id, $username, $text) { $stmt = $this->connection->prepare('INSERT INTO tweets (id, username, text) VALUES (:id, :username, :text)'); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->bindValue(':username', $username, PDO::PARAM_STR); $stmt->bindValue(':text', $text, PDO::PARAM_STR); return $stmt->execute(); } } $pdo = new PDO('sqlite::memory:'); $pdo->exec('CREATE TABLE tweets (id INT NOT NULL, username VARCHAR(255) NOT NULL, text VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); $mapper = new TwitterMapper($pdo); // higher-level layer $mapper->synchronizeLastTweets(array('giorgiosironi'));
Opinions expressed by DZone contributors are their own.
Comments