CakePHP 2.0 - How to remove join tables from containable - cakephp

I'm using Containable in an action like this:
public function index()
{
$this->User->recursive = -1;
$this->User->Behaviors->load('Containable');
if ($this->RequestHandler->accepts('xml'))
{
$this->set('users', array("Users" => array("UserEntry" => $this->User->find('all',
array(
'fields' => array('User.id','User.username', 'User.email', 'User.created', 'User.modified'),
'contain' => array(
'Group' => array(
'fields' => array('Group.id','Group.name','Group.created'),
)
)
)
))));
}
else if ($this->RequestHandler->accepts('json'))
{
}
else if ($this->RequestHandler->accepts('html'))
{
$this->set('users', $this->paginate());
}
}
It gets all of the data I need, but there is one thing that I can't figure out. There is a HABTM relationship between users and groups with a join table users_groups. I'm serializing the output of find('all') into Xml for a REST Api in the view. The problem is that the data contains an extra 'GroupsUser' array nested in my 'Groups' array. The users of the Api do not need to know about the join table information so I would like to remove it. The current output looks like this:
index.ctp
<?php
//debug($users);
$xml = Xml::build($users, array('return' => 'domdocument'));
echo $xml->saveXML();
?>
output of index.ctp -> http://www.pastie.org/2789367
See the GroupsUser tag nested in the Group tag? That is what I want to remove. If there is not a nice easy way to do this I will either build the xml by hand using some loops in the view or create my own find method in the the model and use unset() on GroupsUser. Both of those solutions are not ideal, so I'm hoping someone here has a better one. :)

I you are positive that everything is being done in a join (only belongsTo associations) you may use the containable component with autofields in false something like this
$this->Post->Behaviors->attach('Containable', array('autoFields' => false));
(this is to attach it dynamiclly in the controller part.
if your find has hasMany association, you will have 2 queries instead of so cake needs to fetch for this fields to do the join. You may also use linkable component for this case, that put all in joins instead of a lot of queries giving you chance to select only the fields you want.
here is a link for the linkable component

Related

Multi level ordering in cakePHP

Why don't cakePHP include all my associations into one SQL query? The only way I have been able to do this is using "joins", but I hoped belongsTo and Containable behaviour was enough.
Here is an example:
Post->belongsTo->Category->belongsTo->category_type
(All models are setup correctly and work.
listing posts with pagination in index, I try this:
public function index() {
$this->paginate = array( 'contain' => array('Category' => array('CategoryType')));
$this->set('posts', $this->paginate());
}
This fetches the array correctly, but it does it in many SQLs like this:
SELECT `Post`.`id`, `Post`.`name`, `Post`.`content`, `Post`.`category_id`, `Category`.`id`, `Category`.`name`, `Category`.`category_type_id` FROM `unit_app`.`post` AS `Post` LEFT JOIN `unit_app`.`categories` AS `Category` ON (`Post`.`category_id` = `Category`.`id`) WHERE 1 = 1 LIMIT 20
SELECT `CategoryType`.`id`, `CategoryType`.`name` FROM `unit_app`.`category_types` AS `CategoryType` WHERE `CategoryType`.`id` = 1
SELECT `CategoryType`.`id`, `CategoryType`.`name` FROM `unit_app`.`category_types` AS `CategoryType` WHERE `CategoryType`.`id` = 2
SELECT `CategoryType`.`id`, `CategoryType`.`name` FROM `unit_app`.`category_types` AS `CategoryType` WHERE `CategoryType`.`id` = 2
This makes it difficult to order this query on CategoryType.name ASC.
Any suggestions?
If joins are only option, do I have to unbind the models before querying?
Will pagination work fine with joins?
Note! this is just a small part of all models, the resulting post->index need to fetch many other models through similar associations also.
(tested on cake 2.2.0 and v2.4.0-dev, php v5.4.11)
UPDATE! ---------
I just wanted to show my findings. I have now solved this without joins, but I had to re-bind in the model to get it working.
This is basically what I did to get it to work (also with paginations and sorts):
In Post model:
Added a bind function:
$this->unbindModel(array(
'belongsTo' => array('Category')
));
$this->bindModel(array(
'hasOne' => array(
'Category' => array(
'foreignKey' => false,
'conditions' => array('Category.id = Post.category_id')
),
'CategoryType' => array(
'foreignKey' => false,
'conditions' => array('CategoryType.id = Category.category_type_id')
))));
Then I added this to my index in Post controller:
$this->Post->bindCategory();
$this->paginate = array('contain' => array('Category' ,'CategoryType'));
$this->set('posts', $this->paginate());
I include table headers also just for documentation:
<th><?php echo $this->Paginator->sort('CategoryType.name', 'Type'); ?></th>
<th><?php echo $this->Paginator->sort('Category.name', 'Category'); ?></th>
I hope this post can help others as well :)
I am also going to test this Behaviour to see if I can omit all the bind-functions as well: https://github.com/siran/linkable/
There are lots of plugins to cake, but cake should have a "certification" of the plugins. It is quite difficult to find the fully working and tested ones on github :)
I also miss a site like railscasts.com just for cake :D
/MartOn
Yes, you must use JOINs to be able to order based on an associated models results.
Yes, you can paginate with JOINs. Just pass your options (including JOINs) to your paginate prior to actually calling $this->paginate();. (there are many resources online for how to paginate with JOINs)

