I can't find a way to set the default hydrator in Doctrine. It should be available. Right?
http://docs.doctrine-project.org/projects/doctrine1/en/latest/en/manual/data-hydrators.html#writing-hydration-method
The above documentation page explains how to create a custom hydrator. The drawback here is that you need to "specify" the hydrator each and every time you execute a query.
I figured this out by reading Chris Gutierrez's comment and changing some stuff.
First, define an extension class for Doctrine_Query. Extend the constructor to define your own hydration mode.
class App_Doctrine_Query extends Doctrine_Query
{
public function __construct(Doctrine_Connection $connection = null,
Doctrine_Hydrator_Abstract $hydrator = null)
{
parent::__construct($connection, $hydrator);
if ($hydrator === null) {
$this->setHydrationMode(Doctrine::HYDRATE_ARRAY); // I use this one the most
}
}
}
Then, in your bootstrap, tell Doctrine about your new class.
Doctrine_Manager::getInstance()->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, 'App_Doctrine_Query');
Chris Gutierrez defined the attribute for the connection instead of globally but I have more than one connection and I want to use this default for all of them.
Now you don't have to call Doctrine_Query::setHydrationMode() every time you build a query.
Here's more information
http://www.doctrine-project.org/projects/orm/1.2/docs/manual/configuration/en#configure-query-class
EDIT: Changes below
I have found a problem with the above. Specifically, doing something like "Doctrine_Core::getTable('Model')->find(1)" will always return a hydrated array, not an object. So I have altered this a bit, defining custom execute methods for use in a Query call.
Also, I added memory freeing code.
class App_Doctrine_Query extends Doctrine_Query
{
public function rows($params = array(), $hydrationMode = null)
{
if ($hydrationMode === null)
$hydrationMode = Doctrine_Core::HYDRATE_ARRAY;
$results = parent::execute($params, $hydrationMode);
$this->free(true);
return $results;
}
public function row($params = array(), $hydrationMode = null)
{
if ($hydrationMode === null)
$hydrationMode = Doctrine_Core::HYDRATE_ARRAY;
$results = parent::fetchOne($params, $hydrationMode);
$this->free(true);
return $results;
}
}
That'd be a great idea, and on reading your question I thought it'd be something you could do via Doctrine. However, reading through the code makes me think you can't:
Doctrine_Query::create() creates a new query specifying only the first argument of Doctrine_Query_Abstract::__construct(), the connection, without specifying the second argument - the hydration mode. No calls to configuration are made. As no hydrator is passed, a new Doctrine_Hydrator is created, and its constructor equally does not look anywhere for a configuration option, and thus it has the default Doctrine::HYDRATE_RECORD setting.
Perhaps subclassing Doctrine_Query with the below factory method is the easiest option?
public static function create($conn = null)
{
return new Doctrine_Query($conn,Doctrine::HYDRATE_ARRAY);
}
Related
I use Redbeanphp ( http://redbeanphp.com/ ) in my php project. And i want to use a table prefix for my tables.
Redbeanphp can't support table prefix since the version 3.0. But i want to extend Redbeanphp to support table prefix in my project.
I don't want to modify the redbeanphp code. But if there's no solution, i'll do that.
I have already tried to replace the QueryWriter of Redbeanphp but the QueryWriter class is not always the same (it depends of the type of my database).
What is the best way to do that ?
I now got the response so i answer to myself.
Once redbean is initialized, you can configure a new toolbox. The toolbox in redbean handle 3 important objects : The query writer, the Redbean OODB and the database adapter. You can access the current redbean toolbox with R::$toolbox
You can do this code :
R::configureFacadeWithToolbox(new RedBean_ToolBox(R::$redbean, R::$adapter, R::$writer));
This code does nothing. Because you configure Redbean with a new toolbox but with the same OODB, the same database adapter and the same query writer. But in this code, you can replace one of these object by your own object.
Example, replacing the writer by a dummy writer :
$writer = new MyQueryWriter();
R::configureFacadeWithToolbox(new RedBean_ToolBox(R::$redbean, R::$adapter, $writer));
The probem is the following :
You want to replace the query writer by your own query writer to handle a table prefix
The query writer class is not always the same. Redbean use 5 classes for the query writer. The class depends of the database type. For instance, if you use a Mysql database, the query writer class is RedBean_QueryWriter_MySQL
You don't want to write an entire query writer.
Redbean query writer possible classes are :
RedBean_QueryWriter_CUBRID
RedBean_QueryWriter_MySQL
RedBean_QueryWriter_Oracle
RedBean_QueryWriter_PostgreSQL
RedBean_QueryWriter_SQLiteT
So, this is my solution. I wrote 5 littles classes.
class MyCubridQueryWriter extends RedBean_QueryWriter_CUBRID {
public function safeTable($name, $noQuotes = false) {
$name = prefix($name);
return parent::safeTable($name, $noQuotes);
}
}
class MyMysqlQueryWriter extends RedBean_QueryWriter_MySQL {
public function safeTable($name, $noQuotes = false) {
$name = prefix($name)
return parent::safeTable($name, $noQuotes);
}
}
class MyOracleQueryWriter extends RedBean_QueryWriter_Oracle {
public function safeTable($name, $noQuotes = false) {
$name = prefix($name)
return parent::safeTable($name, $noQuotes);
}
}
class MyPostgreSqlQueryWriter extends RedBean_QueryWriter_PostgreSQL {
public function safeTable($name, $noQuotes = false) {
$name = prefix($name)
return parent::safeTable($name, $noQuotes);
}
}
class MySQLiteTQueryWriter extends RedBean_QueryWriter_SQLiteT {
public function safeTable($name, $noQuotes = false) {
$name = prefix($name)
return parent::safeTable($name, $noQuotes);
}
}
As you can see, each class extend a Redbean query writer class. We override the safeTable method. Redbean always use safeTable on a table name. The prefix function is simple :
function prefix($table) {
return "my_prefix_$table";
}
So now, in our code. We can use an array to map a Redbean query writer class to our own classes and replace it. Here we are :
$writerMapping = array(
'RedBean_QueryWriter_CUBRID' => 'MyCubridQueryWriter',
'RedBean_QueryWriter_MySQL' => 'MyMysqlQueryWriter',
'RedBean_QueryWriter_Oracle' => 'MyOracleQueryWriter',
'RedBean_QueryWriter_PostgreSQL' => 'MyPostgreSqlQueryWriter',
'RedBean_QueryWriter_SQLiteT' => 'MySQLiteTQueryWriter'
);
$class = $writerMapping[get_class(R::$writer)];
$writer = new $class(R::$adapter);
R::configureFacadeWithToolbox(new RedBean_ToolBox(R::$redbean, R::$adapter, $writer));
Et voila. Now Redbean will use your own writer and you can do what you want ! With our safeTable method, we add a prefix to every table name in the database.
I ran into this problem when wanting to use RedBean with Wordpress. My solution was to create another class (WPR for "wordpress redbean"), like so:
class WPR {
public static function __callStatic($method, $params)
{
global $wpdb;
$prefix = $wpdb->base_prefix;
foreach ($params as &$param)
$param = preg_replace('/\{([a-zA-Z0-9_]+)\}/', $prefix . '$1', $param);
// switch to wordpress database
R::selectDatabase('WPR');
// do the dang thing
$res = call_user_func_array(array('R',$method),$params);
// go back
R::selectDatabase('default');
// send it
return $res;
}
};
R::addDatabase('WPR', "mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASSWORD);
I also wanted this class to use a different database than my 'regular' redbean class, so I have the selectDatabase() calls in there. Comment them out if you don't need them.
What it does is it acts as a proxy to redbean, but with each input it checks for some substring like {this} and it expands it out into the full database name, with prefix. Here's an example of your usage:
$my_blog = WPR::find('{blogs}', 'domain=?', array('mydomain.com')); or
$allowed_hosts = WPR::getCol('SELECT domain FROM {blogs}');
In those two cases, {blogs} gets converted to wp_blogs
Magus,
I have the same problem as you. I tried your solution but could not get it working. I wrote a couple of functions for prefixing and my object names into table names and back, which I think will work in my case, but I'd still like to get your way working since it'll be more transparent. I have unprefixed table names working for reading and writing.
I noticed was that Oracle support isn't available out-of-the-box in RedBean, so I added checks for each classname to avoid errors:
if (class_exists('RedBean_QueryWriter_MySQL', false)) {
class MyMysqlQueryWriter extends RedBean_QueryWriter_MySQL {
...
}
The checks should work, I got output to my log within my MySQL (which I'm using) block while loading the prefixing code.
Also, at the end there you wrote:
$class = $writerMapping(get_class(R::$writer));
but you probably meant:
$class = $writerMapping[get_class(R::$writer)];
Based on some debugging, my R::$writer has been changed after configureFacadeWithToolbox, but, for some reason the table names aren't being converted, and nothing within my custom safeTable function is being executed.
If you could give any more info on how you tested your method or what I could be missing, I'd be glad to hear it.
(I'm sorry this message isn't an answer to your question, but I really couldn't find any other way to send you a message or comment on your answer. Damn Stack Overflow! (Just kidding, I love it.))
i'm new to zend framework, in this simple function i want to get a single 'post' and then i want to find all the comments in the related table
public function getPost($idPost)
{
$db= Zend_Registry::get('db');
$select=$db->select()
->from($this->_name, '*')
->where("idPost= ".$db->quote($idPost, 'INTEGER'));
$stmt=$select->query();
$rowset=$stmt->fetchAll();
$post=$rowset->current();
//ora devo aggiungerci i commenti che questo post ha ricevuto
$comm=$post->findDependentRowset('commenti');
$ris=array($post, $comm);
return $ris;
}
in my index controller i i simply call this function, but i get this error:
Call to a member function current() on a non-object in C:\xampp\htdocs\...
where's the mistake?
I think you have a few misconceptions about how you're using Zend_Db.
1. You're not using the ORM, just the PDO wrapper
Which means, your queries won't return Zend rowsets and rows and therefore you can't use the methods of you can use on those.
2. The default fetch mode
The default fetch mode of the Zend_Db_Statement fetchAll() method is array, if you want it to return an object (stdClass), change the fetch mode before fetching the data:
$stmt->setFetchMode(Zend_Db::FETCH_OBJ);
3. Using fetchAll() when you actually want one row
If you just want one row, then don't fetch a whole table! With Zend_Db_Statement, use for example:
$row = $stmt->fetch();
or
$rowObj = $stmt->fetchObject();
... again, that's not a zend row object, just a stdClass instance, but you can do:
$rowObj->some_field;
on it.
On the other hand, if this is a method in your Post model, it should look something like:
public function getPost($idPost)
{
return $this->getRow($idPost);
}
This will return the post, then, if you've setup the table relationships correctly, you can also query for the dependent data or just get all comments with that id separately.
The problem is that unless you define a table class as was previously mentioned you can't uuse the dependent or parent rowsets.
To make your current function work would be best done with two functions, and keep it simple:
public function getPost($idPost)
{
$db= new Zend_Db_Table($this->_name);
$select=$db->select()
->where("idPost= ?", $idPost);
/*Fetch just the row you want, or use fetchAll() if you need to match return types*/
$row = $db->fetchRow($select);
return $row;
}
public function getComments($table='comments', $id) {
$db = new Zend_Db_table($table);
$select = $db->select()->where('post_id = ?', $id)->order('date ASC');
$rowset = $db->fetchAll($select);
return $rowset/* or you could return an array ->$rowset->toArray() */
}
Zend_Db_Table is going to attempt to use the current database adapter, so all you need to do is pass in the tablename.
One more note: you don't need to use any of the quote() function when using select() it's taken care of.
But it is really important, that if you are going to use Zend_Db, you need to learn about "Defining table classes". At least enough to use them in your own classes.
I hope this helps!
To get a rowset and dependent rowset you have to use Zend_Db_Table.
You only use the Zend_Db_Adapter with Zend_Db_Select.
Read from here.
So you have to define a class which extends from Zend_Db_Table_Abstract.
Example:
class Bugs extends Zend_Db_Table_Abstract
{
protected $_name = 'bugs';
protected $_primary = 'bug_id';
}
To get the Zend_Db_Table_Rowset object use:
$bugs = new Bugs();
$rowset = $bugs->fetchAll("bug_status = 'NEW'");
To find dependent rowsets you have to define the relation in your table class. Look here how to define relationships.
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've an ORM model (PHP Active Record), say, for a blogging system. I've something that's a post model that stores the number of likes. The post could either be a picture or quote (say), and they are different tables (and hence models).
The schema is that a post holds data like number of shares, likes, description, etc. along with either a picture or a quote.
So when writing getters for the post model I'm having to write
public function getX() {
if ($this->isPicture()) {
return $this->picture->getX();
}
else if ($this->isQuote()) {
return $this->quote->getX()
}
else {
return self::DEFAULT_X
}
}
I'm currently having to write this structure for many getter. Is there something I can do to avoid that?
PS: Tagged as PHP because that's my code in.
EDIT
Changed comments to code.
This is a model (and a corresponding table in the DB) that has more data than just a picture and quote. Example, description that's part of the post and doesn't reside on either the picture or the quote.
There's tables for pictures and quotes.
Using PHP Active Record and each of the three classes extends the generic model class provided by PHP Active Record.
The picture model has it's own data. Same for quote.
To expand on the idea of the Strategy pattern mentioned in the comments:
class Post {
// get the correct 'strategy'
public function getModel() {
if ($this->isPicture()) {
return $this->picture;
}
if ($this->isQuote()) {
return $this->quote;
}
return null;
}
// using the strategy
public function getX() {
$model = $this->getModel();
if (null === $model) {
return self::DEFAULT_X;
}
return $model->getX();
}
}
Each strategy would presumably implement the same interface as the Post class for exposing those getters. Even better would be to provide a default strategy (rather than returning null) and have that return the default values. That way, the null check in each getter becomes redundant.
An alternative approach to this is a very basic form of metaprogramming. The idea is that you go a level higher than calling your methods by hand, and let the code do it for you.
(Assume that the method definitions are all part of Post)
public function getX($model = null) {
if ($model) return $model->getX();
else return self::DEFAULT_X;
}
// usage
$postModel->getX($pictureModel);
What's happening here is that, in this single instance of getX in your Post model, you're passing in the name of another class, and executing the `getX' method on that instance (if it exists and is callable).
You can extend this in other ways. For example, maybe you don't want to pass an instance in, when the method can do it anyway:
public function getX($model_name = null) {
if ($model_name && $class_exists($model_name) && is_callable(array($model_name, 'getX')) {
$model = new $model_name;
return $model->getX();
} else {
return self::DEFAULT_X;
}
}
// usage
$postModel->getX('Picture');
In this instance, you pass the model in as a string, and the method will do the rest. While this makes it quicker to get what you want, you might find that you don't want to work with fresh instances all the time (or you can't), so there's a bit of a trade-off with this 'convenient' way.
That still doesn't fully solve your problem, though, since you still have to repeat that for each getter, over and over again. Instead, you can try something like this:
public function __call($method, $args) {
$class = $args[0];
if (class_exists($class) && is_callable(array($class, $method))) {
$model = new $class;
return $model->$method();
}
}
// usage
$postModel->getX('Picture');
$postModel->getY('Quote');
$postModel->getZ('Picture');
If you call a function that doesn't exist on the Post model, that magic method will be called, and it'll fire up a new instance of the model name you supply as an argument, and call the getWhatever method on it, if it exists.
It's important to note that you must not define these getters in Post, unless you want to override the methods in the other classes.
There is still the problem of this creating new instances all the time, though, and to remedy this you can use a bit of dependency injection. This means that you let the Post class contains a list of other instances of classes that it wants to use in future, so you can add and remove them at will.
This is what I would consider the actual solution, with the other examples hopefully showing how I've got here (will edit to clarify things, of course).
public $models = array();
public function addModel($instance) {
$this->models[get_class($instance)] = $instance;
}
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
}
}
// usage
$this->addModel($pictureModel);
$this->addModel($quoteModel);
$this->getX('Picture');
$this->getY('Quote');
Here, you're passing in your existing instances of models into the Post class, which then stores them in an array, keyed by the name of the class. Then, when you use the class as described in the last example, instead of creating a new instance, it will use the instance it has already stored. The benefit of this is that you might do things to your instances that you'd want reflected in the Post model.
This means that you can add as many new models as you like that need to plug into Post, and the only thing you need to do is inject them with addModel, and implement the getters on those models.
They all require you to tell the class what models to call at some point or another. Since you have an array of dependent models, why not add a way to get everything?
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
} elseif ($class === 'all') {
// return an array containing the results of each method call on each model
return array_map(function($model) use ($method) {
if (is_callable(array($model, $method) return $model->$method();
}, $this->models);
}
}
// usage
$postModel->getX('all');
Using this, you'll get an array containing the return values of each getX method on each model you added with addModel. You can create pretty powerful functions and classes that do all this stuff without you having to repeat tedious logic.
I have to mention that these examples are untested, but at the very least I hope the concept of what you can do has been made clear.
Note:
The same thing can be applied to __GET and __SET methods, too, which are used for accessing properties. It's also worth saying that there may be the slight risk of a library already using these magic methods, in which case you'll need to make the code a little more intelligent.
I have recently started reading about dependency injection and it has made me rethink some of my designs.
The problem i have is something like this:
Let's say i have two classes: Car and Passenger;
For those two classes i have some data mappers to work with the database: CarDataMapper and PassengerDataMapper
I want to be able to do something like this in code:
$car = CarDataMapper->getCarById(23); // returns the car object
foreach($car->getPassengers() as $passenger){ // returns all passengers of that car
$passenger->doSomething();
}
Before I knew anything about DI, I would build my classes like this:
class Car {
private $_id;
private $_passengers = null;
public function getPassengers(){
if($this->_passengers === null){
$passengerDataMapper = new PassengerDataMapper;
$passengers = $passengerDataMapper->getPassengersByCarId($this->getId());
$this->setPassengers($passengers);
}
return $this->_passengers;
}
}
I would also have similar code in the Passenger->getCar() method to fetch the car the passenger is in.
I now understand that this creates dependencies (well, I understood it before too, but I wasn't aware that this is "wrong") between the Car and the Passenger objects and the data mapper objects.
While trying to think of the solution for this two options came to mind, but I don't really like any of them:
1: Doing something like this:
$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);
foreach($car->getPassengers() as $passenger){
$passenger->doSomething();
}
But what if passengers have objects that they need injected, and what if the nesting goes to ten or twenty levels... I would wind up instantiating nearly every object in the start of my application, which would in turn query the entire database during the process.
If i have to send the passenger to another object which has to do something with the objects that the passenger holds, I do not want to immediately instantiate these objects too.
2: Injecting the data mappers into the car and passenger objects and having something like this:
class Car {
private $_id;
private $_passengers = null;
private $_dataMapper = null;
public function __construct($dataMapper){
$this->setDataMapper($dataMapper);
}
public function getPassengers(){
if($this->_passengers === null && $this->_dataMapper instanceof PassengerDataMapper){
$passengers = $this->_dataMapper->getPassengersByCarId($this->getId());
$this->setPassengers($passengers);
}
return $this->_passengers;
}
}
I dont like this any better, because it's not like the Car is really unaware of the data mapper, and without the data mapper, the Car could behave unpredictably (not returning passengers, when it actually has them)
So my first question is:
Am I taking a completely wrong approach here, because, the more I look at it, the more it looks like I'm building an ORM, instead of a business layer?
The second question is:
is there a way of actually decoupling the objects and the data mappers in a way that would allow me to use the objects as described in the very first code block?
Third question:
I've seen some answers for other languages (some version of C, I think) resolving this issue with something like this described here:
What is the proper way to inject a data access dependency for lazy loading?
As I haven't had time to play with other languages, this makes no sense to me, so I'd be grateful if someone would explain the examples in the link in PHP-ish.
I have also looked at some DI frameworks, and read about DI Containers and Inversion of Control, but from what I understood they are used to define and inject dependencies for 'non dynamic' classes, where for instance, the Car would depend on the Engine, but it would not need the engine to be loaded dynamically from the db, it would simply be instantiated and injected into the Car.
Sorry for the lengthy post and thanks in advance.
Maybe off-topic, but I think that it will help you a bit:
I think that you try to achieve the perfect solution. But no matter what you come up with, in a couple of years, you will be more experienced and you'll definitely be able to improve your design.
Over the past years with my colleagues we had developed many ORMs / Business Models, but for almost every new project we were starting from scratch, since everyone was more experienced, everyone had learned from the previous mistakes and everyone had come across with new patterns and ideas. All that added an extra month or so in development, which increased the cost of the final product.
No matter how good the tools are, the key problem is that the final product must be as good as possible, at the minimum cost. The client won't care and won't pay for things that can't see or understand.
Unless, of course, you code for research or for fun.
TL;DR: Your future self will always outsmart your current self, so do not overthink about it. Just pick carefully a working solution, master it and stick with it until it won't solve your problems :D
To answer your questions:
Your code is perfectly fine, but the more you will try to make it "clever" or "abstract" or "dependency-free", the more you will lean towards an ORM.
What you want in the first code block is pretty feasible. Take a look at how the Doctrine ORM works, or this very simple ORM approach I did a few months ago for a weekend project:
https://github.com/aletzo/dweet/blob/master/app/models
I was going to say "I know this is an old question but..." then I realized you posted it 9 hours ago, which is cool, because I just came to a satisfactory 'resolution' for myself. I thought of the implementation and then I realized it is what people were calling 'dependency injection'.
Here is an example:
class Ticket {
private $__replies;
private $__replyFetcher;
private $__replyCallback;
private $__replyArgs;
public function setReplyFetcher(&$instance, $callback, array $args) {
if (!is_object($instance))
throw new Exception ('blah');
if (!is_string($callback))
throw new Exception ('blah');
if (!is_array($args) || empty($args))
throw new Exception ('blah');
$this->__replyFetcher = $instance;
$this->__replyCallback = $callback;
$this->__replyArgs = $args;
return $this;
}
public function getReplies () {
if (!is_object($this->__replyFetcher)) throw new Exception ('Fetcher not set');
return call_user_func_array(array($this->__replyFetcher,$this->__replyCallback),$this->__replyArgs);
}
}
Then, in your service layer (where you 'coordinate' actions between multiple mappers and models) you can call the 'setReplyFetcher' method on all of the ticket objects before you return them to whatever is invoking the service layer -- OR -- you could do something very similar with each mapper, by giving the mapper a private 'fetcherInstance' and 'callback' property for each mapper the object is going to need, and then set THAT up in the service layer, then the mapper will take care of preparing the objects. I am still weighing the differences between the two approaches.
Example of coordinating in the service layer:
class Some_Service_Class {
private $__mapper;
private $__otherMapper;
public function __construct() {
$this->__mapper = new Some_Mapper();
$this->__otherMapper = new Some_Other_Mapper();
}
public function getObjects() {
$objects = $this->__mapper->fetchObjects();
foreach ($objects as &$object) {
$object->setDependentObjectFetcher($this->__otherMapper,'fetchDependents',array($object->getId()));
}
return $objects;
}
}
Either way you go, the object classes are independent of mapper classes, and mapper classes are independent of each other.
EDIT: Here is an example of the other way to do it:
class Some_Service {
private $__mapper;
private $__otherMapper;
public function __construct(){
$this->__mapper = new Some_Mapper();
$this->__otherMapper = new Some_Other_Mapper();
$this->__mapper->setDependentFetcher($this->__otherMapper,'someCallback');
}
public function fetchObjects () {
return $this->__mapper->fetchObjects();
}
}
class Some_Mapper {
private $__dependentMapper;
private $__dependentCallback;
public function __construct ( $mapper, $callback ) {
if (!is_object($mapper) || !is_string($callback)) throw new Exception ('message');
$this->__dependentMapper = $mapper;
$this->__dependentCallback = $callback;
return $this;
}
public function fetchObjects() {
//Some database logic here, returns $results
$args[0] = &$this->__dependentMapper;
$args[1] = &$this->__dependentCallback;
foreach ($results as $result) {
// Do your mapping logic here, assigning values to properties of $object
$args[2] = $object->getId();
$objects[] = call_user_func_array(array($object,'setDependentFetcher'),$args)
}
}
}
As you can see, the mapper requires the other resources to be available to even be instantiated. As you can also see, with this method you are kind of limited to calling mapper functions with object ids as parameters. I'm sure with some sitting down and thinking there is an elegant solution to incorporate other parameters, say fetching 'open' tickets versus 'closed' tickets belonging to a department object.
Here is another approach I thought of. You can create a 'DAOInjection' object that acts as a container for the specific DAO, callback, and args needed to return the desired objects. The classes then only need to know about this DAOInjection class, so they are still decoupled from all of your DAOs/mappers/services/etc.
class DAOInjection {
private $_DAO;
private $_callback;
private $_args;
public function __construct($DAO, $callback, array $args){
if (!is_object($DAO)) throw new Exception;
if (!is_string($callback)) throw new Exception;
$this->_DAO = $DAO;
$this->_callback = $callback;
$this->_args = $args;
}
public function execute( $objectInstance ) {
if (!is_object($objectInstance)) throw new Exception;
$args = $this->_prepareArgs($objectInstance);
return call_user_func_array(array($this->_DAO,$this->_callback),$args);
}
private function _prepareArgs($instance) {
$args = $this->_args;
for($i=0; $i < count($args); $i++){
if ($args[$i] instanceof InjectionHelper) {
$helper = $args[$i];
$args[$i] = $helper->prepareArg($instance);
}
}
return $args;
}
}
You can also pass an 'InjectionHelper' as an argument. The InjectionHelper acts as another callback container -- this way, if you need to pass any information about the lazy-loading object to its injected DAO, you won't have to hard-code it into the object. Plus, if you need to 'pipe' methods together -- say you need to pass $this->getDepartment()->getManager()->getId() to the injected DAO for whatever reason -- you can. Simply pass it like getDepartment|getManager|getId to the InjectionHelper's constructor.
class InjectionHelper {
private $_callback;
public function __construct( $callback ) {
if (!is_string($callback)) throw new Exception;
$this->_callback = $callback;
}
public function prepareArg( $instance ) {
if (!is_object($instance)) throw new Exception;
$callback = explode("|",$this->_callback);
$firstCallback = $callback[0];
$result = $instance->$firstCallback();
array_shift($callback);
if (!empty($callback) && is_object($result)) {
for ($i=0; $i<count($callback); $i++) {
$result = $result->$callback[$i];
if (!is_object($result)) break;
}
}
return $result;
}
}
To implement this functionality in the object, you would require the injections at construction to ensure that the object has or can get all of the information it needs. Each method that uses an injection simply calls the execute() method of the respective DAOInjection.
class Some_Object {
private $_childInjection;
private $_parentInjection;
public function __construct(DAOInjection $childInj, DAOInjection $parInj) {
$this->_childInjection = $childInj;
$this->_parentInjection = $parInj;
}
public function getChildObjects() {
if ($this->_children == null)
$this->_children = $this->_childInjection->execute($this);
return $this->_children;
}
public function getParentObjects() {
if ($this->_parent == null)
$this->_parent = $this->_parentInjection->execute($this);
return $this->_parent;
}
}
I would then, in the constructor of my service class, instantiate the mappers relevant to that service using the relevant DAOInjection classes as arguments for the mappers' constructors. The mappers would then take care of making sure each object has its injections, because the mapper's job is to return complete objects and handle the saving/deleting of objects, while the service's job is to coordinate the relationships between various mappers, objects, and so on.
Ultimately you can use it to inject callbacks to services OR mappers, so say you want your 'Ticket' object to retrieve a parent user, which happens to be outside the realm of the 'Ticket Service' -- the ticket service can just inject a callback to the 'User Service', and it won't have to know a thing about how the DAL works for other objects.
Hope this helps!