I have a pretty similar technique, but I've implemented it in a way that requires very little work if you're using Zend_Db_Table as your data access layer. There are two relevant interfaces:
1. Galahad_Model_DaoInterface which defines what a data access object should look like (things like save, fetchAll and fetchByPrimary). 2. Galahad_Model_ConstraintInterface which mirrors the Zend_Db_Table_Select API. My data mapper objects have a constraint() method (or getConstraint() if you prefer—I like "constraint" because it leads to a more fluent usage) which just calls the loaded data access object's constraint() method. That way each DAO can implement its own constraint system using a common interface. The beauty is that because the constraint interface mirrors the Zend_Db_Table_Select API, and the DAO interface is pretty close to the Zend_Db_Table API, I don't have to implement much to have everything working with a relational database. If I ever need to switch to a web service or some other data persistence method it will mean that I'll have to implement a somewhat complex constraint class, but the API will be exactly the same from the consuming code's point of view. Some example code to demonstrate: // In Controller $mapper = new Application_Model_Mapper_Article(); $constraint = $mapper->constraint()->where('category_id = ?', $category->getId()); $articles = $mapper->fetchAll($constraint); In that example, my DAO might look something like: class Application_Model_DbTable_Article extends Zend_Db_Table implements Galahad_Model_DaoInterface { public function constraint() { return new Galahad_Model_DbTable_Constraint(); } } And my constraint looks something like: class Galahad_Model_DbTable_Constraint extends Zend_Db_Table_Select implements Galahad_Model_ConstraintInterface {} As you can see, there's virtually no code in these classes because they are essentially Zend_Db_Table and Zend_Db_Table_Select objects, but my consuming code doesn't need to know that. All it knows is that it wants a list of articles with a certain category ID. The downside of this is that if I ever do switch DAO's I'll probably have to write a lot of code to properly mimic Zend_Db_Table_Select's API, but I'm OK with that because it means that for 95% of my projects, where I am using Zend_Db, I'll be up to speed with practically no work. I have a working implementation of this system if anyone's interested. I just recently moved a bunch of things around and haven't yet pushed those changes to GitHub, but I will shortly and would be happy to share the link with anyone who's interested. Cheers, <http://cmorrell.com/> *Chris Morrell* Web: http://cmorrell.com Twitter: @inxilpro <http://twitter.com/inxilpro> On Thu, Apr 15, 2010 at 11:59 AM, Hector Virgen <djvir...@gmail.com> wrote: > We are taking a different approach that works well for us. When you look at > methods like these: > > fetchLatestArticles() > fetchArticlesBasedOnCategory($category) > fetchBlueArticlesByOrderedByGermanyPaidLastMonth() > > They all have two things in common: they return articles and they rely on > criteria. > > With that in mind, we have a single method in our mappers that can cover > all of those cases: > > Default_Model_Mapper_Articles#fetchAll($criteria, $sort) > > The criteria is similar to a WHERE clause in SQL, but it's not in SQL > format. It uses objects instead: > > $category = new Default_Model_Category(); > $category->setId(123); > $mapper = new Default_Model_Articles_Mapper(); > $articles = $mapper->fetch(array( > category => $category > )); > > The mapper's fetch method then inspects the criteria array and maps it to a > SQL WHERE clause. This includes adding any required joins: > > public function fetchAll(array $criteria = null, $sort = null) > { > $select = $this->getSelect(); > if (isset($criteria['category'])) { > $select->where('category_id = ?', $criteria['category']->getId()); > } > return $this->_createCollection($select, $sort); > } > > The getSelect() method returns the bare bones select object for articles, > where only the "id" is returned. The _createCollection() method takes the > select object and fetches the result into a lazy-loading iterator. > > Sometimes this mapping can get very complex, especially if you want to > support lots of criteria. For these cases, I create a helper class whose > only job it is to build that select query. It would be used internally by > the mapper only, transparent from the controllers. > > public function fetch(array $criteria = null, $sort = null) > { > $search = new Default_Model_Mapper_Search_Articles($this); > $search->setCriteria($criteria); > $select = $search->getSelect(); > return $this->_createCollection($select, $sort); > } > > The goal behind this design is to make the controllers completely unaware > that a database even exists and makes it easy to stub in mappers for unit > tests: > > // in controller > public function indexAction() > { > // check for injected mappers (provided by unit tests) > // this logic could be moved to an action helper > if (!$articlesMapper = $this->getInvokeArg('articles_mapper')) { > $articlesMapper = new Default_Model_Mapper_Articles(); > } > > $this->view->articles = $articlesMapper->fetchAll(); > } > > There are a lot of steps in this design but it helps to ensure that at any > time you can swap out mappers or change your DB schema without having to > rewrite a single line in the controllers. I actually used this design for > one of our components and, half-way through, redesigned the entire schema > for better performance. The only changes I had to make were in the mappers > themselves. > > Another benefit was that I was able to quickly build up some new mappers to > benchmark the performance if we switched to a NoSQL database like MongoDB. I > branched my project and rewrote some mappers to use MongoDB and I was able > to compare the app side by side with the MySQL version within just a few > hours. > > -- > Hector > > > > On Thu, Apr 15, 2010 at 7:31 AM, keith Pope <mute.p...@googlemail.com>wrote: > >> On 15 April 2010 14:48, Matthew Weier O'Phinney <matt...@zend.com> wrote: >> > -- Ralf Eggert <r.egg...@travello.de> wrote >> > (on Thursday, 15 April 2010, 02:10 PM +0200): >> >> I have a question regarding a model infrastructure that uses models, >> >> data sources, data mappers and a service layer. I like this concept a >> >> lot but am still thinking about one issue. >> >> >> >> I want to get a list of all blue articles which have been ordered by >> >> users from Germany and have been paid last month. Thinking about the >> >> database this will definitely be a rather complex database join about >> at >> >> least three tables (articles, users, order). Lets call this method >> >> fetchBlueArticlesOrderedByGermanyPaidLastMonth(). >> >> >> >> Where should this method be located? >> >> >> >> - in the data source (Zend_Db_Table class) >> >> - in the model >> >> - in the data mapper >> >> - in the service layer >> >> >> >> I could find pros and cons for each solution if I want to. But what do >> >> others think? >> > >> > I typically think of this as a job for the mapper, as it's mapping the >> > data source to objects. That gives you the flexibility of either >> > wrapping it in your service layer (I do this so I can return a paginator >> > object) or simply calling it as a method off your mapper. >> >> I have started to use the repository pattern lately this seems to work >> well, so you have: >> >> - dbTable >> - mapper >> - model >> - repository >> - service >> >> So now for highly specialized queries I would have them inside the >> repository, not sure about >> fetchBlueArticlesOrderedByGermanyPaidLastMonth() but I would have a >> pretty descriptive method name :) I have implemented a criteria object >> so the rules can be easily added at runtime, so maybe something like >> getOrderedArticlesForMonth(). Using the repo you can then do something >> like: >> >> $articleOrderRepo = new Kp_Model_Repository_ArticleOrders(); >> $articleOrderRepo->addCriteria(new >> Kp_Domain_Repository_Criteria('color', '= ?', 'blue')); >> $articleOrderRepo->addCriteria(new >> Kp_Domain_Repository_Criteria('country', '= ?', 'Germany')); >> $articleOrderRepo->getOrderedArticlesForMonth(10); >> >> The only thing here is that the criteria/repository are fairly one >> dimensional so complex joins would need to be placed inside the mapper >> as its hard to accurately generate the correct joins. Also sometimes >> for large queries, for example when doing a export of all order for a >> year, I have a getRaw type method on the repository, this will then >> return data instead of objects to stop memory errors. >> >> > >> > -- >> > Matthew Weier O'Phinney >> > Project Lead | matt...@zend.com >> > Zend Framework | http://framework.zend.com/ >> > PGP key: http://framework.zend.com/zf-matthew-pgp-key.asc >> > >> >> >> >> -- >> ------------ >> http://www.thepopeisdead.com >> > >