How to sort data of bind table in cakephp 2.0

I need the data from various table. I bind them by using cakephp's $hasMany variable. The data is fetched successfully. But I need to sort the result coming from $hasMany table.
for eg. I have two tables
Survey
Questions
Now Survey table contains the data related to Survey like title, id, purpose and Questions table contains question for related survey. I bind questions table with survey in survey model. Now I have a field in Questions table with name ordering. I need to fetch data in that order.
How can I fetch it in that way?
Please help me.
If you want to sort the data directly when it is fetched from db
You can define default order when adding relations between tables in your models. In your Survey model:
var $hasMany = array(
'Question' => array(
'order' => 'ordering DESC'
)
);
See http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany.
You can also define custom ordering when retrieving data from your controller in your conditional array, http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#find:
$conditions = array(
'order' => array('Question.ordering DESC')
);
If you are paginating your result with the paginator component, you can setup it like in following example book.cakephp.org/2.0/en/core-libraries/components/pagination.html#query-setup:
public $paginate = array(
'order' => array(
'Question.ordering' => 'desc'
)
);
If you want to sort the data presented in the view
Here you can use the pagination helper (together with the pagination component in the controller) as:
echo $this->Paginator->sort('Question.ordering');
see book.cakephp.org/2.0/en/core-libraries/helpers/paginator.html#creating-sort-links
Sorry for removing 'http://' on the two last links, but I am not allowed to post more than two links (<10 rep).
In CakePHP you can sort by associated tables. but you need to sort by column, you should use something like this in your view:
<?php echo $this->Paginator->sort('Question.ordering'); ?>

cakephp: find statement with 'contain'

the following User model function is from MilesJones forum plugin. Can someone tell me on what is the use of 'contain' in the find stmt. I couldn't find any example with contain in the cakephp cookbook. Any helps is appreciated.
public function getProfile($id) {
return $this->find('first', array(
'conditions' => array('User.id' => $id),
'contain' => array(
'Access' => array('AccessLevel'),
'Moderator' => array('ForumCategory')
)
));
}
By default when a find statement executes cake pulls all the data from the model on which the find function is executing plus all the data from the models that are associated with the model. Most of the time you don't need that extra data, Cake has containable behaviour for exactly that purpose. You can specify which associated model's data you want in your result.
In the above example find statement will fetch the first record from the User model plus associated data from Access and Moderator models.
Here is the link from cakephp book http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
Here is cakephp documentation about contain

CakePHP AutoComplete Question

