Virtual field in 'fields' find condition - Cakephp - cakephp

I have a Student model with a virtual field:
var $virtualFields = array(
'full_name' => 'CONCAT(Student.fname, " ", Student.lname)'
);
I am doing a find operation to fetch specific fields using 'fields':
$this->Student->find('all', array('fields' => array('Student.fname','Student.lname')));
For some reason, the virtual field is not being created after finding the records. I tried adding Student.full_name but of course it gives an unknown column error in mysql.
Any ideas?

You can't specify in fields a virtual field you need to call it without the fields option so it bring it with the results... you may wanna read the examples in the cookbook
calling all fields is one option another one is to call the field in the fields option, something like this
$this->Student->find('all', array('fields' => array(
$this->student->virtualFields['full_name'].'AS Student__full_name',
'Student.fname','Student.lname')
));

what about placing a method in your app_model.php like so:
/**
* combine virtual fields with fields values of find()
* USAGE:
* $this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')));
* #param array $virtualFields to include
*/
public function virtualFields($fields = array()) {
$res = array();
foreach ((array)$fields as $field) {
//TODO: if key numeric => value sql!
//TODO: allow combined/other models via Model.field syntax
$sql = $this->virtualFields[$field];
$res[] = $sql.' AS '.$this->alias.'__'.$field;
}
return $res;
}
and then use it like so:
$this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')))
));
or so:
$fields = $this->Model->virtualFields('full_name');
$fields = am($fields, 'status', 'created');
$this->Model->find('all', array('fields' => $fields));
));

On the CookBook:
http://book.cakephp.org/2.0/en/models/virtual-fields.html
It says that you can use it like:
Model:
public $virtualFields = array(
'name' => 'CONCAT(User.first_name, " ", User.last_name)');
Controller or View:
$results = $this->User->find('first');
Result:
array(
[User]
[first_name] => 'Mark',
[last_name] => 'Story',
[name] => 'Mark Story',
//more fields.
)
So you can use it just like this:
$this->set('list_fields', $this->User->find('list',
array('fields' => array('first_name', 'last_name', 'name'),
'recursive' => -1, 'condition' => ...)));

Related

Containable with Condition

I am using containable with CakePHP. My tried code is ...
public function detail($slug = "") {
$this->Poet->contain('User.id', 'User.full_name', 'Song.id', 'Song.name', 'Song.name_hindi', 'Song.slug');
$result = $this->Poet->findBySlug($slug);
if (!$result) {
throw new NotFoundException(__('Invalid Poet - ' . $slug));
}
pr($result);
die();
$this->Poet->id = $result['Poet']['id'];
$this->set('result', $result);
}
Like this. Now I have Song.status as my association with Song table. I want to fetch only those records that has status = 1. Is it possible? Can I select only active records with my piece of code.
Use a normal find
While the magic findBy* methods are handy from time to time, it's a good idea to only use them for trivial queries - your query is nolonger trivial. Instead use a normal find call e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
)
),
'conditions' => array(
'slug' => $slug,
'Song.status' => 1 // <-
)
));
Does a Poet hasMany songs?
You don't mention your associations in the question, which is rather fundamental to providing an accurate answer, however it seems likely that a poet has many songs. With that in mind the first example will generate an sql error, as there will be no join between Poet and Song.
Containable does permit filtering associated data e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
'Song.status = 1' // <-
)
),
'conditions' => array(
'slug' => $slug
)
));
This will return the poet (whether they have relevant songs or not), and only the songs with a status of "1". You can achieve exactly the same thing by defining the condition in the association definition (either directly in the model or by using bindModel).

CakePHP Pagination. Keeping Model Fat

