The recipe, Searching for all items that match search terms, gives us a great starting point to create a custom find type. Custom find types allow us to extend the basic find types any model has, allowing our code to become more readable and extensible.
This recipe shows how to create a custom find type to allow the Post
model to be searched against a set of terms, thus extending the functionality shown in the previous recipe.
We need some sample models and data to work with. Follow the Getting ready section of the recipe, Performing GROUP and COUNT queries.
post.php
file and add the search
find type to the list of find methods using the _findMethods
property, together with the actual implementation of the _findSearch()
method.<?php class Post extends AppModel { public $belongsTo = array('Blog'), public $_findMethods = array('search' => true); protected function _findSearch($state, $query, $results = array()) { if ($state == 'before') { if (!empty($query['terms'])) { $fields = array('title', 'body'), $conditions = array(); foreach ((array) $query['terms'] as $term) { foreach ($fields as $field) { $model = $this->alias; if (strpos($field, '.') !== false) { list($model, $field) = explode('.', $field); } $conditions[] = array( $model . '.' . $field . ' LIKE ?' => '%'.$term.'%' ); } } if (empty($query['fields'])) { $query['fields'] = array('Post.title', 'Post.body'), } $query['conditions'][] = array('or' => $conditions); } return array_diff_key($query, array('terms'=>null)); } return $results; } } ?>
search
find setting:$posts = $this->Post->find('search', array( 'terms' => array( 'Post 1', 'Post 2' ), 'recursive' => -1 ));
http://localhost/posts
, we get the id
and title
fields for four posts, as it is partially shown in the following screenshot: AppModel
. Open the file app_model.php
in your app/
folder (create it if you don't have one), and override the find()
method as shown below:<?php class AppModel extends Model { public function find($conditions=null, $fields=array(), $order=null, $recursive=null) { if ( is_string($conditions) && $conditions=='count' && is_array($fields) && !empty($fields['type']) && array_key_exists($fields['type'],$this->_findMethods) ) { $fields['operation'] = 'count'; return parent::find($fields['type'], array_diff_key( $fields, array('type'=>null) )); } return parent::find($conditions, $fields, $order, $recursive); } } ?>
app/models/post.php
file and make the following changes to the _findSearch()
method:protected function _findSearch($state, $query, $results = array()) { if ($state == 'before') { if (!empty($query['terms'])) { $fields = array('title', 'body'), $conditions = array(); foreach ((array) $query['terms'] as $term) { foreach ($fields as $field) { $model = $this->alias; if (strpos($field, '.') !== false) { list($model, $field) = explode('.', $field); } $conditions[] = array( $model . '.' . $field . ' LIKE ?' => '%'.$term.'%' ); } } if (empty($query['fields'])) { $query['fields'] = array('Post.title', 'Post.body'), } if (!empty($query['operation']) && $query['operation'] == 'count') { $query['fields'] = 'COUNT(*) AS total'; } $query['conditions'][] = array('or' => $conditions); } return array_diff_key($query, array('terms'=>null)); } elseif ( $state == 'after' && !empty($query['operation']) && $query['operation'] == 'count' ) { return (!empty($results[0][0]['total']) ? $results[0][0]['total'] : 0); } return $results; }
$count = $this->Post->find('count', array( 'type' => 'search', 'terms' => array( 'Post 1', 'Post 2' ) ));
Which would correctly return 4.
Custom find types are defined in the model _findMethods
property. We add types by adding the name of the find type to the property as its index, and setting true
as its value in the model that contains the find type.
The method responsible for dealing with the actual find type is named using the following syntax: _findType()
, where Type
is the find type name, with its first case in uppercase. For a find type of name popular, the method would be named _findPopular()
.
Every find type method receives three arguments:
state
: The state at which the find
operation is currently on. This can be before
(used right before the find operation is to be executed), or after
(executed after the find operation is finished, and the perfect place to modify the obtained results.) The before
state is where we change the query parameters to meet our needs.query
: The data for the query, containing typical find settings (such as fields
or conditions
), and any extra settings specified in the find
operation (in our case, terms
).results
: Only applicable when the state is set to after
, and includes the result of the find
operation.When the state of the find is set to before
, the custom find type implementation needs to return the query, as an array of find settings. Therefore, in our implementation, we look for a custom find setting named terms
. If there are terms specified, we use them to add LIKE-
based conditions to a fixed list of fields. Once we are done, we return the modified query.
When the state is set to after
, the implementation needs to return the results. This is the opportunity to modify the resulting rows, if needed, before returning them. In our implementation, we simply return them as they were sent to us.
The last part of the recipe shows us how to add count support for our custom find types. This is something that CakePHP does not offer out of the box, so we implement our own solution. We do so by overriding the find()
method and checking to make sure a set of conditions are met:
find
operation being executed is set to count
type
setting specified in the query type
setting is in fact a valid custom find typeWhen these conditions are met, we add a new query parameter named operation
, setting it to count
, and we then call the parent find()
implementation using the custom find type. This way, our find implementation can check for the operation
find setting, and when it is set to count
, it forces the fields
find setting to COUNT(*)
in the before
state, and correctly gets the result of the count operation in the after
state.
18.191.139.42