join belongsTo in find method - cakephp

Score belongs to Player:
class Score extends AppModel {
public $name = 'Answer';
public $belongsTo = array('Player');
}
In PlayersController, I want to get player scores, with his details.
Question #1: How to include belongsTo model in find method result? (join it)
Question #2: How to get sum of all scores distance (Score.distance) which belongs to that player? (I mean SUM(Score.distance), group by Score.player_id)
Note for Q1: Because each player has a lot scores, I don't like to join scores in each find method I use in that controller. I want to get them in just 1 action)

Cakephp automagic not actually join tables as you can see in sql_dump in your layout at bottom.
You will see below queries.
SELECT `Player`.`id`, `Player`.`name`, `Player`.`created` FROM `players` AS `Player` WHERE `Player`.`id` = 10 GROUP BY `Player`.`id`
SELECT `Score`.`id`, `Score`.`player_id`, `Score`.`scores` FROM `scores` AS `Score` WHERE `Score`.`player_id` = (10)
Quite clear that its not actually join table so you need to do it following way.
And following will help you because i have already tested it.
In Player Controller you need to unbind score model first and then custom code to join score table as below.
In player model create one virtual field as below.
<?php
class Player extends AppModel
{
var $name = 'Player';
var $displayField = 'name';
var $virtualFields = array
(
'Distance' => 'SUM(Score.scores)'
);
var $hasMany = array
(
'Score' => array
(
'className' => 'Score',
'foreignKey' => 'player_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
);
}
?>
For testing i manually added conditions to find player number 10 scores
<?php
class PlayersController extends AppController {
var $name = 'Players';
function index()
{
$this->Player->unbindModel(array
(
'hasMany' => array
(
'Score'
)
));
$this->Player->bindModel(array
(
'belongsTo' => array
(
'Score' => array
(
'foreignKey' => false,
'conditions' => array
(
'Player.id = Score.player_id'
)
)
)
));
$order = "Player.id";
$direction = "asc";
if(isset($this->passedArgs['sort']) && $this->passedArgs['sort']=="Distance")
{
$order = $this->passedArgs['sort'];
$direction = $this->passedArgs['direction'];
unset($this->passedArgs['sort']);
unset($this->passedArgs['direction']);
}
$this->pagination = array();
$this->pagination = array
(
'conditions' => array
(
'Score.player_id' => 10
),
'fields' => array
(
'Player.*',
'SUM(Score.scores) AS Distance'
),
'group' => array
(
'Score.player_id'
),
'limit' => 15,
'order'=>$order." ".$direction
);
$playersScore = $this->paginate('Player');
}
}
?>
And the resulting array would be looks like below.
Array
(
[0] => Array
(
[Player] => Array
(
[id] => 10
[name] => Dexter Velasquez
[created] => 2012-08-02 12:03:07
[Distance] => 18
)
)
)
For testing i have used Generate Mysql Data.
for sorting link
<?php $direction = (isset($this->passedArgs['direction']) && isset($this->passedArgs['sort']) && $this->passedArgs['sort'] == "Distance" && $this->passedArgs['direction'] == "desc")?"asc":"desc";?>
and display above link as below.
<?php echo $this->Paginator->sort('Distance','Distance',array('direction'=>$direction));?>

So, Player has many Scores...
class Player extends AppModel {
public $name = 'Player';
public $hasMany = array('Score');
}
... which means your find() in PlayersController can be something like...
$players = $this->Player->find(
'all',
array(
'contain' => array('Score'), // <-- If you're not using Containable Behavior, remove.
'conditions' => array('Player.id' => $id), // <-- Change or remove as necessary.
'fields' => array(
'Player.id',
'Player.name',
'Sum(Score.distance) as sum_dist',
),
'group' => array(
'Player.id',
'Player.name',
),
'order' => array(
'Player.name',
),
)
);
Hopefully you get the idea.

Related

link between model cake php

I try to use CakePhp but i don't find the solution. Here is my problem :
I have 2 models ant i try to link it.
The First is Player (which mysql table is players)
The second is Toto (which mysql table is totos)
I have a field un players called toto_id
And in my model Player i wrote :
<?php
class Player extends AppModel {
public $validate = array(
'name' => array(
'rule' => 'notEmpty'
)
);
public $hasOne = 'Toto';
}
And in my controller PlayersController
class PlayersController extends AppController {
public $helpers = array('Html', 'Form', 'Session');
public $components = array('Session');
public function index() {
$params = array(
'order' => array("Player.firstname")
);
$a = $this->Player->find('all', $params);
print_r($a);
$this->set('liste', $a);
}
}
And my print_r displays
Array
(
[0] => Array
(
[Player] => Array
(
[id] => 2
[name] => DESMAISON
[firstname] => Christophe
[is_intern] => 1
[score] => 663
[created] => 2014-01-05
[modified] => 0000-00-00 00:00:00
[toto_id] => 2
)
)
[1] => Array
(
[Player] => Array
(
[id] => 10
[name] => CHARLERY
[firstname] => Daphné
[is_intern] => 1
[score] => 572
[created] => 2014-01-04
[modified] => 0000-00-00 00:00:00
[toto_id] => 0
)
)
I don't understand why in my array i haven't a reference to Toto. Someone can help me ?
Thank you for your help
Ps : i use cakephp 2.4.4 PHP 5.4.7
I think this is BelongsTo Association .
Instead of HasOne association try to use BelongsTo,
class Invoice extends AppModel {
public $belongsTo = array(
'Toto' => array(
'className' => 'Toto',
'foreignKey' => 'toto_id'
),
);
if you want to retrieve data from database you have to set your recursivity and there is two ways first one :
$players = $this->Player->find('all',array('recursive'=>2));
// or
$this->Player->recursive = 2;
$player = $this->Player->find('all');

CakePHP 2.x containable behavior conditions on deep associations

I have model relations like this:
Project hasMany SubProject hasMany Item
I want to set up a containable array so that I can find all of the Items which belong to a particular Project, and paginate the results. So, in my ItemsController I have:
public $paginate = array(
'Item' => array(
'limit' => 10,
'order' => array('
'Item.create_time' => 'desc'
),
'contain' => array(
'SubProject' => array(
'Project'
)
)
)
);
Somewhere, obviously, I need to place a condition like "SubProject.project_id = $pid", but nothing I've tried yields the correct results. The best I can manage is results that look like this:
Array
(
[0] => Array
(
[Item] => Array
(
[id] => 13
[file_name] => foo.tar.gz
.... other keys ...
[create_time] => 2013-01-23 14:59:49
[subProject_id] => 4
)
[SubProject] => Array
(
[id] => 4
[name] => foo
[project_id] => 2
..... other keys ....
[Project] => Array
(
)
)
)
[1] => Array
.....
Edit: It is quite correctly omitting the Project record that doesn't match; I want to skip any Item records with out a matching Project record.
It has crossed my mind to manually specify my joins, but I feel like that shouldn't be necessary.
It seems like this should be obvious, but alas, the solution escapes me.
I did eventually solve this problem, so I thought I'd explain what I did in the hope it might help someone else.
After reading this blog post by Mark Story (which is from the days of 1.2 but still relevant) I decided that the thing to do was create a custom find type in my Item model that binds the Project model directly. This gives a first-level association that Containable can filter correctly.
So, in the Items model, I have something like the following (see the documentation on custom find types).
public $findMethods = array('byProject' => true);
public function _findByProject($state, $query, $results=array()) {
if ($state == 'before') {
$this->bindModel(array(
'hasOne' => array(
'Project' => array(
'foreignKey' => false,
'conditions' => array('Project.id = SubProject.project_id')
)
)
));
return $query;
}
return $results;
}
Note that setting foreignKey to false is necessary to prevent CakePHP from trying to automatically use a non-existent database key. In the ItemsController, the pagination options now look like this:
public $paginate = array(
'Item' => array(
'findType' => 'byProject',
'limit' => 10,
'order' => array(
'Item.create_time' => 'desc'
),
'contain' => array(
'SubProject',
'Project'
),
'conditions' => array('Project.id' = $pid)
),
);
...where $pid is the id of the project to display. Some minor tweaks in the view code to accomodate the slightly different results array structure, and I was all set.
EDIT ===============
public $paginate = array(
'limit' => 10,
'order' => 'Item.create_time DESC', //'order' => array(''Item.create_time' => 'desc'),
'contain' => array(
'SubProject' => array(
'Project' => array(
'conditions' => array(
'id' => $project_id // Passed parameter
)
)
)
)
);
=================================================
Have you tried using conditions as in the following? Also, I did not write the 'order' section of the code the way you have it.
public $paginate = array(
'Item' => array(
'limit' => 10,
'order' => 'Item.create_time DESC', //'order' => array(''Item.create_time' => 'desc'),
'contain' => array(
'SubProject' => array(
'Project' => array(
'conditions' => array(
'id' => $project_id // Passed parameter
)
)
)
)
)
);

CakePHP Many To Many With Conditions

I'm newbie on CakePHP, and now I'm stuck on many to many situation
ok, i have 3 Table :
questions
with fields (id, question)
question_product
with fields (id, question_id, product_id, question_number, is_enabled)
products
with fields (id, name, code, is_enabled)
so when i want to select questions with specific field, i don't know how to fix it
for now, my code is like this :
Question.php (Model)
class Question extends AppModel {
public $hasAndBelongsToMany = array (
'Product' => array (
'joinTable' => 'question_product',
'foreignKey' => 'question_id',
'associationForeignKey' => 'product_id',
'unique' => 'keepExisting',
'order' => 'question_number',
'fields' => array (
'QuestionProduct.question_number',
'Product.id',
'Product.name'
),
'conditions' => array (
'QuestionProduct.is_enabled' => 1,
)
)
);
}
QuestionsController.php (Controller)
public function loadQuestions($productId) {
$this->view = 'load_questions';
$questions = $this->Question->find('all', array (
'fields' => array (
'Question.id',
'Question.question',
'Question.is_optional',
'Question.reason_optional',
'Question.text_size'
),
'conditions' => array (
'QuestionProduct.product_id' => $productId
)
));
$this->set($questions);
}
method loadQuestions have one parameter to select with specified product
if i using sql query, it will be like this
select all from Question with condition Product.product_id=4, sorted by QuestionProduct.question_number ascending
select questions.*
from questions
join question_product on questions.id=question_product.question_id
join products on products.id=question_product.product_id
where products.id=4
order by question_product.question_number;
any answer will be appreciated :)
Thanks !
Any time you use a many-many (HABTM) relation with any other field that requires conditions, it is no longer many-many as far as Cake is concerned. You want the HasManyThrough relationship
Instead of using hasAndBelongsToMany relation, use two belongsTO relation from question_product to questions and another time from question_product to products.
question_product belognsTo questions
question_product belongsTo products
NOTE:you should change the table name from question_product to question_products as cakePHP convention
in your model QuestionProduct model :
<?php
// declares a package for a class
App::uses('AppModel', 'Model');
class QuestionProduct extends AppModel {
/**
* #see Model::$actsAs
*/
public $actsAs = array(
'Containable',
);
/**
* #see Model::$belongsTo
*/
public $belongsTo = array(
'Product' => array(
'className' => 'Product',
'foreignKey' => 'product_id',
),
'Question' => array(
'className' => 'Question',
'foreignKey' => 'question_id',
),
);
then in your Controller :
public function loadQuestions($productId) {
$this->view = 'load_questions';
$questions = $this->QuestionProduct->find('all', array (
'fields' => array (
'Question.id',
'Question.question',
'Question.is_optional',
'Question.reason_optional',
'Question.text_size'
),
'conditions' => array (
'QuestionProduct.product_id' => $productId
),
'contain' => array('Product','Question')
));
$this->set($questions);
}
It should make exactly the query you want, and I don't think it has any other way to produce that query.

Cakephp IN(x,x) with 'AND'

I am struggeling with a custom find in cakephp 2.1.
In my model I have this function:
public function findByGenres($data = array()) {
$this->Item2genre->Behaviors->attach('Containable', array('autoFields' => false));
$this->Item2genre->Behaviors->attach('Search.Searchable');
$query = $this->Item2genre->getQuery('all', array(
'conditions' => array('Genre.name' => $data['genre']),
'fields' => array('item_id'),
'contain' => array('Genre')
));
return $query;
}
This returns the following query:
SELECT `Item`.`id` FROM `items` AS `Item`
WHERE `Item`.`id`
IN(SELECT `Item2genre`.`item_id` FROM `item2genre` AS Item2genre
LEFT JOIN `genres` AS Genre ON(`genre_id` = `Genre`.`id`)
WHERE `Genre`.`name`
IN ('Comedy', 'Thriller')
)
The result of the query returns Items with either 'Comedy' or 'Thriller' genre associated to them.
How can I modify the query to only return Items with 'Comedy' AND 'Thriller' genre associated to them?
Any suggestions?
edit:
content of data is:
'genre' => array(
(int) 0 => 'Comedy',
(int) 1 => 'Thriller'
)
You would want your 'conditions' key to be this:
'conditions' => array(
array('Genre.name' => 'Comedy'),
array('Genre.name' => 'Thriller')
)
So specifically to your problem your $data['genre'] is array('Comedy', 'Thriller'). So you could create a variable that has contents similar to what you need by doing:
$conditions = array();
foreach ($data['genre'] as $genre) {
$conditions[] = array('Genre.name' => $genre);
}

Second Level Expansion?

First of all, sorry if this sounds too simple. I'm new to cakePHP by the way.
My objective is to expand the team_id in season controller where it linked thru standings table.
My current model rule are:
Each season has many standings
Each standings denotes a team.
Basically is a many to many relation between season and team = standings.
Here's my SeasonController:
class SeasonsController extends AppController
{
public $helpers = array('Html', 'Form');
public function view($id)
{
$this->Season->id = $id;
$this->set('season', $this->Season->read());
}
}
When I browse a single season thru /cakephp/seasons/view/1, the $season array shows:
Array
(
[Season] => Array
(
[id] => 1
[name] => English Premier League 2013/2014
[start] => 1350379014
[end] => 0
)
[Standing] => Array
(
[0] => Array
(
[id] => 1
[team_id] => 1
[win] => 1
[lose] => 0
[draw] => 1
[GF] => 5
[GA] => 4
[season_id] => 1
[rank] => 1
)
[1] => Array
(
[id] => 2
[team_id] => 2
[win] => 0
[lose] => 1
[draw] => 1
[GF] => 4
[GA] => 5
[season_id] => 1
[rank] => 2
)
)
)
The result only shows team_id, but how can I get further expansion for team_id?
I have put below in my Team's model, but still doesn't auto linked. Below are all my models:
class Team extends AppModel
{
public $name = 'Team';
public $hasMany = array(
'Standing' => array(
'className' => 'Standing'
));
}
class Season extends AppModel
{
public $name = 'Season';
public $hasMany = array(
'Standing' => array(
'className' => 'Standing'
));
}
class Standing extends AppModel
{
public $name = 'Standing';
public $belongsTo = array(
'Team' => array(
'className' => 'Team',
'foreignKey' => 'team_id',
),
'Season' => array(
'className' => 'Season',
'foreignKey' => 'season_id',
),
);
}
TLDR:
Use CakePHP's [Containable] behavior.
Explanation & Code example:
"Containable" lets you "contain" any associated data you'd like. Instead of "recursive", which blindly pulls entire levels of data (and can often cause memory errors), "Containable" allows you to specify EXACTLY what data you want to retrieve even down to the field(s) if you'd like.
Basically, you set your model to public $actsAs = array('Containable'); (I suggest doing that in your AppModel so Containable is available for all models). Then, you pass a "contain" array/nested array of the associated models you want to retrieve (see below example).
After reading about it and following the instructions, your code should look similar to this:
public function view($id = null)
{
if(empty($id)) $this->redirect('/');
$season = $this->Season->find('first', array(
'conditions' => array(
'Season.id' => $id
),
'contain' => array(
'Standing' => array(
'Team'
)
)
);
$this->set(compact('season'));
}
Once you get more comfortable with CakePHP, you should really move functions like this into the model instead of having them in the controller (search for "CakePHP Fat Models Skinny Controllers"), but for now, just get it working. :)
Use :
$this->Season->id = $id;
$this->Season->recursive = 2;
$this->set('season', $this->Season->read());

Resources