Objects in the model layer: Part 2
I previously talked about the terms I use for objects in the model layer and now it’s time to put some code on those bones. Note that,as always, all code here is example code and not production-ready.
An entity
My entities are plain old PHP objects:
namespace Book;
class Entity
{
protected $id;
protected $author;
protected $title;
protected $isbn;
public function __construct($data = array())
{
$this->populate($data);
}
// Data transfer methods
public function populate($data)
{
if (array_key_exists('id', $data)) {
$this->setId($data['id']);
}
// repeat for other properties
}
public function getArrayCopy()
{
return array(
'id' => $this->getId(),
// repeat for other properties
);
}
// Entity-specific methods
public function isValidIsbn()
{
// validate ISBN and return true/false
}
// Property getters and setters
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
// Repeat for other properties...
}
There’s nothing particularly complicated here. We have an object with a number of properties and some methods. This object represents a book, so we have properties of author, title and isbn. We need to be able to set and retrieve the properties so there is a get and set method for each one (only getId() and setId() are in the code snippet above to save space!)
Generally, I populate an entity from a mapper and use a pair of methods to do this: populate() and getArrayCopy(). These methods transfer the data in the properties to and from an array.
There are also entity-specific methods within the entity. For this object, I have a method called isValidIsbn(); for a user object, I may have a method called getFullName() which concatenates the user’s first name and surname.
A mapper
The mapper knows how to load and save entities. This is a hand-rolled one:
namespace Book;
use PDO;
use BookEntity;
class Mapper
{
protected $pdo;
public function __construct($dsn, $username, $password)
{
$this->pdo = new PDO($dsn, $username, $password);
}
public function loadById($id)
{
$sql = 'SELECT * FROM book WHERE id = :id';
$statement = $this->pdo->prepare($sql);
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute(array('id' => $id));
$result = $statement->fetch();
if ($result) {
$book = new Entity($result);
return $book;
}
return false;
}
public function fetchAll($order)
{
// Select all books from database using PDO
// iterate over each one and create a BookEntity object
}
public function save(Entity $book)
{
$data = $book->getArrayCopy();
if ($data['id'] > 0) {
// Update data in table using PDO and set $result
} else {
// Insert data into table using PDO and set $result
}
return $result;
}
public function delete($id)
{
// Delete row in table using PDO
}
}
In the mapper, I have methods that load with multiple entities and also ones that work on a single one. I like to use the method prefix “find” for methods that will return an array of entities and “load” for methods that return a single entity. This is just a stylistic thing, but I find it makes reading code easier. We then have save and delete methods that allow us to save and remove an entity from the data store.
This is just a skeleton of a specifically written mapper that users PDO. In a ZF2 application I use ZfcBaseMapperAbstractDbMapper and in other applications I tend to abstract the common code into a base class and extend.
Service objects
Lastly, service objects provide the API to the rest of the application:
namespace Book;
use BookMapper;
class Service
{
protected $mapper;
public function __construct(Mapper $mapper)
{
$this->mapper = $mapper;
}
public function fetchAllByTitle()
{
$results = $this->events->trigger(__FUNCTION__.'.pre', $this, array(),
function ($result) {
return is_array($result);
}
);
if ($results->stopped()) {
return $results->last();
}
$books = $this->mapper->fetchAll('title');
$this->getEventManager()->trigger(__FUNCTION__.'.post', $this,
array('books' => $books));
return $books;
}
public function loadById($id)
{
$results = $this->events->trigger(__FUNCTION__.'.pre', $this,
array('id' => $id),
function ($result) {
return ($result instanceof BookEntity);
}
);
if ($results->stopped()) {
return $results->last();
}
$book = $this->mapper->loadById($id);
$this->getEventManager()->trigger(__FUNCTION__.'.post', $this, array('book' => $book));
return $book;
}
// etc
}
A simple service object essentially proxies through to the mapper. I generally have more specific methods, such as fetchAllByTitle(), but that’s a personal preference. In this example, I have an ZF2 event manager in play and the service object triggers events as required.
The service object is also useful when there are multiple related objects. For instance, if books had tags that were loaded separately, then I would have a method such as loadTagsIntoBook($book) on this service object. Of course, others prefer to use an ORM, such as Doctrine for these things.
Summary
This overview shows the type of methods that I have in each type of core object in my model layer. My controllers and view helpers only ever deal with service objects and entities, so I can change my mapper at any time.
You also need to think carefully where the business logic lives. I’m a fan of putting the logic in the entities as well as in service objects. Others tend to like their entities to be quite “dumb”, though.
So if you load the book tags with a separate method, then you don't have joins in your sqls? Or is that again something on a case-to-case basis?
Thanks :)
Robert,
Yes. If I'm using a loadTagsIntoBook method, then it's a separate call to the datastore. I would do this for situations when I don't need the related data most of the time when interacting with the entity.
For data that's needed all the time, then I use a join and a more complex mapper.
Hi Rob,
Out of curiosity, since in my company I have almost the same code architecture in all our projects that you're using:
Based on your example, if you have the book tags (1 boog = many tags), do you usually go with a set/get tags method in your Book entity, and populate the book with the tags in your mapper? If so, what happens if you need have a case when you want a single book? Since if you load the book, running a "getTags()" would return an empty set, even if in the DB there are actually sets.. A solution is the lazyLoading… But was curious how do you do it?
Thanks!
Andrei,
I load the book via a method in the service object. This method can then ensure that the tags are loaded for the book entity.
That's uncanny – I swear, this code is practically identical to mine :-)
Not sure if I like this example entirely; just talking out loud for a sec.
How are is the Event Manger being passed or instantiated?
More importantly, the pre and post methods look like good candidates for a caching mechanism. Which makes me concerned that the real logic is not accessible unless all the irrelevant event listeners are removed prior.
What I'm trying to say is that I think I'd prefer having an additional layer that could accommodate things like persistence or caching but still have a service object, ie. Manager, that would solely executes the real logic, $book = $this->mapper->loadById($id), is a simple example, and doesn't answer my previous question about where the other listeners register them selves, e.g lets say the tags supplement wants to know when to add its data.
Another personal preference would be that service objects exposing their public API should contain methods that can be more easily searched for, i.e. if you have a bunch of services all with 'loadById' then which one pertains to the Book service? Maybe my regex is not that great…, I'd suggest loadBookById.
Greg,
I'd love to see a blog post showing sample code to explain your thoughts in more detail.
@ Greg
Give the bunch of services proper names like $bookService, so $bookService->loadById() is more than clear.
@ Rob
Any suggestions for proper error handling in the mapper/service layer, f.ex. handling PDOException?
Frank,
I generally catch storage-layer specific exceptions in the mapper and re-throw my own exception. The service layer tends to deal with these on an application specific basis.
Great insights Rob, (as always)
Ive been trying to follow similar model inspired by Domain Driven Design practises, however I wonder if you could help me with a puzzle. The entities I am creating now are to represent objects with more than 20-30 properties.
This, in a class, looks very unmanagable and unfriendly.
What would you do if yuo have to model objects/entities with 30-40 properties ?
The only thing that comes to mind is to break it up into smaller entities based on how the properties are grouped in their bussiness logic ?
Bobby,
I have entities with over 100 properties :)
The obvious choices are:
1. have lots of getters and setters
2. Write an __call() method
I've done both…
Thanks for writing about this. Would love to know how you handle more complex models. If you start with an entity "book" and build a model, mapper and service, it's all pretty basic. But say you have to deal with a CMS form being saved, with data including the book, the tags, some author data, and other meta data properties, all spread out over 6 or more db tables. And you also want to make sure you use PDO transactions for the whole process. So you can't use a sequential style of first saving Tags in a Tagmapper, then saving Author data in a Authormapper, etc. It has to be done in one transaction.
Which is the best layer to use DB cache? Service? Mapper?.
Thanks for the post.
@Matt,
I've done several different types of modeling while my approach looks similar to this is it also slightly different.
The service is really just a container that controls the creation, updating, and over all flow of the "process" that you are doing. They are not limited to a single area (although can work together for shared functionality).
For instance; I might have a piece of functionality to create an organization. This organization also has a many-to-many user table that needs a transaction and might do more work later. I create both objects say Organization and OrganizationUser (which is also stored in UserOrganization for performance). I call my organization service create method…
My mappers control all of the inserting of data and handling of the data; I generally use an abstract mapper to handle this. Then you can create either helper static methods on the abstract to start and close a transaction OR even just fetch the connection and do it manually — the abstract here helps from a unit testing perspective.
The data that is coming in is not so much the worry; you SHOULD design your service so that it takes all of the relevant information since it is a "process" and as such should be treated like it :)
Example:
You can do even more if you start attaching and handling things by events as well but this is a great place to start.
Jose,
As with all these things, it depends :) Personally I cache db specific in the db layer and business data in the service layer.
@Mike: thanks a lot for your reply. Makes sense what you say. The thing is, there's a mismatch between the Domain objects and the database tables. In some projects very big.
Say you have a social webapp in which people can add "Visits to remarkable places". They fill in a form with the date, the name of the place, the street, city, country, etc. In a normalized db these will be put in a table Visits, Addresses, Cities, Countries, Users, etc. all referencing each other with foreign keys/constraints. So saving this single webform will put data in 5 or more tables. And the real complicated stuff happens when more users add data and an item in one table (say an address in table Address) is being referenced by more users and Visits. When user 1 wants to come back and update the street name of an address, you first have to check if not someone else is referencing that street name also. And in that case, you can't update the street name but have to duplicate it (otherwise user A could change the data of user B, not what you want in this case). Etc etc, all quite complex logic.
In some layer of the application this mismatch must be handled, but I am having trouble figuring out where. You don't want to bloat the controllers. Mixing a lot of database methods in the model feels messy as well. But it's also difficult to control the db queries and the logic I describe above when the db layer is abstracted away 'too early' (leading to too many queries being run without knowing it, not having control over the data integrity, the transactions, etc.).
Your example of the Service looks like a good option I can use as an idea to work out further.
@Mike: one issue I still see in your code example, is that the saveBook() method still expects a full BookEntity object. And separately an array of tags. Which both get saved separately inside the method. So something higher up in the chain using the CmsAuthorService, say a Controller, has to put together the BookEntity. And that Book Entity consists of data that is spread out over multiple tables (say the BookEntity contains a reference AuthorID and PublisherID). And so has all kinds of constraints and relations between those tables which have to be checked before saving or updating.
So before the controller can use the saveBook() method, it has to create the BookEntity object. And to be able to create that BookEntity object, that controller has to go save or update an author in the db and get the AuthorId, save or update some publisher data and get a PublisherID, check what can be updated or what need to be duplicated, etc. It's all this complex logic for which I find it difficult to find the right place.