CakePHP Count in a Loop - cakephp

I extended the CakePHP Blog Tutorial, and added Categories for my Posts. The Posts Model belongsTo the Category Model. In my Posts View I am looping threw my Categories table to list the Categories for a Menu in the View, which works fine:
/* gets the category names for the menu */
$this->set('category', $this->Post->Category->find('all'));
Now I am trying to add the Post count to each Menu (Category) Item. So far I got this:
/* gets the category count for category 2*/
$this->set('category_2_count', $this->Post->find('count', array(
'conditions' => array('Category.id =' => '2'))));
The Problem is that I obviously can't use the Loop in my View anymore. With this I have to get each Category + each Count, which seems very inelegant. Is there a way to query the Category Names and the Count and get one Array for the View?
Any Ideas? I am new to Cake and any help is greatly appreciated.

In your controller:
$this->set('category', $this->Post->Category->find('all', array(
'fields' => array(
'*',
'(SELECT COUNT(*) FROM posts WHERE category_id = Category.id) AS `post_count`'
)
)));
In your view:
foreach ($category as $c) {
echo $c['Category']['name']; // category name
echo $c[0]['post_count']; // post count
}

try this:
$stats = $this->Category->Post->find('all', array(
'fields' => array(
'Category.*',
'COUNT(Post.id) AS cnt'
),
'group' => 'Category.id'
));

Related

how to make associations between grand parent & parent & child in cakephp 2

I have 4 tabels
stores
categories
items
item_images
Now i want to make association between them categories have store_id foreign key and items table have category_id and item_images have item_id foreign key.
public $hasMany = array(
'Item' => array(
'className' => 'Item',
'foreignKey' => 'category_id'
));
The above association is for category that find the related items but i want to do associate it with item_images. How i can do it with item_images now?
In Controller i have this query
$storeCategoriesDetails = $this->Category->find("all", array('conditions' => array('Category.storeId' => $id)));
You want to put your association for the item_images on the Item model like you've done for items on the Category model. You can then retrieve the item images along with the items when retrieving categories using contain:-
$storeCategoriesDetails = $this->Category->find('all', array(
'contain' => array('Item' => array('ItemImage'))
'conditions' => array('Category.storeId' => $id)
));
I added this line in my controller function and it's working now.
$this->Category->Behaviors->load('Containable');

CakePHP find() group and order by date complex conditions

