Accessing more than one model deep relationships in Lithium - php

Is it possible to access more than one model deep in a relationship in Lithium?
For example, I have a User model:
class Users extends \lithium\data\Model {
public $validates = array();
public $belongsTo = array("City");
}
and I have a City model:
class Cities extends \lithium\data\Model {
public $validates = array();
public $belongsTo = array("State");
}
and a State model, and so on.
If I'm querying for a User, with something similar to Users::first(), is it possible to get all the relationships included with the results? I know I can do Users::first(array('with' => 'City')) but I'd like to have each City return its State model, too, so I can access it like this:
$user->city->state->field
Right now I can only get it to go one deep ($user->city) and I'd have to requery again, which seems inefficient.

Using a recent master you can use the following nested notation:
Users::all( array(
'with' => array(
'Cities.States'
)
));
It will do the JOINs for you.

I am guessing you are using SQL?
Lithium is mainly designed for noSQL db´s, so recursiveness / multi joins are not a design goal.
You could set up a native sql join query and cast it on a model.
query the city with Users and State as joins.
you could setup a db based join view and li3 is using it as a seperate model.
you probably should split your planned recursive call into more than one db requests.
Think about the quotient of n Cities to m States. => fetch the user with city and then the state by the state id. => pass that as two keys or embed the state info. This would be acceptable for Users::all() queries aswell.
Example using Lithiums util\Set Class:
use \lithium\util\Set;
$users = Users::all(..conditions..);
$state_ids = array_flip(array_flip(Set::extract($users->data(), '/city/state_id')));
$stateList = States::find('list',array(
'conditions' => array(
'id' => $state_ids
),
));

You can set up relationships in this way, but you have to use a more verbose relationship definition. Have a look at the data that gets passed when constructing a Relationship for details about the options you can use.
class Users extends \lithium\data\Model {
public $belongsTo = array(
"Cities" => array(
"to" => "app\models\Cities",
"key" => "city_id",
),
"States" => array(
"from" => "app\models\Cities",
"to" => "app\models\States",
"key" => array(
"state_id" => "id", // field in "from" model => field in "to" model
),
),
);
}
class Cities extends \lithium\data\Model {
public $belongsTo = array(
"States" => array(
"to" => "app\models\States",
"key" => "state_id",
),
);
}
class States extends \lithium\data\Model {
protected $_meta = array(
'key' => 'id', // notice that this matches the value
// in the key in the Users.States relationship
);
}
When using the States relationship on Users, be sure to always include the Cities relationship in the same query. For example:
Users::all( array(
'with' => array(
'Cities',
'States'
)
) );
I have never tried this using belongsTo relationships, but I have it working using hasMany relationships in the same way.

Related

Global belongsTo conditions defined in model not being applied (CakePHP v2.4.2)

