Associated model conditions being ignored - cakephp

I have 2 models: Option and Modifier with such Option belongsTo Modifier relation:
public $belongsTo = array(
'Modifier' => array(
'className' => 'Modifier',
'foreignKey' => 'modifier_id',
'conditions' => array('Modifier.type' => '3'),
'fields' => 'Modifier.name',
'order' => 'Modifier.name ASC'
)
);
In OptionsController I try to get list of Modifiers:
$modifiers = $this->Option->Modifier->find('list');
And CakePHP generates SQL without conditions, so I get full list of Modifiers. Why CakePHP 2.4.4 ignores conditions, defined in model belongsTo relation? It also ignores fields and order.

this is the right behavior
when you write:
$modifiers = $this->Option->Modifier->find('list');
you are just accessing Modifier model and not all modifiers related to Option.
to achieve what you want you have to do this:
$modifiers = $this->Option->Modifier->find(
'list',
array('conditions' => array('Modifier.type' => '3')
)
you can also create your own find type (see manual)
class Modifier extends AppModel {
public $findMethods = array('type3' => true);
protected function _findType3($state, $query, $results = array()) {
if ($state === 'before') {
$query['conditions']['Modifier.type'] = 3;
return $query;
}
return $results;
}
}
and in your controller do this
$modifiers = $this->Option->Modifier->find('type3');

Related

Dynamic model relations in CakePHP

I'm trying to define the relations for a specific Model depending on environment variables.
Like this:
class Book extends AppModel {
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
if (Configure::read('prefix') == 'admin') {
$this->hasMany['Page'] = array(
// ...
'conditions' => array( /* all pages */ )
);
} else {
$this->hasMany['Page'] = array(
// ...
'conditions' => array( /* only public pages */ )
);
}
}
}
You could argue that I should apply these conditions in the query. But because I'm working with deeply nested relations I wanted to keep conditions centralised.
Now the problem that occurs is: if the Page model has relations to e.g. the Paragraph model and from the BookController I'm trying:
$this->Book->find('first', array(
'conditions' => array('Book.id'=>1),
'contain' => array('Page' => array('Paragraph'))
));
... CakePHP will tell me that Paragraph is not related to the Page model.
If I create the relation by defining a model attribute all goes well:
class Book extends AppModel {
public $hasMany = array(
'Page' => array(
// ...
)
);
}
Why is this? Do I need to manually establish those relations? Is my timing (__construct()) incorrect and should this be done elsewhere?
Kind regards,
Bart
Yes, your timing is incorrect. You should either apply these configuration options before invoking the parent constructor:
if (Configure::read('prefix') == 'admin') {
// ...
}
parent::__construct($id, $table, $ds);
or use Model::bindModel() instead which will create the necessary links:
$this->bindModel(
array('hasMany' => array(
'Page' => array(
// ...
'conditions' => array( /* ... */ )
)
)),
false
);
See also http://book.cakephp.org/...html#creating-and-destroying-associations-on-the-fly

Simple CakePHP search action

