Implementing the Domain Model - Entries and Authors
At a Glance
This chapter puts the model theory into practice by implementing Entry and Author domain entities, building data mapper classes for persistence, defining entity relationships, and wiring the domain layer into controllers and views.
What Changed Since Zend Framework 1
Modern PHP codebases typically handle entity relationships through ORMs like Doctrine (with annotations or attributes for mapping) or Eloquent (with relationship methods). The manual mapper approach shown here gives you a clearer understanding of what those ORMs do under the hood and helps you debug the inevitable edge cases that automated tools handle poorly.
Theory is worthless until you write code. The previous chapter explained why domain entities, data mappers, and persistence separation matter. This chapter implements them. As part of the Survive The Deep End: PHP and Zend Framework book, it takes the blogging application from earlier and refactors it with proper Entry and Author entity classes, dedicated mapper objects for each, relationship loading between entities, collection handling, and the controller wiring that connects the domain layer to HTTP requests and view rendering. Every pattern here - entity design, mapper persistence, lazy-loaded relationships, testable domain objects - translates directly to how modern ORMs like Doctrine and Eloquent work internally.
The Database Schema
The entries table from the previous chapter gets an author_id column, and we add an authors table:
1 2 3 4 5 6 7 8 9 10 11 12
CREATE TABLE authors ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, url VARCHAR(255) DEFAULTNULL, UNIQUE KEY (email) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE entries ADDCOLUMN author_id INTNOT NULL AFTER id, ADD CONSTRAINT fk_entries_author FOREIGN KEY (author_id) REFERENCES authors(id);
The foreign key constraint enforces referential integrity at the database level. Every entry must have a valid author. This is a database-level rule, but the domain model should enforce it too. You do not rely on the database alone for business rules.
The Author Entity
The Author entity is straightforward. It holds author data and enforces invariants:
The email validation uses filter_var(), which is sufficient for most cases. The email is normalised to lowercase on set. The URL is optional and validated only when provided. These are domain rules, not form rules. Even if you create an Author programmatically without a form, these rules still apply.
Refactoring the Entry Entity
The Entry class from the previous chapter needs to support an author relationship:
The constructor now requires an Author object. You cannot create an entry without an author. The type hint enforces this at the PHP level. If someone passes null or a string, PHP throws a fatal error before your code even runs. This is a deliberate design choice: the entity makes invalid states impossible to represent.
The Author Mapper
The Author mapper follows the same pattern as the Entry mapper from the previous chapter:
The findByEmail method is a domain-specific query. Not everything is a simple find($id). Your mapper should expose methods that match how the application actually looks up data. If the blogging app needs to find authors by email (for login, for duplicate checking), the mapper provides that method.
The Entry Mapper with Relationship Loading
The Entry mapper now needs to load the associated Author when converting database rows to entities:
The _rowToEntity method accepts an optional Author parameter. When loading entries for a specific author (via fetchByAuthor), the author is already known, so passing it in avoids a redundant database lookup for each row. When loading a single entry or all entries, the mapper fetches the author from the AuthorMapper.
This is the N+1 query problem in its raw form. Loading 50 entries triggers 50 author lookups. For a blog with a single author, this is fine - the same author gets loaded once and cached by the mapper if you add caching. For a multi-author blog at scale, you would batch-load the authors in a single query. ORMs like Doctrine handle this with eager loading and proxy objects. Here, you handle it manually, which means you see exactly what queries run and when.
Wiring the Domain Layer into Controllers
The controller changes are minimal. Instead of calling Zend_Db_Table directly, you call the mapper:
The controller is thinner now. It coordinates between the HTTP request, the form, the mapper, and the view. It does not contain business logic. It does not contain SQL. The createAction loads the author, constructs the entity (which validates the data), and asks the mapper to save. If any step fails, an exception propagates to the error handler.
Mapper tests use mock Zend_Db_Table objects to verify that the mapper calls the right methods with the right data, without touching a real database. Integration tests against a test database verify the full round trip.
Frequently Asked Questions
Why not just use JOINs and return flat arrays? You can. For read-only reporting queries, flat arrays are often appropriate. But flat arrays do not enforce business rules. They do not validate data on construction. They do not protect invariants. Domain entities give you a place to put that logic. If you skip entities, the validation and rule enforcement scatters across controllers, forms, and ad hoc checks throughout the codebase.
How do I handle collections of entities? In this example, fetchAll() returns a plain PHP array of Entry objects. For more sophisticated needs - lazy loading, filtering, sorting without hitting the database - you can wrap results in a collection class. Doctrine uses ArrayCollection and PersistentCollection. For a ZF1 application, a simple array is usually sufficient until you have a specific reason to add collection behaviour.
Should the Author mapper be injected into the Entry mapper? Yes, and this example does that through the constructor. It is basic dependency injection. If you wanted to test the Entry mapper in isolation, you would pass a mock Author mapper that returns predictable Author objects without touching the database. This makes tests fast and reliable.
What if I need to update both the entry and the author in one operation? That is a service layer concern. The controller would call a service method like $entryService->updateEntryWithAuthor($entryData, $authorData), and the service would coordinate the two mapper calls inside a database transaction. Neither mapper knows about the other’s transactions. The service wraps both in a single beginTransaction / commit / rollBack block.
Related Reading
The Model - The theory behind the patterns implemented here