I am working on a book review application and I am using autoComplete to search for titles in when creating a review. The review model has an associated book_id field and the relationship is setup as the review hasOne book and a book hasMany reviews.
I am trying to pass the Book.id (into the book_id field), but I want to display Book.name for the user to select from. With the default setup (accomplished via CakePHP's tutorial), I can only pass Book.name. Is it possible to display the name and pass the id?
Also, I am passing it via the following code in the create() action of the review controller:
$this->data['Review']['book_id'] = $this->data['Book']['id'];
Is that the proper way to do it in CakePHP? I know in Ruby on Rails, it is automatic, but I can't seem to make it work automagically in CakePHP. Finally, I am not using the generator because it is not available in my shared hosting environment... so if this is the wrong way, what do I need other than associates in my models to make it happen automatically?
Thanks for the help and I promise this is my question for awhile...
UPDATE- I tried the following, but it is not working. Any ideas why?
function autoComplete() {
$this->set('books', $this->Book->find('all', array(
'conditions' => array(
'Book.name LIKE' => $this->data['Book']['name'].'%'
),
'fields' => array('id','name')
)));
$this->layout = 'ajax';
}
The problem is that when I use the code above in the controller, the form submits, but it doesn't save the record... No errors are also thrown, which is weird.
UPDATE2:
I have determine that the reason this isn't working is because the array types are different and you can't change the array type with the autoComplete helper. As a workaround, I tried the follow, but it isn't working. Can anyone offer guidance why?
function create() {
if($this->Review->create($this->data) && $this->Review->validates()) {
$this->data['Review']['user_id'] = $this->Session->read('Auth.User.id');
$this->Book->find('first', array('fields' => array('Book.id'), 'conditions' => array('Book.name' => $this->data['Book']['name'])));
$this->data['Review']['book_id'] = $this->Book->id;
$this->Review->save($this->data);
$this->redirect(array('action' => 'index'));
} else {
$errors = $this->Review->invalidFields();
}
}
FINAL UPDATE:
Ok, I found that the helper only takes the find(all) type or array and that the "id" field wasn't passing because it only applied to the autoComplete's LI list being generated. So, I used the observeField to obtain the information and then do a database lookup and tried to create a hidden field on the fly with the ID, but that didn't work. Finally, the observeField would only take the characters that I put in instead of what I clicked, due to an apparent Scriptaculous limitation. So, I ended up going to a dropdown box solution for now and may eventually look into something else. Thanks for all of the help anyway!
First of all, $this->data will only contain ['Book']['id'] if the field exists in the form (even if it's hidden).
To select something by name and return the id, use the list variant of the find method, viz:
$selectList = $this->Book->find('list', array(
'fields' => array(
'id',
'name'
)));
$this->set('selectList', $selectList);
In the view, you can now use $selectList for the options in the select element:
echo $form->input('Book.id', array('type' => 'hidden'));
echo $form->input('template_id', array(
'options' => $selectList,
'type' => 'select'
));

Forcing left join on my simple inner join query

The site is already built from years. I am doing some modifications to it. It has controllers "Posts" and "Topics". On the topics page all recent posts are displayed. So it is a simple find of posts from "Posts" table. The fetch is not all assiciated to topic as we are showing all "Posts" and not "Topic" specific "Posts". In Topics controller,
App::import('Model', 'Post'); and $Posts = new Post;
are added and I call the action "getDiscussions" from Model "Post". The problem is that though I am not using any left join with "Topics" and "Users" (which is one more model used to display user details with post), the query is adding Left joins with two tables and that is giving a wrong result.
Please help.
Many Thanks
Yukti
I think it is better to try to find out why cake is doing these "joins", rather than performing yet another forced query, so you might want to look into your model associations to check dependancies. Also, you can set $this->Posts->recursive = -1; to make sure you don't import unwanted stuff.
In the end, I am not sure what query you are trying to perform, so I can't help you further. However, here is a sample of a query I have successfully used to perform a left join, as I've noticed that many examples I found while writing my own were bugged:
$user_record = $this->User->find('first', array(
'conditions' => array('`Openidurl`.`openid`' => $openid),
'joins' => array(
array(
'table' => 'openidurls',
'alias' => 'Openidurl',
'type' => 'LEFT',
'foreignKey' => 'user_id',
'conditions'=> array('`Openidurl`.`user_id` = `User`.`id`')
)
)
)
);

Resources