CakePHP's Model::afterFind() callback looks like:
afterFind(array $results, boolean $primary = false)
According to the documentation:
The $primary parameter indicates whether or not the current model was the model that the query originated on or whether or not this model was queried as an association. If a model is queried as an association the format of $results can differ.
They can differ, but experimentation shows that they don't always differ. As far as I can tell, the $primary parameter isn't actually all that useful. If it's set to false you may or may not get a flattened data structure, so you may or may not wind up with the dreaded "cannot use string offset as an array" error message.
Although I haven't tried it yet, my thought based on the documentation was to ignore the $primary flag altogether and just check the data:
public function afterFind($results, $primary = false) {
if (array_key_exists(0, $results) {
// operate on $results[0]['User']['fieldname']
} else {
// operate on $results['fieldname']
}
return $results;
}
This is hackish and I don't like it, but it seems likely to be more useful than $primary.
Explicitly stated, my questions are:
What is the $primary flag actually useful for?
Am I correct that it is not useful for determining the structure of the $results array, or have I missed something there?
Indeed the $primary parameter seems to only be useful in warning you of cases where the format of $results is unpredictable. It is not useful in determining the format of $results.
More information here: https://groups.google.com/forum/?fromgroups=#!topic/cake-php/Mqufi67UoFo
The solution offered there is to check !isset($results[$this->primaryKey]) to see what format $results is. This is also a bit of a hack, but arguably better than checking for a key '0'.
The solution I ultimately came up with is to do something like this:
public function afterFind($results, $useless) {
// check for the primaryKey field
if(!isset($results[$this->primaryKey])) {
// standard format, use the array directly
$resultsArray =& $results;
} else {
// stupid format, create a dummy array
$resultsArray = array(array());
// and push a reference to the single value into our array
$resultsArray[0][$this->alias] =& $results;
}
// iterate through $resultsArray
foreach($resultsArray as &$result) {
// operate on $result[$this->alias]['fieldname']
// one piece of code for both cases. yay!
}
// return $results in whichever format it came in
// as but with the values modified by reference
return parent::afterFind($results, $useless);
}
This reduces code duplication because you don't have to write your field alteration logic twice (once for an array and once for non-array).
You may be able to avoid the references stuff altogether by just returning $resultsArray at the end of the method, but I wasn't sure what issues that might cause if CakePHP (or some other parent class) expects $results in the way it was passed in. Plus this way doesn't have the overhead of copying the $results array.
If you can't always rely on primaryKey being in the fields list AND you know the key you are looking for, you can get away with something a bit more simple. Here is an example:
/**
* Decrypt password
*
* #see Model::afterFind()
*/
public function afterFind($results, $primary = false) {
if (!empty($results['password'])) {
$results['password'] = Security::rijndael($results['password'], Configure::read('encrypt.key'), 'decrypt');
return $results;
}
foreach ($results as &$r) {
if (!empty($r[$this->alias]['password'])) {
$r[$this->alias]['password'] = Security::rijndael($r[$this->alias]['password'], Configure::read('encrypt.key'), 'decrypt');
}
}
return $results;
}
I ran into this issue. The accepted answer works good. However, I had to make a minor adjustment. If you're looking to modify a field for example construct a fully qualified file name from logo, it's better to create a new field, as "return parent::afterFind($results, $useless);" will do it twice if the model find is called from some other model.
foreach($resultsArray as &$result) {
// operate on $result[$this->alias]['fieldname']
// one piece of code for both cases. yay!
// Added logoFull instead of modifying logo
if(isset($result[$this->alias]['logo'])){
$result[$this->alias]['logoFull'] = Configure::read('urlImg') . 'logos' . DIRECTORY_SEPARATOR .
'carrier' . DIRECTORY_SEPARATOR . $result[$this->alias]['logo'];
}
}
Answers in the book...
The $primary parameter indicates whether or not the current model was
the model that the query originated on or whether or not this model
was queried as an association. If a model is queried as an association
the format of $results can differ;
Code expecting $primary to be true will probably get a "Cannot use
string offset as an array" fatal error from PHP if a recursive find is
used.
Thus it could be useful in certain situations for logic processing and may could be used to have a knock on effect into your $results
Related
Is there a way to cache virtual fields? I mean automatically, with the entity to which they belong, because I understand that, even if an entity is retrieved from the cache, virtual fields are generated whenever it is necessary.
Obviously I know I can take care of it personally, so (example):
protected function _getFullName()
{
$fullName = Cache::read('full_name_for_' . $this->_properties['id'], 'users');
if (empty($fullName)) {
$fullName = $this->_properties['first_name'] . ' ' . $this->_properties['last_name'];
Cache::write('full_name_for_' . $this->_properties['id'], $fullName, 'users');
}
return $fullName;
}
But I wanted to know if in fact CakePHP can do it directly.
EDIT
Context.
The Post entity has the text property. text can contain images (as html code), even remote. Now I have to store somewhere the url of the first image contained in the text and its size. So I have created the first_image virtual field, that uses a regex. The problem is rather with the image size: I can not do run every time the getimagesize() function, especially if the image is remote, for reasons that you can easily understand. So how to do?
Is there a way to cache virtual fields?
No.
And what you do doesn't make much sense. The caching is for sure causing more overhead than that you gain anything from it in this case.
Use concat() on the DB level to concatenate the name instead.
Also if there would be a real need to cache a virtual property, then I would say there went something clearly wrong in the architecture.
It makes sense to me to want to prevent code in accessors/virtual fields from being executed more than once on the same request, which can easily happen if you use them several places in your script.
You can do a solution like this, but I'm not entirely sure how kosher it is:
private $fullName_cache = false;
protected function _getFullName()
{
if(!$this->fullName_cache){
$fullName = Cache::read('full_name_for_' . $this->_properties['id'], 'users');
if (empty($fullName)) {
$fullName = $this->_properties['first_name'] . ' ' . $this->_properties['last_name'];
Cache::write('full_name_for_' . $this->_properties['id'], $fullName, 'users');
}
$this->fullName_cache = $fullName;
}
return $this->fullName_cache;
}
I think there might be a nicer way to do this. There is mention of this sort of thing in the cookbook:
Code in your accessors is executed each time you reference the field. You can use a local variable to cache it if you are performing a resource-intensive operation in your accessor like this: $myEntityProp = $entity->my_property.
Anyone had luck implementing this?
http://example.com/api/transfer/transfers/code/456/code/234
When using $this->get('code') on a url like above I expect the REST library to return an array or a list of the codes.
Instead it returns the last one.
Is there a way to return both values in a list, or is there another recommandation for formating the URL.
Thank you
I know it has been long since you posted the question. However it could help other people looking for the same thing.
Assuming that transfer is your controller and transfers is the function, another way to format your url could be:
http://example.com/api/transfer/transfers?code[]=456&code[]=234
This was you perform $this->get('code') you'll get an array back.
If you are creating the url via code then you may use http_build_query(). It handles the necessary escaping. It means it will replace [ for %5B and ] for %5D, in this case.
The code would be like:
$codes = array(456, 234);
$query = http_build_query(array('code' => $data));
$url = 'http://example.com/api/transfer/transfers?' . $query;
As my CakePHP 2.4 app gets bigger, I'm noticing I'm passing a lot of arrays around in the model layer. Cake has kinda led me down this path because it returns arrays, not objects, from it's find calls. But more and more, it feels like terrible practice.
For example, in my Job model, I've got a method like this:
public function durationInSeconds($job) {
return $job['Job']['estimated_hours'] * 3600; // convert to seconds
}
Where as I imagine that using active record patter, it should look more like this:
public function durationInSeconds() {
return $this->data['Job']['estimated_hours'] * 3600; // convert to seconds
}
(ie, take no parameter, and assume the current instance represents the Job you want to work with)
Is that second way better?
And if so, how do I use it when, for example, I'm looping through the results of a find('all') call? Cake returns an array - do I loop through that array and do a read for every single row? (seems a waste to re-fetch the info from the database)
Or should I implement a kind of setActiveRecord method that emulates read, like this:
function setActiveRecord($row){
$this->id = $row['Job']['id'];
$this->dtaa = $row;
}
Or is there a better way?
EDIT: The durationInSeconds method was just a simplest possible example. I know for that particular case, I could use virtual fields. But in other cases I've got methods that are somewhat complex, where virtual fields won't do.
The best solution depends on the issue you need to solve. But if you have to make a call to a function for each result row, perhaps it is necessary to redesign the query taking all the necessary data.
In this case that you have shown, you can use simply a virtual Field on Job model:
$this->virtualFields = array(
'duration_in_seconds' => 'Job.estimated_hours * 3600',
):
..and/or you can use a method like this:
public function durationInSeconds($id = null) {
if (!empty($id)) {
$this->id = $id;
}
return $this->field('estimated_hours') * 3600; // convert to seconds
}
What function is equal to mysql_num_rows in Magento?
For Magento, the proper equivalent is PHP's count() function.
Why?
Magento usually uses Varien_Data_Collection instances to fetch result sets containing multiple records. Varien implements the Lazy Load Pattern for these collections, that is, no result set will be fetched before you really need it.
If you take a look at the Varien_Data_Collection class, you'll see, that this class does implement PHP's Countable interface and the proper count() method for this interface:
class Varien_Data_Collection implements IteratorAggregate, Countable
{
:
public function count()
{
$this->load();
return count($this->_items);
}
:
}
If you're asking yourself now, what got lazy loading to do with counting records, then you need to know that querying a collection the usual Magento way, e.g. like this:
$collection = Mage::getModel('catalog/product')
->getCollection()
->addFieldToFilter(
'status',
Mage_Catalog_Model_Product_Status::STATUS_ENABLED
);
does not fetch the result set at all. But, how do you count records of a result set which hasn't been fetched yet? Right, you can't. And neither can mysql_num_rows. It fetches the result set first.
Now, when you call count() on the collection, e.g. by
$n = count($collection);
PHP's core count() function will detect that the passed argument $collection implements a Countable interface and has its own count() method defined, so it will call that one.
This leads to really fetching the result set* and storing it to $this->_items, which finally allows counting the records and return the number.
* In Magento you can also call foreach ($collection as $product) to really fetch the result set, but that's another story.
Is it possible to craft a wildcard WHERE query with Doctrine, like so:
->where('this_field = ?', ANYTHING);
I'd be using this to build a search query dynamically out of unknown user-selected parameters and therefore would need to have the condition in place but capable of accepting the value "anything".
To add, I can get this to work:
$field = 'this_field = ?';
$value = 5;
...
->where($field, $value);
... but it's still doesn't allow me to use "anything" as value or to eliminate the entire query condition. The following fails:
$field = NULL;
$value = NULL;
...
->where($field, $value);
Thanks
If you build the query dynamically you can also check wther $value has a value or not and then add the where part if necessary. E.g.
$q; // Your query object.
if(isset($value)) { // or empty() or maybe just if($value) depending on your needs.
$q->where('this_field = ?', $value);
}
This is easier to understand and easier to debug imo.
[Answer to own question]
After playing around with this, found a solution that works, posting here for others.
Using ->whereIn allows Doctrine to ignore a query condition completely:
$empty = array();
...
->andWhereIn('this_field', $empty)
The above condition doesn't get included in the resulting SQL at all.