CakePHP - Just find related model data - cakephp

I have Users and Dashboards. They both have a HABTM relationship with each other. How do I just retrieve all of a user's dashboards, without any of the User data?
I tried this but it returns the User model as well:
$this->User->id = $this->Auth->user('id');
$this->User->find('all', array('contain' => 'Dashboard'));

You can set the fields option so that you retrieve only the id field and none of the others. (You probably can't set it to fetch no fields at all.)
$this->User->find(
'all',
array(
'contain' => 'Dashboard',
'fields' => 'User.id'
)
);

Use the Dashboard model instead with a condition for the user id:
$this->User->Dashboard->bindModel(array('hasMany' => array('DashboardsUsers')));
$this->User->Dashboard->find('all', array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'recursive' => -1
));
I think that should work :)

Related

Saving data with HABTM and HasMany looses foreignKey

I have a User model with two relations:
HasAndBelongsToMany
public $hasAndBelongsToMany = array(
'group' => array(
'className' => 'group',
'foreignKey' => 'user_id'
)
);
HasMany
public $hasMany = array(
'phonenumber' => array(
'className' => 'phonenumber',
'foreignKey' => 'user_id'
)
);
Phonenumber and Group have set
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
When I use
$this->saveAll(array(
'User' => $data,
'phonenumber' => $numbers, //array with data
'group' => $groups //array with data
)
);
The data gets saved in the tabels but User_id is "0" in phonenumber and group table.
How can I get the correct ID saved ? (CakePHP v 2.5)
FWIW saveAll() should work as advertised, populating the new user_id in the child tables in one fell swoop.
Have you paid attention to the relevant options: atomic & deep?
Especially if database does not support transactions, you'll need to pass in atomic:
$this->saveAll(array(
'User' => $data,
'phonenumber' => $numbers, //array with data
'group' => $groups //array with data
),
array('atomic' => false)
);
Considering the CakePHP documentation you will find this hint:
When working with associated models, it is important to realize that
saving model data should always be done by the corresponding CakePHP
model. If you are saving a new Post and its associated Comments, then
you would use both Post and Comment models during the save operation
(http://book.cakephp.org/2.0/en/models/saving-your-data.html#saving-related-model-data-hasone-hasmany-belongsto)
Based on that information I suggest you try the following:
$this->User->save($data); // first save the user
Assuming you have multiple numbers:
foreach($numbers as $key => $nbr) {
// set user_id related data
$numbers[ $key ]['Phonenumber']['user_id'] = $this->User->id;
}
Finally save your related data:
$this->User->Phonenumber->saveAll($numbers);
Since this code is untested you may need to take some adjustments. Always ensure to follow the Cake-Conventions and use CamelCase ModelNames.

cakephp find associated models of associated model

I am calling a find on a model called Book which is associated with a model Page(book_id)
However Page is associated with a model called Asset(page_id). I would like to get the array with all three models
Book
Page1
Asset1
Asset2
Asset3
Page2
Asset1
Asset2
Asset3
The code I have at the moment only get me Book and Page
$options = array(
'conditions' => array('Book.' . $this->Book->primaryKey => $id),
'contain' => 'Page'
);
$books = $this->Book->find('first', $options);
Book hasMany Pages
Page hasMany Assets
You can contain deeper associations, like it says in the docs
Example from the docs
$this->User->find('all', array(
'contain' => array(
'Profile',
'Account' => array(
'AccountSummary'
),
'Post' => array(
'PostAttachment' => array(
'fields' => array('id', 'name'),
'PostAttachmentHistory' => array(
'HistoryNotes' => array(
'fields' => array('id', 'note')
)
)
),
'Tag' => array(
'conditions' => array('Tag.name LIKE' => '%happy%')
)
)
)
));
Same thing with your models...
$options = array(
'conditions' => array('Book.' . $this->Book->primaryKey => $id),
'contain' => array('Page' => array('Asset')))
);
$books = $this->Book->find('first', $options);
Should work if your associations are set correctly (and if all models implement containable behavior).
EDIT
(to address the confusion the OP had)
The nested contain options works for the model expanding the array. For example, if models are associated like this
Model-A -> Model-B -> Model-C & Model-D
-> Model-E -> Model-C
you could get the entire array with data like
Model-A
Model-B1
Model-C1
Model-C2
Model-D2
Model-B2
Model-C (null)
Model-D3
Model-E1
Model-C1
Model-C3
using something like
$this->ModelA->find('all'), array(
'contain' => array(
'Model-B' => array('Model-C', 'Model-D'),
'Model-E' => array('Model-C')
)
);
Also, you can add options to the containable array, including the ones used for searching, like 'conditions' (though be careful with this, it means that if the model doesn't match the condition it will return a null array, it does not mean the the whole "Model-A" will not be in the returned data since one of the nested conditions was not fulfilled).

Include a subset of fields in the Auth User session

My Users table has a whole bunch of fields, most of which I don't need/want stored in the Auth User session. How do you restrict which fields are stored in the session for the logged in user?
I know you can choose fields of associated models with the 'contain' key, but normally to select fields of the top-level model, you'd use the 'fields' key. But in the case of Auth, the 'fields' key is used to choose which fields to authenticate the user by, not which fields to include in the session.
To give some context, here's my code so far... what would I do to make it so that only the email and firstname fields are stored in the Auth session, as opposed to all fields in the Users table.
$this->Auth->authenticate = array(
'Blowfish' => array(
'fields' => array(
'username' => 'email',
'password' => 'password',
)
)
);
I've upvoted the answers which were useful, albeit work-around solutions - thanks.
I think the "correct" answer is that there's no way to do this with CakePHP Auth component out of the box, and you have to hack it (eg, using one of the solutions below). I took a look at the _findUser method in BaseAuthenticate.php and it confirms this.
In case a CakePHP core dev is reading (DeEuroMarK?), this is probably a pretty common requirement, and I think it's a feature worth having built in.
Suggested implementation: just include the fields you want as extra fields in the 'fields' array - and just assume that every key other than 'username' and 'password' is an extra field that should be included in the auth session. That way it's consistent with other Model find syntax.
Example:
$this->Auth->authenticate = array(
'Blowfish' => array(
'fields' => array(
'username' => 'email',
'password' => 'password',
'another_field',
'yet_another_field'
)
)
);
in the beforeFilter of my UsersController I have something similar as your login.
Then I set a afterLogin function as the redirect
$this->Auth->loginRedirect = array('controller' => 'users', 'action' => 'afterLogin');
$this->Auth->loginRedirectTrue = array('controller' => 'users', 'action' => 'index');
$this->Auth->logoutRedirect = array('controller' => 'pages', 'action' => 'display');
the login function dus some checks and afterwards redirects to
if ($this->Auth->login()){
// code here
$this->redirect($this->Auth->redirect());
}
and afterLogin function like this
function afterLogin(){
$session = $this->Session->read('Auth');
$user_id = $session['User']['id'];
// change this to find only the fields you need and then override the Auth.User...
$user = $this->User->findById($user_id);
if (!empty($user)){
$this->Session->write('Auth.UserProfile', $user['UserProfile']);
}
$this->redirect($this->Auth->loginRedirectTrue);
}
You should change the findById to suit your needs and then override the Auth.User fields in the session.
Good Luck!
I think the simplest is to add something like this:
add a contain to your Auth-Component configuration
$this->loadComponent('Auth', [
'authorize' => 'Controller',
'loginRedirect' => [
'controller' => 'Users',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login'
],
'authenticate' => [
'Form' => [
'fields' => ['username' => 'email'],
'contain' => ['Groups']
]
],
'unauthorizedRedirect' => $this->referer()
]);
...
And in you login-action save the user in session:
$foundUser = $this->Auth->identify();
if ($foundUser) {
$this->Auth->setUser($foundUser);
}
...
this will add the containing groups to the Auth.User
The is for CakePhp3 - in older versions it may be different.

Associate different column name to one Model + cakephp

how to get name of (UserTransactionType.name) with Transaction.who_pay_fee_1,2,3 fields.
'user_transaction_type_id' works well but how to get the rest of fields work :(
//Transaction Model
public $belongsTo = array(
'UserTransactionType' => array(
'className' => 'UserTransactionType',
'foreignKey' => 'user_transaction_type_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
//UserTransactionType Model
public $hasMany = array(
'Transaction' => array(
'className' => 'Transaction',
'foreignKey' => 'user_transaction_type_id',
'dependent' => false,
))
This is the sample code for your controller:
$this->UserTransactionType->find('all',array(
'fields' => array('name'),
'contain' => array('Transaction')
)
);
If Models are associated you can specify in 'contain' which of them you want to get in the result.
If you want to have only some fields of related Model, you can determine them after 'Transaction' in 'contain' just like in the regular find() query:
'contain' => array('Transaction' => array('fields' => array('field_1',
'field_2') ))
But in your case, you don't need to specify fields, because by default you get all fields.
So no matter if you define or not fields "who_pay_fee_1,2,3" because if you use 'contain' by default you will get foreing_key - user_transaction_type_id.
I hope it's helpful
For people they like CakePhp :)
in Controller ->
get the list of 'UserTransactionType'
in View ->
after looping trough all the transactions; in Transaction Status column simply load the 'UserTransactionType'array and assign the number of array to $userTransactionTypes.
$userTransactionTypes[$transaction['Who_pay_fee_1']];
To be honest it was straight forward but needed a bit concentration :)

Containable behavior doesn't return deeper model association and selecting fields

I want to limit the fields returned by a deeper association using containable.
My associations:
Game hasMany Review
The paginate and containable code:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Game' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
),
)
);
$games = $this->paginate('Game');
Currently, all of the fields in the Review table are returned. 'ROUND(AVG(Review.score),1)' is never returned. How can I specify what fields I want returned from the Review association?
SQL dumps for two search results using #theJetzah's answer. The first is a search with one game as a result and the second is a search returning three games.
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` = (55)
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` IN (55, 56, 57)
Not a full answer, but an attempt to get it working :)
Approach1 (UPDATE: Containable doesn't support 'group by')
First of all, try to add the 'Game' model to the $uses array of your Controller, if it is not included yet, and re-organise the pagination array (as previously suggested by Sam), so that you'll be pagination the Game model itself.
Then, It may help to create a virtual field for the calculated score, but the results of 'Review' need to be grouped, otherwise you'll not be able to calculate the average score.
I'm not able to test this, but it may worth trying
something like this;
public $uses = array(
'Game',
// other models
);
public function myfunction()
{
$this->Game->Review->virtualFields['average_score'] = 'ROUND(AVG(Review.score),1)';
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array(
'Review.game_id,
'Review.average_score',
),
'group' => array(
'Review.game_id,
),
)
)
)
);
// Conditions can be passed to paginate,
// that way you can specify 'paginate' at
// one place and don't have to modify it
// to include the conditions
$games = $this->paginate('Game', $conditions);
}
Alternative approach: Using joins and a database-view
Apparently, the Containable behavior doesn't like group-by clauses; See this ticket for more information: Containable behavior does not implement 'group' option
CakePHP allows you to manually specify a join: Joining Tables
To simplify things and to prevent having to add a 'group by' for all fields, create a simple database-view in your database;
CREATE VIEW review_scores AS
SELECT
game_id,
ROUND(AVG(score),1) AS average_score,
COUNT(id) AS total_reviews
FROM
reviews
GROUP BY
game_id;
If you're unfamiliar with this; a database 'view' is basically a 'stored query', which can be accessed as if it was a regular table. See Create View
Then, use a 'manual' join, using the newly created database-view as the source-table. In your case, this will look something like this;
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name',
'ReviewScore.average_score',
'ReviewScore.total_reviews',
),
'joins' => array(
array(
'table' => 'review_scores',
'alias' => 'ReviewScore',
'type' => 'LEFT',
'conditions' => array(
'ReviewScores.game_id = Game.id',
)
)
)
)
);
Hope this helps
I think your array is a configured a little wrong, try:
$this->paginate = array(
'Game' => array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
)
)
);
$games = $this->paginate('Game');
As an aside, from personal experience, specifying the fields in a query doesn't always speed it up (certainly for small number of fields), assuming this is the motive for doing so. It does reduce memory occupancy but this is only relative to original size of the record and the number of records returned.

Resources