This is a starting point for applications with complex business logic. It is a PHP MVC skeleton with a Domain (Domain Model), Data Mapper and Service Layer. It uses Zend Framework 2 however may be ported to other frameworks quite easily. You are welcome to use it as the foundation for your next application.
2. Dealing with business complexity
General Requirements
• Means of organizing logic
• Flexible database interaction
• A clear API for client code
Solution
• MVC skeleton with a Model
• Model
• The Domain, with:
• Domain object factories
• Collection handling and logic
• Proxies
• Single-valued properties
• Multi-valued properties
• Identity map
• The Data Mapper
• The Service Layer
• View – OOTB
• Controller – OOTB
3. The code
• Zend Framework 2
• Easily ported to others
• Actively maintained
• Fork it on github
• Two modules
• Core
• Mapper (implementation)
• http://kimprince.com
• Detailed usage discussion
• Code examples
4. THE MVC ‘MODEL’
- An umbrella for a whole other structure
- The Domain, The Data Mapper, The Service Layer
5. The Domain: Domain objects
• Represent business objects
• Typically nouns such as ‘customer’, ‘product’
• Include data and behaviour
• Each implements its own interface
• Easily proxied or mocked
• Abstract parent
• Provides common features
• Encapsulation
• $allowed array
• Keys are property names
• Boolean values describe required/optional behaviour
• $data array stores real values
6. The Domain: Domain objects (cont)
• Construction
• Object factories do the heavy lifting
• Constructor expects an array of filtered values
• Checks that mandatory values have been supplied
• Sets any factories and finders which may have been injected
• Access
• Properties are ‘open for reading, closed for writing’
• __get() – assumes client code can read properties
• __set() – assumes client code can NOT write properties
• Override these with getPropertyName(), setPropertyName($value)
• Identity
• Abstract accessors: getId(), setId()
• For globally unique id: getShortType($object) . $object->getId()
• Value objects are handled differently
7. The Domain: Object proxies
• Most Domain objects have a proxy
• Factories may substitute proxies for real properties
• A proxy has a real object id and finder (Mapper)
• A proxy is realised if/when it is accessed
• Unless accessing the id only
8. The Domain: Collections
• One per Domain class (usually)
• Each implements own interface
• Easily proxied or mocked
• Iterable and countable
• Commonly returned by Mappers
• Contain a factory and an array of raw data
• May contain custom logic for the given type
• For sorting, filtering, adding members, removing, comparing, …
• Pass out a clone when filtering or sorting
9. The Domain: Collection proxies
• Most collections have a proxy
• Class diagram (over)
• Collections and their proxies inherit from a common hierarchy
• Key difference:
• A collection becomes an iterator
• A collection proxy becomes an iterator aggregate
• Iterator aggregate references the real iterator
• On construction, proxies receive
• A finder – for retrieving the collection
• A method name – which exists on the finder
• An array of arguments – which are passed to the finder method
• Collection proxies are used extensively by factories
• As a substitute for multi-valued properties
11. The Domain: Object factories
• Often long and complex classes
• Loaded via an abstract factory
• Use the factory method pattern
• Factories are service locator aware
• Include features related to N+1 selects handling (more later)
Method Responsibilities
New Object Defaults • Add defaults for new objects
• Override inputs where necessary
Type Conversion • Cast inputs to required types or proxies
Add Relations • Add collection proxies for multi-valued properties
Instantiation • Inject to constructor: $data, $finders, $factories
12. The Domain: Identity map
• A safety net
• Performance
• Model integrity
• Main clients: Object factories
• Check for existing before instantiating a Domain object
• Add newly instantiated Domain objects to the map
• Other clients: Data Mappers
• Check for existing before executing a ‘find’ query
• Add newly created entities to the map
• Following a db insert, since that’s when the id is assigned
13. The Mapper
• Translates between object world and the db
• Converts between under_score and camelCase
• Usually one Mapper class per Domain object
• A typical Mapper has:
• find($id) – returns a single Domain object
• Other single-object finders, such as findByName($name)
• Numerous collection finders, such as findByFoo($id)
• insert($object) – executes an sql insert
• update($object) – executes an sql update
• Mappers are loaded via an abstract factory
14. The Service Layer
• Highest layer of the Model
• Depends on Domain and Mapper
• Service-locator-aware
• Composes an event manager
• Loaded via an abstract factory
• Tasks
• “Whatever clients need…”
• Persisting changes
• Including database transactions
16. Views, controllers, helpers
• Views
• OOTB
• Extensive use of helpers
• Forms – Consider hand-crafted for more control
• Confidence in performance, based on:
• Identity map means no fetching duplicates
• Object factories use collection proxies to avoid N+1 selects
• Controllers
• OOTB
• Service locator aware
• Helper trait
• Inserted into most supertypes
• getShortType() identifies a family of objects
• TheObject, TheObjectMapper, TheObjectFactory, TheObjectCollection
17. A note on filtering and validation
• May be located in Service Layer or Domain
• Option 1: Service Layer
• Easy to manage – i.e. audit, fix gaps
• Option 2: Domain (and Mapper)
• More difficult to manage, but a better fit with OO
• Three data contexts:
• Newly created entities – filter and validate in the object’s factory
• Updates to object properties – filter and validate in custom setters
• Finder parameters – filter and validate in Mappers themselves
18. The N+1 selects problem
• Occurs where a Domain object has a multi-valued property
• ‘First degree’ problems solved when factory used correctly, e.g.:
• If bars is multi-valued property of Foo…
• The Foo factory sets Foo->bars to a Bar collection proxy
• ‘Second degree’ problems require new factory ‘flavours’
• Flavours are stored in factories as class constants, e.g. FOO::BARS_WITH_BAZ
• Use setFlavour() and addFlavour() to configure factory
• In flavoured factories, collection proxies eagerly load navigable associations
• This affects Mappers too, e.g. Bar Mapper needs findByFooWithBaz($foo)
• See detailed usage discussion for more examples
19. Using the Starter Application
• See detailed usage discussion for notes on:
• Defining the business problem
• Building the Domain
• Building the Data Mapper
• Building the Service Layer
• Building the presentation layer (views, controllers)
• Iterating to completion
• Also in the usage discussion:
• Create your own Starter Application from Zend’s ZF2 Skeleton App.