Could you help to extend a little bit about the Zend Quickstart: In the tutorial, we use the Mapper to update a single Guestbook. What if I want to update more than one Guestbook? And based on some conditions?
For example, I have an action to delete all Guestbooks that were created before 2012-12-21. What should I update to achieve that?
Does my approach make sense?
// application/models/GuestbookMapper.php
class Application_Model_GuestbookMapper
{
public function deleteByCreatedBefore($date)
{
$this->getDbTable()->deleteByCreatedBefore($date);
}
}
// application/models/DbTable/Guestbook.php
class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
{
public function deleteByCreatedBefore($date) {
$where = $this->getAdapter()->quoteInto('created < ?', $date);
$this->delete($where);
}
}
Thanks,
If you are using the quickstart model/mapper and want to stay true to that data mapper paradigm you wouldn't have anything in your Application_Model_DbTable_Guestbook except for properties ('name', 'primary'...). The DbTable model would exist as the database adapter for that single table.
Your delete function would be placed in the mapper.
class Application_Model_GuestbookMapper
{
public function deleteByCreatedBefore($date)
{
$where = $this->getDbTable()->quoteInto('created < ?', $date);
//delete() returns num of rows deleted
$this->getDbTable()->delete($where);
}
}
This will work but may not be the best/safest way to achieve the required functionality.
This particular example of the Data Mapper is very simple and might be somewhat misleading to some people. The Guestbook example of the Mapper is really not a good representation of the mapper as the database row and the domain model (Application_Model_Guestbook) map 1 to 1 (one database column to one model property).
Where the Data Mapper starts to shine is when you need to map several database tables to a single Domain Model. With the understanding that your Domain Model (Application_Model_Guestbook) may have to effect more then one database table each time delete() is called, the structure for the delete() function is important.
What should you do to accomplish a delete with the mapper?
First: update Application_Model_GuestbookMapper::fetchAll() to accept a $where parameter, I usually setup this type of function to accept an array that sets the column and the value.
//accepted parameters: Zend_Db_Table::fetchAll($where = null, $order = null, $count = null, $offset = null)
//accepts array (column => value )
public function fetchAll(array $where = null)
{
$select = $this->getDbTable()->select();
if (!is_null($where) && is_array($where)) {
//using a column that is not an index may effect database performance
$select->where($where['column'] = ?, $where['value']);
}
$resultSet = $this->getDbTable()->fetchAll($select);
$entries = array();
foreach ($resultSet as $row) {
$entry = new Application_Model_Guestbook();
$entry->setId($row->id)
->setEmail($row->email)
->setComment($row->comment)
->setCreated($row->created);
$entries[] = $entry;
}
return $entries;
}
Second: Refactor your Application_Model_GuestbookMapper::deleteByCreatedBefore() to accept the output from fetchAll() (actually it would be simpler to just build a delete() function that accepts the output: array of Guestbook objects)
//accepts an array of guestbook objects or a single guestbook object
public function deleteGuestbook($guest)
{
if (is_array($guest) {
foreach ($guest as $book) {
if ($book instanceof Application_Model_Guest){
$where = $this->getDbTable()->quoteInto('id = ?', $book->id);
$this->getDbTable()->delete($where);
}
}
} elseif ($guest instanceof Application_Model_Guest) {
$where = $this->getDbTable()->quoteInto('id = ?', $guest->id);
$this->getDbTable()->delete($where);
} else {
throw new Exception;
}
}
Deleting a domain object as an object will become more important as you have to consider how deleting an object will affect other objects or persistence (database) paradigms. You will at some point encounter a situation where you don't want a delete to succeed if other objects still exist.
This is only an opinion but I hope it helps.
Related
I have the following example in which I tend to use a couple of classes, to create a simple web app.
The file hierarchy seems like this.
> cupid
- libs
- request
- router
- database
- view
- bootstrap.php
- index.php
The index.php just calls the bootstrap.php which in turn contains something like this:
// bootstrap.php
namespace cupid
use request, router, database, view;
spl_autoload_register(function($class){ /* autoload */ });
$request = new view;
$response = new response;
$router = new router;
$database = new database;
$router->get('/blog/{id}', function($id) use ($database, $view) {
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?",[$id]);
$view->layout('blogPage', ['article'=>$article]);
});
As you can probably tell, my problem is this line:
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?", [$id]);
Which I don't want to use, and instead try a " Domain Object Model " approach.
Now, given that I will add another folder called domain, with blog.php
> cupid
- domain
- Blog.php
- libs
...
And fill blog.php with properties mapping table rows, and getter and setters ..
namespace App\Domain;
class Blog {
private $id, $title, $content, $author;
public function getTitle(){
return $this->title;
}
public function setTitle($title){
$this->title = $title;
}
...
}
My question is: Assuming my understanding of DOM is so far correct, and that I have a CRUD/ORM class, or a PDO wrapper to query the database;
"How can I tie together, i.e. the blog model with the PDO wrapper to fetch a blog inside my bootstrap file?"..
As far as a Domain Object you basically already have written one, your blog object. To qualify as a domain model all a class must to is to provide a representation along with any of the functionality of a concept within your problem space.
The more interesting problem here and the one you appear to be struggling with is how to persist a domain model. Keeping with the tenet of the single responsibility principle your Blog class should deal with being a blog post and doing the things that a blog post can do, not storing one. For that you would introduce the concept of a repository of blog posts that would deal with storing and retrieving objects of this type. Below is a simple implementation of how this can be done.
class BlogRepository {
public function __construct(\cupid\database $db){
$this->db = $db;
}
public function findById($id){
$blogData = $this->db->select("select * from blog where id = ?", [$id]);
if ($blogData){
return $this->createBlogFromArray($blogData);
}
return null;
}
public function findAllByTag($tag){...}
public function save(Blog $blog) {...}
private function createBlogFromArray(array $array){
$blog = new Blog();
$blog->setId($blogData["id"]);
$blog->setTitle($blogData["title"]);
$blog->setContent($blogData["content"]);
$blog->setAuthor($blogData["author"]);
return $blog;
}
}
Then your controller should look something like this.
$router->get('/blog/{id}', function($id) use ($blogRepository, $view) {
$article = $blogRepository->findById($id);
if ($article) {
$view->layout('blogPage', ['article'=>$article]);
} else {
$view->setError("404");
}
});
To truly be SOLID the above class should be a database specific implementation of a BlogRepository interface to adhere to IoC. A factory should also probably be supplied to BlogRepository to actually create the blog objects from data retrieved from the store.
In my opinion one of the great benefits of doing this is you have a single place where you can implement and maintain all of your blog related interactions with the database.
Other Advantages to this method
Implementing caching for your domain objects would be trivial
Switching to a different data source (from flat files, blogger api, Document Database Server,PostgresSQL etc.) could be done easily.
You can alternatively use a type aware ORM for a more general solution to this same problem. Basically this Repository class is nothing more than a ORM for a single class.
The important thing here is that you are not talking directly to the database and leaving sql scattered throughout your code. This creates a maintenance nightmare and couples your code to the schema of your database.
Personally I always tend to stick the database operations in a database class which does all the heavy lifting of initialising the class, opening the connection etc. It also has generic query-wrappers to which I pass the SQL-statements which contains the normal placeholders for the bound variables, plus an array of the variables to be bound (or the variable number of parameters approach if thats suits you better). If you want to bind each param individually and not use the $stmt->execute(array()); You just pass in the types with the value in a data structure of your choosing, multi dim array, dictionary, JSON, whatever suits your needs and you find easy to work with.
The model class it self (Blog in your case) then subclasses the Database. Then you have a few choices to make. Do you want to use the constructor to create only new objects? Do you want it to only load based on IDs? Or a mix of both? Something like:
function __construct(id = null, title = null, ingress = null, body = null) {
if(id){
$row = $this->getRow("SELECT * FROM blog WHERE id = :id",id); // Get a single row from the result
$this->title = $row->title;
$this->ingress = $row->ingress;
$this->body = $row->body;
... etc
} else if(!empty(title,ingress,body)){
$this->title = title;
... etc
}
}
Maybe neither? You can skip the constructor and use the new(title, ingress, body), save() and a load(id) methods if thats your preference.
Of course, the query part can be generalised even further if you just configure some class members and let the Database-superclass do the query building based on what you send in or set as member-variables. For example:
class Database {
$columns = []; // Array for storing the column names, could also be a dictionary that also stores the values
$idcolumn = "id"; // Generic id column name typically used, can be overridden in subclass
...
// Function for loading the object in a generic way based on configured data
function load($id){
if(!$this->db) $this->connect(); // Make sure we are connected
$query = "SELECT "; // Init the query to base string
foreach($this->columns as $column){
if($query !== "SELECT ") $query .= ", "; // See if we need comma before column name
$query .= $column; // Add column name to query
}
$query .= " FROM " . $this->tablename . " WHERE " . $this->idcolumn . " = :" . $this->idcolumn . ";";
$arg = ["col"=>$this->idcolumn,"value"=>$id,"type"=>PDO::PARAM_INT];
$row = $this->getRow($query,[$arg]); // Do the query and get the row pass in the type of the variable along with the variable, in this case an integer based ID
foreach($row as $column => $value){
$this->$column = $value; // Assign the values from $row to $this
}
}
...
function getRow($query,$args){
$statement = $this->query($query,$args); // Use the main generic query to return the result as a PDOStatement
$result = $statement->fetch(); // Get the first row
return $result;
}
...
function query($query,$args){
...
$stmt = $this->db->prepare($query);
foreach($args as $arg){
$stmt->bindParam(":".$arg["col"],$arg["value"],$arg["type"]);
}
$stmt->execute();
return $stmt;
}
...
}
Now as you see the load($id), getrow($query,$args) and query($query,$args) is completely generic. ´getrow()´is just a wrapper on query() that gets the first row, you may want to have several different wrappers that to or interpret your statement result in different ways. You may also even want to add object specific wrappers to your models if they cannot be made generic. Now the model, in your case Blog could look like:
class Blog extends Database {
$title;
$ingress;
$body;
...
function __construct($id = null){
$this->columns = ["title","ingress","body","id",...];
$this->idcolumn = "articleid"; // override parent id name
...
if($id) $this->load($id);
}
...
}
Use it as so: $blog = new Blog(123); to load a specific blog, or $blog = new Blog(); $blog->title = "title"; ... $blog->save(); if you want a new.
"How can I tie together, i.e. the blog model with the PDO wrapper to fetch a blog inside my bootstrap file?"..
To tie the two together, you could use an object-relational mapper (ORM). ORM libraries are built just for glueing your PHP classes to database rows. There are a couple of ORM libraries for PHP around. Also, most ORMs have a built in database abstraction layer, which means that you can simply switch the database vendor without any hassle.
Considerations when using an ORM:
While introducing a ORM also introduces some bloat (and some learning), it may not be worthwhile investing the time for simply a single Blog object. Although, if your blog entries also have an author, one or multiple categories and/or associated files, an ORM may soon help you reading/writing the database. Judging from your posted code, an ORM will pay off when extending the application in the future.
Update: Example using Doctrine 2
You may have a look at the querying section of the official Doctrine documentation to see the different options you have for read access. Reconsider the example you gave:
// current implementation
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?",[$id]);
// possible implementation using Doctrine
$article = $em->getRepository(Blog::class)->find($id);
However, ideally you define your own repository to separate your business logic from Doctrines API like the following example illustrates:
use Doctrine\ORM\EntityRepository;
interface BlogRepositoryInterface {
public function findById($id);
public function findByAuthor($author);
}
class BlogRepsitory implements BlogRepositoryInterface {
/** #var EntityRepository */
private $repo;
public function __construct(EntityRepository $repo) {
$this->repo = $repo;
}
public function findById($id) {
return $this->repo->find($id);
}
public function findByAuthor($author) {
return $this->repo->findBy(['author' => $author]);
}
}
I hope the example illustrates how easily you can separate your business domain models and logic from the underlying library and how powerful ORMs can come into play.
I'm requesting the community's wisdom because I want to avoid bad coding practices and/or mistakes.
I'm having a php class wich is an objects manager. It does all the work with the database: inserting new data, updating it, getting it and deleting it (I've read it's called CRUD...). So it has a function that gets an element by id.
What I want to write is a function that gets a list of objects from the table.
I will then use a mysql query that goes something like
SELECT * FROM mytable WHERE column1='foo'
And then some order by and limit/offset.
However, in my application there are different cases in which I will need different lists from this table. The WHERE clause will then be different.
Should I write different functions, one per type of list?
Or should I write one generic function to which I will send arguments that then dynamically creates the query? If so, do you have any advice on how to do this properly?
EDIT:
Thanks for all your answers! I should tell that I'm not using any framework (maybe wasn't the best idea...), so I didn't know about query builders. I'll investigate that (either finding a standalone uery builder or migrating to a framework or writing my own, I don't know yet). That will be useful any time I need to execute a mysql query :-)
Although I'm still confused:
Let's say I need several lists of clients (objects), for example all clients, clients over 18, clients currently online...
What approach would be best to retrieve those lists? I can either have 3 functions in my clients manager
allClients() {//execute a specific query and return list of objects}
allClientsOver18() {//execute specific query and return list of objects}
allClientsOnline() {//execute specific query and return list of objects}
or I can have one function tht builds the query based on parameters
listClients($some, $parameters)
{
//Build the query based on the parameters (definitely need a query builder!)
//Execute the query
//return list of objects
}
Which approach would be best (I guess it depends on circumstances) and mostly, why?
Thanks in advance!
Rouli
Thanks for all the info on query builders, I didn't even know it existed! :-) However I'm still confused as to wether I should write one specific function for each case (that function can still use the query builder to write its specific query), or write one generic function that builds dynamically the query based onf parameters. Which would be better in which case? I've added an example in my question, hope it makes it clearer!
This depends on how often you use each of these isolated queries, how complex the conditions are and how often you my need to combine the conditions with other queries. For eaxample if each the "online" and "over18" are just simple conditions then you could just use the normal findBy logic from my example:
$table = new MyTable($db);
$onlineOnly = $table->findBy(array('is_online' => true), null, null);
$over18Only = $table->findBy(array('is_over_18' => true), null, null);
$onlineOver18 = $table->findBy(array('is_over_18' => true, 'is_online' => true), null, null);
If the query is more complex - for example to get over 18 clients you have to do:
select client.*, (YEAR(CURDATE()) - YEAR(client.birthdate)) as age
FROM client
WHERE age >= 18
Then its probably better to make this into a separate method or create methods to work on Query objects directly to add complex conditions for example - especially if you will need this condition in a few different queries in the app:
$table = new MyTable($db);
// creates a basic query defaulted to SELECT * FROM table_name
$query = $table->createQuery();
// adds the complex condition for over 18 resulting in
// SELECT table_name.*, (YEAR(CURDATE()) - YEAR(table_name.birthdate)) as age WHERE age >= 18
$over18 = $table->applyOver18Query($query)->execute();
This way you can apply your over 18 condition easily to any query with out manually manipulating the builder ensure that your over 18 condition is consistent. But for simplicity you could also have a convenience method like the following:
public function findOver18By(array $criteria, $limit = null, $offest = null) {
$query = $this->findBy($criteria, $limit, $offset);
$this->applyOver18Query($query);
return $query->execute();
}
Normally you would use some kind of query builder at the lower level like:
$query = $db->createQuery()
->select($fields)
->from($tableName)
->where($fieldName, $value);
$results = $query->execute();
Then you might have a class that makes use of this like:
class MyTable
{
protected $tableName = 'my_table';
protected $db;
public function __construct($db) {
$this->db = $db;
}
public function findBy(array $criteria, $limit = null, $offset = null) {
$query = $this->db->createQuery();
$query->select('*')->from($this->tableName);
foreach ($criteria as $col => $value) {
// andWhere would determine internally whether or not
// this is the initial WHERE clause or an AND clause
// something similar would happen with an orWhere method
$query->andWhere($col, $value);
}
if (null !== $limit) {
$query->limit($limit);
}
if (null !== $offset) {
$query->offset($offset);
}
return $query->execute();
}
}
Usage would look like:
$table = new MyTable($db);
$result = $table->findBy(array('column1' => 'foo'), null, null);
This is a lot to implement on your own. Most people use an ORM or a DBAL to provide these features and those are often included with a framework like Eloquent with Laravel, or Doctrine with Symfony.
I guess at start you should need some main data like
$main = [
'from' = '`from_table`',
]
Then you should add selects if had
$selects = ['fields1','field2'];
$where = ['some condition', 'other condition'];
Then you could
$query = "SELECT ".implode(',', $selects ." FROM ".$main['from']."
WHERE ".implode('AND ', $where .";";
That's some approaches for simple one table query.
If you need Joins, then $selects better would be make with aliasos, so no field will be lost if they are not different, like
select temp.id as temp_id , temp2.id temp2_id from temp
left join temp2 on temp2.temp_id = temp.id
Feel free to ask some questions, maybe i haven't told , but you should also check bound parameters with some functions to avoid sql injections
I suggest using a CLASS for your database which holds all your database accessing functions as it makes your code cleaner making it more easier to look through for errors or modifications.
class Database
{
public function connect() { }
public function disconnect() { }
public function select() { }
public function insert() { }
public function delete() { }
public function update() { }
}
sample connect function for connecting to a selected database.
private db_host = ‘’;
private db_user = ‘’;
private db_pass = ‘’;
private db_name = ‘’;
public function connect()
{
if(!$this->con)
{
$myconn = mysqli_connect($this->db_host,$this->db_user,$this->db_pass);
if($myconn)
{
$seldb = mysqli_select_db($this->db_name,$myconn);
if($seldb)
{
$this->con = true;
return true;
} else
{
return false;
}
} else
{
return false;
}
} else
{
return true;
}
}
with this approach will make creating CRUD functions easier. Heres a sample insert function.
public function insert($table,$values,$rows = null)
{
if($this->tableExists($table))
{
$insert = 'INSERT INTO '.$table;
if($rows != null)
{
$insert .= ' ('.$rows.')';
}
for($i = 0; $i < count($values); $i++)
{
if(is_string($values[$i]))
$values[$i] = '"'.$values[$i].'"';
}
$values = implode(',',$values);
$insert .= ' VALUES ('.$values.')';
$ins = #mysql_query($insert);
if($ins)
{
return true;
}
else
{
return false;
}
}
}
heres a quick view on using this.
;<?php;
$db->insert('myDataBase',array(3,"Name 4","this#wasinsert.ed")); //this takes 3 paramteres
$result = $db->getResult(); //Assuming you already have getResult() function.
print_r($result);
?>
EDIT
there are more purist approach to handling database operations. I highly suggest it because handling information is very delicate and should be fronted with many safety measures But it requires deeper php knowledge. Try PDO for php and this article by matt bango on prepared statements and its significance.
I'm using Zend Framework and following the design pattern of separating the Data layer from the Domain layer
the problem raises when implementing the methods for the Data mapper
so i implemented the save() which insert & update based on whether domain model contains id property and find() which return the records domain object based on id parameter
but what if i need to
search all/some rows in a table and return all the columns
search the same rows and return a mysql COUNT value
should i just directly use the class the inherited the Zend_Db_Table_Abstract for these needs or
should i implement method for every need ?
i'm a little confused on how to divide the functionality of the Data Mapper that will fit my needs and my future needs
You can add individual finder Methods, e.g.
class PersonMapper
{
… // other code
public function findByLastName()
{
// … fetch rowset and map them
}
public function countByLastName()
{
…
However, that will quickly get out of hand when you need to query multiple columns or want to handle CRUD by arbitrary criteria. You don't want methods like
public function findByLastNameAndBirthdayAndMaritalStatus()
The easy solution would be to use Zend_Db_Table_Select to create the queries and then pass those to the Data Mapper to execute and map them, e.g. in your DataMapper
public function getSelect()
{
return $this->personTable->select();
}
public function findBy(Zend_Db_Table_Select $select)
{
$people = $this->personTable->fetchAll($select);
// map people to People objects
}
You could abstract this further with the Mapper returning and accepting PersonQueryBuilder instead, which hides the SQL Semantics inside and let's you specify against your Domain Objects instead. It's more effort though.
Also have a look at the Repository and Specification Pattern.
As much as Gordon very likely has the correct answer, I find it overly complex for my tastes and needs at the moment.
I use a base mapper class for all of my domain mappers and I put as much functionality into the base class as I can.
I use a find by column method that works fairly well in all of my mappers:
//from abstract class Model_Mapper_Abstract
//The constructor of my base class accepts either a dbtable model
// or the name of a table stored in the concrete mapper tablename property.
public function __construct(Zend_Db_Table_Abstract $tableGateway = null)
{
if (is_null($tableGateway)) {
$this->tableGateway = new Zend_Db_Table($this->tableName);
} else {
$this->tableGateway = $tableGateway;
}
}
/**
* findByColumn() returns an array of entity objects
* filtered by column name and column value.
* Optional orderBy value.
*
* #param string $column
* #param string $value
* #param string $order optional
* #return array of entity objects
*/
public function findByColumn($column, $value, $order = null)
{
//create select object
$select = $this->getGateway()->select();
$select->where("$column = ?", $value);
//handle order option
if (!is_null($order)) {
$select->order($order);
}
//get result set from DB
$result = $this->getGateway()->fetchAll($select);
//turn DB result into domain objects (entity objects)
$entities = array();
foreach ($result as $row) {
//create entity, handled by concrete mapper classes
$entity = $this->createEntity($row);
//assign this entity to identity map for reuse if needed
$this->setMap($row->id, $entity);
$entities[] = $entity;
}
//return an array of entity objects
return $entities;
}
I hope you find this useful as an idea generator at the least. Also if you wish to implement a SQL Count() statement in a method similar to this it will go easier if you use Zend_Db_Expr() when you build that select().
I'm using Doctrine with Symfony in a couple of web app projects.
I've optimised many of the queries in these projects to select just the fields needed from the database. But over time new features have been added and - in a couple of cases - additional fields are used in the code, causing the Doctrine lazy loader to re-query the database and driving the number of queries on some pages from 3 to 100+
So I need to update the original query to include all of the required fields. However, there doesn't seem an easy way for Doctrine to log which field causes the additional query to be issued - so it becomes a painstaking job to sift through the code looking for usage of fields which aren't in the original query.
Is there a way to have Doctrine log when a getter accesses a field that hasn't been hydrated?
I have not had this issue, but just looked at Doctrine_Record class. Have you tried adding some debug output to the _get() method? I think this part is where you should look for a solution:
if (array_key_exists($fieldName, $this->_data)) {
// check if the value is the Doctrine_Null object located in self::$_null)
if ($this->_data[$fieldName] === self::$_null && $load) {
$this->load();
}
Just turn on SQL logging and you can deduce the guilty one from alias names. For how to do it in Doctrine 1.2 see this post.
Basically: create a class which extends Doctrine_EventListener:
class QueryDebuggerListener extends Doctrine_EventListener
{
protected $queries;
public function preStmtExecute(Doctrine_Event $event)
{
$query = $event->getQuery();
$params = $event->getParams();
//the below makes some naive assumptions about the queries being logged
while (sizeof($params) > 0) {
$param = array_shift($params);
if (!is_numeric($param)) {
$param = sprintf("'%s'", $param);
}
$query = substr_replace($query, $param, strpos($query, '?'), 1);
}
$this->queries[] = $query;
}
public function getQueries()
{
return $this->queries;
}
}
And add the event listener:
$c = Doctrine_Manager::connection($conn);
$queryDbg = new QueryDebuggerListener();
$c->addListener($queryDbg);
I'm sing the Data Mapper Pattern in Zend Framework. This works well so far, but now I got to a point where I need your help/opinion. So let's start with the Code:
We got a table with several Persons:
CREATE TABLE `persons` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`age` int(3) NOT NULL,
`haircolor` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id``),
);
Now I try to select all people, that have brown hair. I use the following method in the ServiceLayer
public function getPeopleByHaircolor($hair) {
return $this->getMapper()->fetch('haircolor = ?', $hair);
}
The method in the Mapper looks like this:
public function fetch($condition, $value) {
$resultSet = $this->getTable()->fetchAll($this->getTable()->select()->where($cond, $value));
$entries = array();
foreach($resultSet as $row) {
$entry = new Default_Model_Person();
$entry->id = $row->id;
$entry->name = $row->name;
[...]
}
return $entries;
}
I think I follow the Data Mapper Pattern with this methods...
Now the Problem:
I want to select Persons, that have brown hair AND that are younger than 20 yrs.
So how can I do that? My try:
public function getTeens($hair) {
$rows = $this->getMapper()->fetch('haircolor = ?', $hair);
$return = array();
foreach($rows as $row) {
if((int)$row->age < 20) $return[] = $row;
}
return $return;
}
But if you get more variables, like "people with brown hair, younger than 20yrs and with the name 'Foo Bar'", I need more and more methods and/or foreach loops.
My Question:
How would you do this in the Data Mapper Pattern? If I do a native SQL query like $serviceLayer->mapper->table->qry('SELECT ...'), is this breaking the Data Mapper Pattern?
I don't like those additional foreach loops and it feels like I'm doing something wrong, so I wrote this question.
Indeed, for two conditions, it inadvisable to query on one condition and then filter on your other while you iterate. It leaves you no clear approach for more than two conditions. And implementing pagination adapters becomes pretty messy.
Seems to me that the issue is that your mapper fetch() method supports permits only a single condition. So:
Modify the signature to support an array of conditions.
Alternatively, you could create a separate mapper method for each enhanced fetch method, ex: fetchByHairAndAge($hair, $age), etc.
In my limited experience with data mappers I have found the following approach to work quite well for the scenarios I have encountered so far:
public function getPeopleByHaircolorAndAge($haircolor, $age, $limit=null, $offset=null)
{
$people = new PersonCollection;
$people->filterByHaircolor($haircolor);
$people->filterByAge($age);
$people->setLimit($limit);
$people->setOffset($offset);
$personCollectionMapper = new PersonCollectionMapper;
$personCollectionMapper->fetch($people);
return $people;
}
By instantiating the domain object first I have the option to set filters and other variables that the data mapper can read from the object when it is injected into the mapper.
To me this has been the superior approach so far compared to having multiple mapper methods that returns a domain object.