Why are my global belongsTo conditions defined within a model not being applied?
I'm using CakePHP v2.4.2.
In model Order.php:
public $belongsTo = array(
...,
'Agent' => array(
'className' => 'Party',
'foreignKey' => 'agent_id',
'conditions' => array(...)
),
...
In controller OrdersController.php:
$agents = $this->Order->Agent->find('list');
In rendered view the following SQL statement is being applied:
SELECT `Agent`.`id`, `Agent`.`name` FROM `zeevracht2`.`parties` AS `Agent` WHERE 1 = 1;
I tried different conditions, but even a simple string containing true isn't being applied (while adding this condition to PartiesController.php within a $this->Order->Agent->find(); works fine:
$agents = $this->Order->Agent->find('list', array(
'conditions' => array('true')
));
leads to:
SELECT `Agent`.`id`, `Agent`.`name` FROM `zeevracht2`.`parties` AS `Agent` WHERE true;
After collaborating on IRC I found the answer on my own question.
It seems that conditions in the belongsTo condition of a model is only applied to the JOIN when querying an Order.
I was trying to filter Party on specific roles, e.g. Agent, which is an alias of a Party with a role as agent. So the association should be conditioned with a role set as agent. Ideally, this would automatically condition any $this->Order->Agent->find() calls. But unfortunately, this is not possible due to a technical issue which is being addressed in the development of version 3 of CakePHP.
The solution is to have two types of conditions on the association: one for the JOIN and one for the association itself.
Why one for the JOIN? E.g. when a Post belongs to User, but only post.validated should be displayed.
If you’re trying to find the Agent that a particular Order belongs to, then you should get the record back when querying orders. So something like:
<?php
class OrdersController extends AppController {
public function view($id) {
$order = $this->Order->findById($id);
pr($order); exit;
}
}
Should yield something like:
Array
(
[Order] => Array
(
[id] => 83
…
)
[Agent] => Array
(
[id] => 1
…
)
)
In your question where you then make an additional model call like $this->Order->Agent->find('list');, that’s making a new query, which will fetch all agents with no conditions. The conditions key, where you pass true, will have no affect, because that’s not a condition. Conditions should be an array, like this:
$this->Order->Agent->find('all', array(
'conditions' => array(
'Agent.id' => 1
)
));
But as I say, if your Order model belongs to your Agent model, then you should get an Agent result set back when you get your Order result set. If not, try adding the Containable behavior to your Order model:
<?php
class Order extends AppModel {
public $actsAs = array(
'Containable'
);
}
Try this:
public $belongsTo = array(
...,
'Agent' => array(
'className' => 'Party',
'foreignKey' => false,
'conditions' => array('joinField'=>'joinedField', ...more conditions)
),

Getting All groups that belong to user

I have HABTM for Users and Groups. I want to show in a Group view - all the Groups that belong to a User. Or to put it differently - all the Groups that have the User.
I am getting tangled in the MVC and am not able to figure it out. Here are my two models:
class Course extends AppModel
public $name = 'Course';
public $hasAndBelongsToMany = array('User' =>
array(
'unique' => false
)
);
And...
public $name = 'User';
public $hasAndBelongsToMany = array('Course' =>
array(
'unique' => true
)
);
The table name in the database is courses_users - this table houses group ids and user ids.
Should be simple enough but I'm new to CakePHP so I'd love some help. Thank you!
CakePHP has recursive set to 1 by default, which means that assuming you have not changed the recursive setting, it will automatically fetch all associated courses when you call find on a user, assuming you set up the HABTM relationship when doing the find. Therefore, all you have to do is:
$this->User->find('first', array('conditions' => array('User.id' => $this->Auth->user('id'))));
In your User model, I don't think it's strictly necessary, but I like to specify the join table and such on HABTM relationships:
public $hasAndBelongsToMany = array('Course' =>
array(
'unique' => true,
'dependent' => false,
'joinTable' => 'courses_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'course_id',
)
);
Keep in mind that in HABTM relationships, you don't ever really touch the joinTable beyond specifying which table to use as the joinTable when setting up the relationship. CakePHP will automatically do the rest of the work.

Pass foreign key from controller to model for specific query cakephp 2.3.9

I want to use two different foreign key for two different query
For my first query I want like:
my model code is like
public $belongsTo = array(
'Emailformatstype' => array(
'className' => 'Emailformatstype',
'foreignKey' => 'id'
)
);
now for my second query I want like:
my model code is like
public $belongsTo = array(
'Emailformatstype' => array(
'className' => 'Emailformatstype',
'foreignKey' => 'New_id'
)
);
So my question is is there any technique so I can pass foreignKey from controller for specific query
something like as we provide recursive
$this->Model->recursive = 0;
same I want like:
$this->Model->foreignKey= 'My_foreignKey';
Simply access the associations property:
$this->Model->belongsTo['YourAssoc']['foreignKey'] = 'my_foreignKey';
Some best practice: Emailformatstype is a bad name, this should be EmailFormatType. Reads better and matches the convention. Notice the plural you had before (formats) which would make it a join table by convention.

has_many through using custom table and field names

I'm using php.activerecord, and I am trying to link tables together. I'm not using their structure, but php.activerecord assumes I am, so it doesn't always work. I'm trying to use it on an already made app, so I can't change the database.
I learned from my previous question - Model association with custom table and key names - that I need to be as explicit as possible with the primary_key and foreign_key fields.
I'm having issues now using has_many through. I keep getting NULL, and I have no idea why.
So, here's a scenario: I have 3 tables, contacts, contactPrefs, and preferences. Those tables are as follows
contacts
--------
contactID
name
status
contactPrefs
------------
contactID
prefID
prefValue
preferences
-----------
prefID
name
description
Each contact has multiple contactPrefs. Each contactPrefs has one preferences. I tried to use has_many to get this working, but it's not. Here are my models:
Contacts.php:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
}
ContactPref.php:
<?php
class ContactPref extends ActiveRecord\Model {
static $table_name = 'contactPrefs';
static $belongs_to = array(
array(
'contact',
'foreign_key' => 'contactid',
'primary_key' => 'contactid'
),
array(
'preference',
'foreign_key' => 'prefid',
'primary_key' => 'prefid'
)
);
}
Preference.php:
<?php
class Preference extends ActiveRecord\Model {
static $primary_key = 'prefID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'class_name' => 'ContactPref'
)
);
}
According to the docs, I now should be able to the following:
<?php
var_dump(Contact::find(1234)->preference);
I cannot. I get NULL. Oddly, I can do this:
<?php
var_dump(Contact::find(1234)->prefs[0]->preference);
That works correctly. But, shouldn't I be able to access the preference object directly through the contact object? Am I misunderstanding the docs (they aren't the greatest, in my opinion)? Am I doing something wrong?
First you are reading the docs with a small flaw. In the docs you are shown:
$order = Order::first();
# direct access to users
print_r($order->users); # will print an array of User object
Which you are already doing via Contact::find(1234)->prefs. Let me boil it down a bit
$contact = Contact::find(1234);
# direct access to prefs
print_r($contact->prefs); # will print an array of ContactPref object
Second, what you actually want is undefined. What should Contact::find(1234)->preference actually do? Return the preference of the first ContactPref? Return an array of Preference objects?
I feel like offering both:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
public function get_preference() {
return isset($this->prefs[0])
? $this->prefs[0]->preference
: null
;
}
public function get_preferences() {
$preference=array();
foreach($this->prefs as $pref) {
$preference[]=$pref;
}
return $preference;
}
}
Let me explain a little bit what I have done. The ActiveRecord\Model class has a __get($name) function that looks for another function called get_$name, where $name in your case is preference (for the first result) and preference (for the entire collection). This means you can do Contact::find(1234)->preference which would be the same as doing Contact::find(1234)->prefs[0]->preference (but safer, due to the check) and Contact::find(1234)->preferences to get the entire collection of preferences.
This can be made better or optimized in numerous ways, so please don't take it as it is, but do try and adapt it to your specific situation.
For example you can either use the id of the preference as an index in the array or either not force a load of more data from ContactPrefs than the ones you are going to use and try a more intricate query to get the preference objects that you specifically need.
If I find a better implementation by getting through to work in the relationship definition, I'll return. But seeing the Unit Tests for active record, I'm skeptical.
There are several things that look strange, so it's not easy to come to a "this will fix it" for you, but this is an issue at least:
Fieldnames should always be lower-case in phpactiverecord. SQL doesn't mind it either way (not that table names ARE case-sensitive, but column names aren't). So make this:
static $primary_key = 'contactID';
into
static $primary_key = 'contactid';
The connections // find commands can be used in SQL, in which case it doesn't really matter how your key-string is 'cased', so some stuff works. But if the connection goes trough the inner-workings of phpmyadmin, it will fail. So check out this contactID but also the prefID.
Again, this goes only for COLUMN names, so don't go changing classnames or table-names to lowercase.
(extra point: phpmyadmin has trouble with combined primary keys. So while it might be ugly, you could add an extra row to your contactprefs table (if you don't allready have it) called id, to make that table actually have something to work with. It wouldn't give you much trouble, and it would help the activerecord library a lot)
Try the following:
<?php
var_dump(Contact::find(1234)->preferences);
The documentation says that with a has_many relationship, it should be referenced by a plural (http://www.phpactiverecord.org/projects/main/wiki/Associations#has_many_through). The Contact::find(1234) returns a Contact object which has multiple contactPrefs with their each Preference. In addition, in your Contact model, you specify the has_many as preferences .
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
Edit Through Modification:
Try the following Contact model
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'class_name' => 'ContactPref'
),
array('preferences',
'through' => 'prefs',
'class_name' => 'Preference',
'primary_key' => 'prefID')
);
}

