Tag: cakephp find

find(‘first’)… gotcha

Just a simple tip…

Here’s a typical find() example:

$this->Post->find('first', array('condition' => array('id' => 5)));

Why does it return me the post with ID = 1, rather than ID = 5?

Of course, a careful reader spotted the spelling issue in the key:
‘condition’ instead of ‘conditions’.

Moral of the story is that find(‘first’) can be a little tricky and misleading and if you are getting an unexpected result be sure to double-check your implementation.

find(‘first’, ‘blah’);

Example of many nested conditions in CakePHP's find()

Just a quick example on how you can use deep, complex find conditions with OR, AND and NOT arrays in one shot…

We need to get a list of all companies, where:
Company.name is either ‘Future Holdings’ OR ‘Steel Mega Works’ AND we need to ensure that Company.status is either ‘active’ OR NOT ‘inactive’ OR ‘suspended’…

Here’s how you can accomplish this in cake:

$conditions = array(

   'OR' => array(
      array('Company.name' => 'Future Holdings'),
      array('Company.name' => 'Steel Mega Works')
   ),

   'AND' => array(
      array(

         'OR'=>array(
            array('Company.status' => 'active'),

            'NOT'=>array(
               array('Company.status'=> array('inactive', 'suspended'))
            )
         )
     )
   )
);

Which produces the following SQL:

[sourcecode language="sql"]
SELECT `Company`.`id`, `Company`.`name`, `Company`.`description`, `Company`.`location`, `Company`.`created`, `Company`.`status`, `Company`.`size`

FROM
`companies` AS `Company`
WHERE
((`Company`.`name` = ‘Future Holdings’)
OR
(`Company`.`name` = ‘Steel Mega Works’))
AND
((`Company`.`status` = ‘active’)
OR (NOT (`Company`.`status` IN (‘inactive’, ‘suspended’))))
[/cc]

find(‘list’) with three (or combined) fields

Update 01/07/2010: Good news everyone, for those switching over to cake 1.3, there is a great new feature:
http://book.cakephp.org/view/1608/Virtual-fields
————–

How about a little trick to extend the find(‘list’) functionality?..

Let’s say we need to display a list of users, but instead of just User.id and User.name we need to have User.id, as well as User.name and User.email combined. Unfortunately find(‘list’) doesn’t have that type of functionality out-of-the-box. Well, that’s OK, we’ll make our own…

Before I continue, I’d like to thank grigri for a very creative way to use Set::combine() as well as cakebaker for his tips on extending the find() method functionality.

Alright, let’s add this to our app model:

 function find($type, $options = array()) {
        switch ($type) {
            case 'superlist':
                if(!isset($options['fields']) || count($options['fields']) < 3) {
                    return parent::find('list', $options);
                }

                if(!isset($options['separator'])) {
                    $options['separator'] = ' ';
                }

                $options['recursive'] = -1;              
                $list = parent::find('all', $options);

                for($i = 1; $i <= 2; $i++) {
                    $field[$i] = str_replace($this->alias.'.', '', $options['fields'][$i]);               
                }           

                return Set::combine($list, '{n}.'.$this->alias.'.'.$this->primaryKey,
                                 array('%s'.$options['separator'].'%s',
                                       '{n}.'.$this->alias.'.'.$field[1],
                                       '{n}.'.$this->alias.'.'.$field[2]));
            break;                      

            defau<              
                return parent::find($type, $options);
            break;
        }
    }

Now all we have to do in our controller is:

$this->User->find('superlist', array('fields'=>array('User.id',
                                                                  'User.name',
                                                                  'User.email'),
                                              'separator'=>' * '));

Which ultimately gives us something like:

...
<option value=&quot;1&quot;>Bob * bob@hotmail.com</option>
...

Let’s quickly go over what we’ve done…

We overrode the default Model::find() method by extending it with a new find type called ‘superlist’. We also allow a new key ‘separator’ to specify the delimiter between the fields (for the sake of the example I chose a ‘ * ‘).

The code is not very complicated, so I hope you can figure out what’s going just by examining our custom find() function… but just a few points to help you along:

  • Specify at least 3 fields, or the method will default to the regular find(‘list’)
  • Field order is important
  • We need to specify the primary key field to extract the appropriate value
  • Model’s primary key field is always used for the option value
  • Fields can be specified as ‘fieldName’ or ‘Model.fieldName’
  • If you do not specify the ‘separator’ key, it will default to a space character

Keep in mind that this is more of an exercise, and you can achieve the same results without any overrides, but this certainly helps to keep your controllers nice, clean and skinny.

'fields', 'conditions' and associated models in CakePHP 1.2

Sometimes you see code that tries to do something like this:

$this->Company->find('all', array('conditions' =>array('Profile.name'=>'test'),
                                            'fields'=>array('Profile.id, Profile.name')))

Of course most of the time you’ll hear: “Well, this doesn’t work!”
Yet, some people will disagree: “Works for me!”

So, what’s really going on here?

Whether the above syntax works or not, would depend on your model association. Or, more specifically, whether or not an SQL JOIN is built.

If we assume that Company hasOne Profile, then the above find() will build a query like:

[sourcecode language="sql"]
SELECT
Profile.id, Profile.name
FROM
`companies`
AS
`Company`
LEFT JOIN
`profiles` AS `Profile`
ON
(`Profile`.`company_id` = `Company`.`id`)
WHERE
`Profile`.`name` = ‘test’
[/cc]

… which is perfectly legal.

Of course if you attempt to do this for hasMany or HABTM, the query is going to fail.
You’ve probably guessed that the same works for belongsTo.

read() vs find()

What exactly is the differednce between $this->User->read() and $this->User->find()?

Seemingly, they both retrieve the data (and possibly associated data) for the User model. Yet, there is a an important distinction: read() will also set the data for your model object, while find() will simply return the resultset array.

What’s the big deal?
None, really. However, if you are not planning to manipulate the object in any way, what exactly is the point of populating it with any data?
If your goal is simply to get some data and pass it to the view for display, you should stick with using find(). As a matter of fact read() will call find() internally anyways.

That beings said, read() can probably be useful if you need to manipulate your model’s data and then save it. Surely the same can be done with find(), but read() will provide a cleaner syntax, if that’s what you are after.

I’m not going to address any issues with performance, because if you are running into bottlenecks due to the performance of read() vs find(), I’m sure you’ve got more pressing issues at hand to deal with (like saving your DB or purchasing a better server).

I need the first record from my table…

Let’s say you need to know when was the very first user record created.
Add a method like this to your User model (assuming you have a ‘created’ field):

[sourcecode language='php']
function getFirstDay() {
return $this->find(‘first’, array(
‘fields’ => array(‘created’),
‘order’ => ‘User.created ASC’));
}
[/cc]

Try in the controller: pr($this->User->getFirstDay());

Similar method can be used to get information about any other arbitrary field.