I want a simple search feature that can search the current selected results on the model's index page. I have created a model Search which has no actual table:
class Search extends AppModel {
protected $_schema = array(
'search_term' => array('type' => 'string' , 'null' => true, 'default' => '', 'length' => '255'),
'model' => array('type' => 'string' , 'null' => true, 'default' => '', 'length' => '255'),
);
public $useTable = false;
public $validate = array(
'search_term' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
'message' => 'Please enter a search term'
),
'between' => array(
'rule' => array('between',3,30),
'message' => 'Please enter a search term greater than 3 characters.'
)
)
);
}
In any index.ctp view I have this with a hidden input field with the model's name:
echo $this->Form->create('Search, array('action' => 'search'));
echo $this->Form->input('search_term', array('label'=> 'Search'));
echo $this->Form->input('model', array('type'=> 'hidden', 'value'=>$this->params['controller']));
echo $this->Form->end(__('Submit'));
In the SearchesController:
public function search() {
$conditions = null;
if( $this->request->is('post') ) {
$searchModel = $this->request->data[$this->modelClass]['model'];
...
$this->{$this->modelClass}->useTable = Inflector::tableize($searchModel);
...
$this->paginate = array('conditions'=>array($groups,'OR' => $conditions));
$this->set($searchModel, $this->paginate());
$this->render("/$searchModel/index");
}
Problem is paginate is returning an array with the model labelled as 'Search' (understandably because of the useTable call) and not say Groups or Users, the model's being searched. Any way to relabel the array returned from paginate to the model being searched ? The alternative is to modify all the index.ctp files or create a results.ctp for each model.
I wouldn’t create another model merely for searching; it’s a hack and not extendable.
In the past, I’ve just used parameters (usually in the query string) to alter the conditions array (whether it’s a normal find operation of a paginate operation). An example:
<?php
class ItemsController extends AppController {
public function index() {
$conditions = array();
if (isset($this->request->query['search'])) {
$conditions['Item.title'] = $this->request->query['search'];
}
$items = $this->Item->find('all', array(
'conditions' => $conditions
));
$this->set(compact('items'));
}
}
Hopefully the above demonstrates this approach.

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.

HABTM Find with CakePHP 2.0

I am trying to do a search, using pagination for posts which have a specific tag or tags (for example, if a user was to select two tags, then posts containing either tag would be returned).
I have the relationship defined in my Posts table
public $hasAndBelongsToMany = array('Tags' => array(
'className' => 'Tags',
'joinTable' => 'posts_tags',
'foreignKey' => 'post_id',
'associationForeignKey' => 'tag_id',
'unique' => 'keepExisting'));
How do I use Find to retrieve rows with a given tag (name or ID would be fine)
Trying:
// other pagination settings goes here
$this->paginate['conditions']['Tags.id'] = 13;
gives me an error that the relationship does not exist.
Looking at the debug info it appears that the tables are not joining the Posts_Tags and Tags table, however, when I debug the data making it to the view, the Posts objects contain the tags data.
Most of the documentation I can find for this seems to revolve around earlier versions of CakePHP, any help would be appreciated.
Could not find a satisfying solution myself.
I created a behavior to take care of this.
Create a file called HabtmBehavior.php and put it in your app/Model/Behavior folder.
Put the block of code in there and save file.
Add the behavior to your model: eg public $actsAs = array('Habtm');
Here is a usage example with find.
<?php $this->Entry->find('all', array('habtm'=>array('Tag'=>array('Tag.title'=>'value to find'))) ?>
Paginate would look something like this:
$this->paginate['Entry']['habtm']['Tag'] = array('Tag.title'=>'value to find');
You are free to add as many relations as you want by adding additional Model Names in the habtm array.
(Just be careful not to make it to complex since this could start slowing down your find results.)
<?php
class HabtmBehavior extends ModelBehavior {
public function beforeFind(Model $model, $options) {
if (!isset($options['joins'])) {
$options['joins'] = array();
}
if (!isset($options['habtm'])) {
return $options;
}
$habtm = $options['habtm'];
unset($options['habtm']);
foreach($habtm as $m => $scope){
$assoc = $model->hasAndBelongsToMany[$m];
$bind = "{$assoc['with']}.{$assoc['foreignKey']} = {$model->alias}.{$model->primaryKey}";
$options['joins'][] = array(
'table' => $assoc['joinTable'],
'alias' => $assoc['with'],
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array($bind)
);
$bind = $m.'.'.$model->{$m}->primaryKey.' = ';
$bind .= "{$assoc['with']}.{$assoc['associationForeignKey']}";
$options['joins'][] = array(
'table' => $model->{$m}->table,
'alias' => $m,
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array($bind) + (array)$scope,
);
}
return $options;
}
}
Hope this helps.
Happy baking.
I think the best solution is apply find function on join table Model. I try this before and it's work fine.
in your PostTag model :
/**
* #see Model::$actsAs
*/
public $actsAs = array(
'Containable',
);
/**
* #see Model::$belongsTo
*/
public $belongsTo = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'post_id',
),
'Tags' => array(
'className' => 'Tag',
'foreignKey' => 'tag_id',
),
);
in your controller :
// $tagsId = tags ids
$posts = $this->PostTag->find('all', array('conditions' => array('PostTag.tag_id' => $tagsId),'contain' => array('Post')));
also is better follow cake naming convention, if you have tags(plural), post_tags(first singular second plural),posts(plural) tables you must have Tag,PostTag,Post Models.

CakePHP - How can I find all languages with tongue twisters?

Model
<?php
class Tonguetwister extends AppModel {
var $name = 'Tonguetwister';
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'language' => array(
'className' => 'language',
'foreignKey' => 'language_alias',
'dependent'=> true
)
);
}
?>
Controller
<?php
class TonguetwistersController extends AppController {
var $name = 'Tonguetwisters';
var $uses = array('Tonguetwister', 'Language');
function index() {
$this->set('languages', $this->Language->find('all'));
}
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid tonguetwister', true));
$this->redirect(array('action' => 'index'));
}
$this->set('tonguetwisters', $this->Tonguetwister->find('all', array('conditions' => array('language_alias' => $id))));
}
}
?>
I only want to see languages on index() that have tongue twisters. How can I do this?
There might be a more efficient way, but here's how to pick only unique languages from the Tonguetwister table:
function index() {
$languageList = $this->Tonguetwister->find(
'list',
array(
'fields' => array( 'language_alias', 'language_alias' ),
'group' => 'Tonguetwister.language_alias',
'recursive' => -1
)
);
// $languageList is now an array that holds the language ids
$this->set(
'languages',
$this->Tonguetwister->Language->find(
'all',
array(
'conditions' => array(
'Language.id' => $languageList
)
)
)
);
}
By the way, you don't need to put Language into $uses. Since they have a relation set you can access the Language model with $this->Tonguetwister->Language.
You don't really need to do two SQL queries for this. If the tables are joined on "language_alias" you can do something like this:
function index() {
$this->Language->recursive = 0;
$this->set('languages', $this->Language->find('all', array(
'conditions' => array($this->Language->alias.'.language_alias' => $this->Tonguetwister->alias.'.language_alias')
));
}
You should just do one query that's going to join the tables properly.

Resources