CakePHP belongsTo multiple Models?

I apologize in advance if this question has been answered elsewhere; I searched high and low for an existing answer but came up empty. Perhaps I am looking with the wrong keywords.
I have three models, two (Student and Tutor)of which have a $hasMany relationship with a common model(Appointment) which has a $belongsTo relationship with both of the other models. Exact models are below...
A Student hasMany Appointments...
class Student extends AppModel {
var $hasMany = array (
'Appointment' => array (
'foreignKey' => 'student_id'
)
);
A Tutor hasMany Appointments...
class Tutor extends AppModel {
var $hasMany = array (
'Appointment' => array (
"foreignKey" => 'tutor_id'
)
);
An Appointment belongsTo a Student and a Tutor...
class Appointment extends AppModel {
var $belongsTo = array (
'Student' => array (
'foreignKey' => 'student_id'
),
'Tutor' => array (
'foreignKey' => 'tutor_id'
)
);
When looking at a student's record (e.g. 'app/student/view/4') I need to be able to list the dates of their upcoming appointments and the name of the tutor for that appointment. So I need to access data from the Tutor model (name) while looking at the Student model. Right now I have only been able to accomplish this using $this->Student->query() to pull the related tutor records, but this results in the student's information repeating in the resulting array.
All of the $hasMany and $belongsTo relationships work just fine. Does CakePHP have a built-in function that I am missing to get the related information, or does this need to be done longhand somewhere?
You should consider using Containable. In case you're not familiar with it, it's a Behavior which you can attach to models and use it to define precisely which associated models are to be included in a find, as many models and as deep as you like.
I always use it for all models, by setting it in AppModel:
var $actsAs = array('Containable');
Then in the controller:
$this->Student->find('first', array(
'conditions' => ...,
'contain' => array('Appointment' => 'Tutor'),
...
));

Categories