Note: This post refers to Yii Framework version 1.1.11 and Zend Framework version 2.0.0
I’ve been working with the Yii Framework for about six months now. While it’s decently designed there are a few points that bother me about it. My favourite PHP framework that I’ve worked with is Zend Framework so this post is a little biased towards it. I find Zend Framework extremely well-designed and thought out. It’s not for beginners and there is a bit of a steep learning curve. I wouldn’t consider it a rapid development framework but it helps keep code very clean when dealing with larger systems.
One of things I find missing the most from Zend when working in Yii is the ResultSet object (previously the Rowset class in Zend v1.x). Yii has the ActiveRecord class for holding individual records that are returned from the database but if a query returns multiple rows, the objects are just stored in an array. This makes things difficult if I want to be able to manipulate the results or convert them to a specific JSON object.
To remove this annoyance, I did the best thing I could think of - I altered the ActiveRecord class to use ResultSet’s for multiple records that are returned.
To do this, you first need to add the Zend Framework library code to the application. Yii recommends placing 3rd-party library code under the /protected/vendors
directory so add the Zend/
directory there.
You will need to make sure this is added to the application’s path. Your setup may vary, but for the app I’m working on, we have a configuration file and I added Zend
to the aliases key like the following:
...
'aliases' => array(
// ...
'Zend' => realpath(__DIR__.'/../vendors/Zend/'),
// ...
),
This example assumes your configuration file is in the /protected/config/
directory.
After this, Zend Framework is ready to use within Yii.
To get Yii to convert arrays of records into a ResultSet, you will need to override some methods in the CActiveRecord
class. In our application, we have a ActiveRecord
class that inherits from the CActiveRecord
to provide custom functionality needed for our app.
Unfortunately, Yii does not provide a single method that handles returning a rowset. There are three methods that need to be overwritten:
- CActiveRecord::getRelated()
- CActiveRecord::findAllBySql()
- CActiveRecord::query()
- CActiveRecord::__get()
The getRelated()
handles the logic for getting related records. Any data resulting from relationships such as one-to-many, many-to-many, etc. are processed through this method before being returned.
The query()
method is called as the final step by most of the findX() methods - find(), findAll(), findByPk() and findByAttributes().
The findAllBySql()
method needs to be overwritten separately because it (along with findBySql()
) does not use the query()
method.
__get()
is a magic method that needs to be overwritten for instances where the Result Set data is saved as an array in the object to avoid hitting the database on subsequent calls in the same request.
Even though we are overwriting the query()
method which also handles single ActiveRecord objects, we will add logic to prevent altering them.
Since the code we want to use will be the same for all methods, we can extract it into a private method to avoid code duplication. Therefore, all the methods that we are overwriting simply need to call the parent method and then call our private method which I have named getResult()
.
public function getRelated($name,$refresh=false,$params=array())
{
$related = parent::getRelated($name, $refresh, $params);
return $this->getResult($related);
}
public function findAllBySql($sql,$params=array())
{
$resultSet = parent::findAllBySql($sql, $params);
return $this->getResult($resultSet);
}
protected function query($criteria,$all=false)
{
$query = parent::query($criteria, $all);
return $this->getResult($query);
}
public function __get($name)
{
$hasRelated = $this->hasRelated($name);
$result = parent::__get($name);
if ($hasRelated) {
return $this->getResult($result);
}
return $result;
}
Notice that the method signatures have all stayed exactly the same as the parent signatures. If you are overriding a method, it’s important to keep the signatures the same, especially a framework method, as you will most likely get errors when they are called incorrectly.
Finally, we can write our getResult()
method.
/**
* Converts the result into a ResultSet if it contains multiple records
* Leaves an ActiveRecord untouched
* @param array|ActiveRecord $dataSource
* @return \Zend\Db\ResultSet\ResultSet|ActiveRecord
*/
private function getResult($dataSource)
{
if (is_array($dataSource)) { // assuming a ResultSet
$resultSet = new \Zend\Db\ResultSet\ResultSet();
$resultSet->initialize($dataSource);
return $resultSet;
}
return $dataSource;
}
As you can see, this method will only return a ResultSet
if $dataSource
is an array. If it is something else, for example a User ActiveRecord object, it will be returned as is.
In my actual code, I have extended the \Zend\Db\ResultSet\ResultSet
class with my own app-specific class to provide further logic that is specific to my application. But this can be an exercise for the reader.
In summary, although Yii does not offer it’s own class for handling result sets, it’s easy to either write your own or use a 3rd-party library to do so as we did in this case.