I am trying to get messages with their corresponding users, in order to display a chat list of profiles with their last message in chronological order.
array(
0 => array(
'Recepient' => array(
'id' => ...
'name' => ...
...
),
'Message' => array(
'content' => ...
'created' => ...
...
)
),
1 => ...
)
and in order to retrieve the results, I've written this find() method:
$msgs = $this->Message->find('all', array(
'group' => array('Recepient.id'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
What I have:
message with corresponding "Recepient",
in chronological order
The problem:
the query DOES NOT retrieve the most recent message from $recepient_id/$sender_id combination.
So instead of list of users with the last message, I have a list of users with a message. What's wrong with my query? Thanks for help!
METHOD 2 results
I've created "chat_id" field in the database which is basically recepient_id+sender_id sorted alphabetically (because if user1 send user2 a message user1 is sender, later when user2 responds, he becomes the sender, so sorting will ensure two users will always have the same chat_id).
Than I added DISTINCT to the query:
$this->Message->recursive = 0;
$msgs = $this->Message->find('all', array(
'fields' => array('DISTINCT Message.chat_id','Message.*','Recepient.*'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
and it does NOT work! I am now getting multiple messages for the same conversation.
If I remove Message fields and Recipient fields from the query, I get correct amount of "chats".
'fields' => array('DISTINCT Message.chat_id'),
but that's not the solution.
CakePHP version 2.7.0
MySQL DB
METHOD 3 results
$msgs = $this->Message->find('all', array(
'order'=>'Message.created DESC',
'fields' => 'recepient_id, content, max(Message.created) as max_created',
'group'=>'recepient_id',
// 'contain' => array('Recepient'),
'conditions'=>array( 'chat_id' => $chats )
));
I gave up on single-find method to resolve this, so now 1.I am getting list of chats, 2.I want to find the last message from each chat.
Acording to http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ my find query should work. What happens is
(int) 0 => array(
'Message' => array(
'recepient_id' => '55e6d764-1444-4aad-a909-9042f07f76da',
'content' => '1st msg',
'created' => '2015-09-20 18:24:17',
'created_nice' => '2 hours ago'
),
(int) 0 => array(
'max_created' => '2015-09-20 18:24:28'
)
),
the field max(Message.created) is indeed showing the latest message from conversation but the 0=>Message array is for different message! As you can see $results[0]['Message']['created'] time and $results[0][0]['max_created'] are different!
Finally! Working solution:
$db = $this->Message->getDataSource();
$chats = $db->fetchAll(
'select id from (select * from messages order by created desc) Message
where recepient_id = "'.$pid.'" or sender_id = "'.$pid.'"
group by `chat_id`'
);
Above code will retrieve all messages, following the requirements. You can
a) SINGLE QUERY add mysql JOIN to include associated model (we keep the code single-query neat)
b) TWO QUERIES BUT SMARTER easier method, to select just "ids" of the messages and create another query that will have cake's build in containable behaviour. This might be better also because Behaviours will be applied.
You will have array tree as a result, to dig out the actual ids for next query use following:
$chats = Set::extract("/Message/id",$chats);
c) translate the solution to CakePHP query builder... :) up for a challenge?

Display Image next to HABTM checkboxes

Cane Somebody give me an ideas on this Please!
I Generate multiple checkboxes from a table that is related with another table with HABTM relationship association.
I would like to generate multiple checkboxes with an image along with the text in the label.
My two tables are items and items_characteristics. So an Item HasAndBelongToMany characteristics, and an ItemCharacteristic HasAndBelongToMany Items.
echo $this->Form->input('Item.ItemCharacteristic',array(
'label' =>false,
'type'=>'select',
'multiple'=>'checkbox',
'options' => $itemCharacteristics ,
'selected' => $this->Html->value('ItemCharacteristic.ItemCharacteristic')
));
This code generate the list of the checkboxes properly and works perfect:
This is what i have:
Which is generated from DB from the table items_characteristics.
And this is what i wanna have:
Does Anyone have any Idea how i can achieve this Please?
I assume that in your controller you did something like:
$this->request->data = $this->Item->find('first', ... );
so that $data contains the information about selected characteristics in form of a subarray,
edit: I also assume that Item habtm ItemCharacteristic
then in your view
$checked_characteristics = Hash::extract($this->data, 'ItemCharacteristic.{n}.id');
foreach($itemCharacteristics as $id => $itemCharacteristic )
{
$checked = in_array($id, $checked_characteristics );
$img = $this->Html->image('cake.icon.png'); // put here the name
// of the icon you want to show
// based on the charateristic
// you are displayng
echo $this->Form->input(
'ItemCharacteristic.ItemCharacteristic.',
array(
'between' => $img,
'label' => $itemCharacteristic,
'value' => $id,
'type' => 'checkbox',
'checked' => $checked
)
);
}
edit: from your comment I understand that $itemCharacteristics come from a find('list') statement.
change it into a find('all', array('recursive' => -1));
now your code becomes
foreach($itemCharacteristics as $itemCharacteristic )
{
$id = $itemCharacteristic['ItemCharacteristic']['id'];
$icon_name = $itemCharacteristic['ItemCharacteristic']['icon_name']; //or wherever you get your icon path
$img = $this->Html->image($icon_name);
$itemCharacteristicName = $itemCharacteristic['ItemCharacteristic']['name'];
// same as above
}

pagination; Results from multiple models

I have built a cake model that, when searched, needs to return paginated results that exclude some items based on data in another model.
I have a model called Box and a model called Item.
Each box can have 0 or more items, but I only want the boxes with 1 or more items with the category of Fruit to appear in a pagination result.
The Box model has a 'hasMany' association with the Item model.
The Item model has a field called 'is_friut'.
take care,
lee
This will make an inner join between the tables, only when the item is_fruit.
public $paginate = array(
'joins' => array(
array(
'table' => 'items',
'alias' => 'ItemJoin',
'type' => 'INNER',
'conditions' => array(
'ItemJoin.is_fruit' => 1
)
)
)
);

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.

Resources