I currently have this in my Model (Referer Model):
public function getReferers($type = 'today') {
if ($type == 'this_month') {
return $this->_getThisMonthsReferers();
} elseif ($type == 'today') {
return $this->_getTodaysPageReferers();
}
}
private function _getThisMonthsReferers() {
$today = new DateTime();
return $this->Visitor->find('all', array(
'fields' => array(
'Referer.url',
'COUNT(UserRequest.visitor_id) as request_count',
'COUNT(DISTINCT(Visitor.id)) as visitor_count',
'COUNT(UserRequest.visitor_id) / COUNT(DISTINCT(Visitor.id)) as pages_per_visit',
'COUNT(DISTINCT(Visitor.id)) / COUNT(UserRequest.visitor_id) * 100 as percent_new_visit'
),
'joins' => array(
array(
'table' => 'user_requests',
'alias' => 'UserRequest',
'type' => 'RIGHT',
'conditions' => array(
'UserRequest.visitor_id = Visitor.id'
)
)
),
'conditions' => array(
'Visitor.site_id' => $this->Site->id,
'MONTH(UserRequest.created)' => $today->format('m'),
'YEAR(UserRequest.created)' => $today->format('Y')
),
'group' => array(
'url'
)
));
}
The thing is that I how I would paginate this. It will be so easy if just copy my code out of the model and to the controller. The thing is I want the keep the query in my Model.
How is this supposed to be done in CakePHP?
A custom find type is one method. You can find more information here: http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html#custom-query-pagination
To turn your _getThisMonthsReferers into a custom find, follow this http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#creating-custom-find-types
For example:
// model
protected function _findThisMonthsReferers($state, $query, $results = array()) {
if ($state === 'before') {
$query['fields'] = ....
$query['joins'] = ....
return $query;
}
return $results;
}
// controller
public $paginate = array('findType' => 'thisMonthsReferers')
EDIT:
I think it should be :
public $paginate = array('thisMonthsReferers');
However the Solution I used derived from this answer is adding this to the method I am using
$this->paginate = array('thisMonthsReferers');
Since I don't want i used in all my actions. Then paginating the Model like this.
$this->paginate('Visitor);
Instead of returning the results of the find, just return it's array of options:
return array(
'fields' => array(
//...etc
Then use those options to paginate in the controller. More details on this answer of this similar question: Paginate from within a model in CakePHP
It still keeps the model fat (with any logic that might alter the conditions, joins, fields...etc), and the controller skinny, which just uses the returned array as paginate options.

cakephp model with hasmany

I have question in cakephp model,
I want to add dynamic condition in var $hasMany keyword
I want to add condition like current user_id, i got user Id after my login.
var $hasMany = array(
"AskComment"=>array('limit'=>3),
'AskStatistic',
'AskContactsLink',
'AskStatistic',
'AskObject',
'AskLikes'
);
If you want to add dynamic condition in your model, then you might have to bind the model association-ship dynamically into your controller's code. Write the following code into your controller's method for which you want to impose some new condition on the existing/new associated models.
$this->PrimaryModel->bindModel(array('hasMany' => array(
'AskComment' => array(
'className' => 'AskComment',
'foreignKey' => 'primary_id',
'conditions' => array('AskComment.user_id' => $user_id)
)
)
));
Take a look at this link: Creating and destroying associations on the fly. This will surely help you to achieve the same.
I think its better to put your association in the construct function of your Model.
like this:
/**
* #see Model::__construct
*/
public function __construct($id = false, $table = null, $ds = null) {
public $hasMany = array(
'AskComment' => array(
'className' => 'AskComment',
'foreignKey' => 'primary_id',
'conditions' => array(
'AskComment.user_id' => $user_id,
),
),
);
}

Pagination with Containable conditions work with hasOne, but not with hasMany

For example, I have this relationship:
UserContact hasMany Contact
Contact hasOne Info
Contact hasMany Response
And I need to paginate Contact, so I use Containable:
$this->paginate = array(
'limit'=>50,
'page'=>$page,
'conditions' =>array('Contact.id'=>$id),
'contain'=>array(
'Response',
'Info'
)
);
I want to add search by Info.name, and by Response.description. It works perfect for Info.name, but it throws an error if I try using Response.description, saying that the column doesn't exist.
Additionally, I tried changing the relationship to Contact hasOne Response, and then it filters correctly, but it only returns the first response and this is not the correct relationship.
So, for example, if I have a search key $filter I'd like to only return those Contacts that have a matching Info.name or at least one matching Response.description.
If you look at how CakePHP constructs SQL queries you'll see that it generates contained "single" relationships (hasOne and belongsTo) as join clauses in the main query, and then it adds separate queries for contained "multiple" relationships.
This makes filtering by a single relationship a breeze, as the related model's table is already joined in the main query.
In order to filter by a multiple relationship you'll have to create a subquery:
// in contacts_controller.php:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$dbo = $this->Contact->getDataSource();
$subQuery = $dbo->buildStatement(array(
'fields' => array('Response.id'),
'table' => $dbo->fullTableName($this->Contact->Response),
'alias' => 'Response',
'conditions' => $conditionsSubQuery
), $this->Contact->Response);
$subQuery = ' EXISTS (' . $subQuery . ') ';
$records = $this->paginate(array(
'Contact.id' => $id,
$dbo->expression($subQuery)
));
But you should only generate the subquery if you need to filter by a Response field, otherwise you'll filter out contacts that have no responses.
PS. This code is too big and ugly to appear in the controller. For my projects I refactored it into app_model.php, so that each model can generate its own subqueries:
function makeSubQuery($wrap, $options) {
if (!is_array($options))
return trigger_error('$options is expected to be an array, instead it is:'.print_r($options, true), E_USER_WARNING);
if (!is_string($wrap) || strstr($wrap, '%s') === FALSE)
return trigger_error('$wrap is expected to be a string with a placeholder (%s) for the subquery. instead it is:'.print_r($wrap, true), E_USER_WARNING);
$ds = $this->getDataSource();
$subQuery_opts = array_merge(array(
'fields' => array($this->alias.'.'.$this->primaryKey),
'table' => $ds->fullTableName($this),
'alias' => $this->alias,
'conditions' => array(),
'order' => null,
'limit' => null,
'index' => null,
'group' => null
), $options);
$subQuery_stm = $ds->buildStatement($subQuery_opts, $this);
$subQuery = sprintf($wrap, $subQuery_stm);
$subQuery_expr = $ds->expression($subQuery);
return $subQuery_expr;
}
Then the code in your controller becomes:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$records = $this->paginate(array(
'Contact.id' => $id,
$this->Contact->Response->makeSubQuery('EXISTS (%s)', array('conditions' => $conditionsSubQuery))
));
I can not try it now, but should work if you paginate the Response model instead of the Contact model.

cakephp find list

Hi I want to be able to generate a list using find so that I can use in select helper. but there is a problem. i want too fetch id,name(first + last). so how can I achieve it. I want first_name and last_name to be joined as name . How can I achieve it.
$this->User->find('all',array('fields' => array('first_name','last_name','id')));
I can't use model filters and callback Please suggest me how can I do it in controllers itself.
I think this can be done using the virtualFields and displayField properties in your model.
In your model, define a virtual field for the full name like this:
public $virtualFields = array(
'full_name' => 'CONCAT(User.first_name, " ", User.last_name)'
);
If you now set displayField to full_name you should be able to get a list of your users with the $this->User->find('list') method which you can use without problems with the Form-helper.
public $displayField = 'full_name';
... or:
public $displayField = 'User.full_name';
The id is fetched automatically.
Another solution is to use Cake's Set::combine to build what you need...
$users = $this->User->find('all',array('fields' => array('first_name','last_name','id')));
$user_list = Set::combine($users, '{n}.User.id', array('{0} {1}', '{n}.User.first_name', '{n}.User.last_name'));
Result will look something like:
array(
[2] => 'First Last',
[5] => 'Bob Jones'
)
Here's the documentation link:
http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::combine
To achieve this first go to the model and add this line
public $virtualFields = array('full_name' => 'CONCAT(first_name, " ", last_name)');
and then go to controller file just use the name "full_name" which you put in virtual fields
$this->User->find('all',array('fields' => array('full_name','id')));
It returns name with combined fields
+1 on Tim's answer, however, if you need an alternative, teknoid wrote a nice article about this a long time ago:
http://nuts-and-bolts-of-cakephp.com/2008/09/04/findlist-with-three-or-combined-fields/
In my case, Set::combine was the way to go, since I had to deal with concatenation of fields in associated models, like:
$bancos_enteros = $this->Financiacion->Banco->find('all', array(
'fields' => array('Empresa.codigo_contable','Empresa.nombre_corto', 'Banco.id'),
'order' => array('Empresa.codigo_contable' => 'asc'),
'recursive' => 1
));
$bancos = Set::combine(
$bancos_enteros,
'{n}.Banco.id',
array(
'{0} {1}',
'{n}.Empresa.codigo_contable',
'{n}.Empresa.nombre_corto'
)
);
returning
array(
(int) 14 => '57200002 Caixa',
(int) 15 => '57200003 Sabadell',
(int) 3 => '57200005 BBVA',
(int) 16 => '57200006 Deutsche Bank',
(int) 17 => '57200007 Popular',
(int) 18 => '57200009 March',
(int) 26 => '57200010 Bankinter',
(int) 4 => '57200011 Santander'
)
While
$this->Financiacion->Banco->Empresa->virtualFields = array(
'codigo_nombre' => 'CONCAT(Empresa.codigo_contable,Empresa.nombre_corto)'
);
$this->Financiacion->Banco->virtualFields['codigo_nombre'] = $this->Financiacion->Banco->Empresa->virtualFields['codigo_nombre'];
$bancos = $this->Financiacion->Banco->find('list', array(
'fields' => array('Banco.id','Banco.codigo_nombre'),
'order' => array('Banco.codigo_nombre' => 'asc'),
'recursive' => 1
)
);
returns a SQL error in a following query if I don't delete the virtual fields first:
unset($this->Financiacion->Banco->Empresa->virtualFields);
unset($this->Financiacion->Banco->virtualFields);

Resources