'Through' association with 'foreign_key' & 'model' fields in CakePHP 3 - php

This is a pretty standard thing that I've done probably 600 times in CakePHP 2, but for the life of me, I can't get it to work in CakePHP 3.
I have Videos, Photos, Articles. I also have Categories.
Videos, Photos, and Articles can all belong to one or more Categories.
The goal of the current problem is to pull videos that are under a certain category.
So, I tried this:
// VideosTable
$this->belongsToMany('Categories', [
'joinTable' => 'categorized',
'className' => 'Categorized',
'foreignKey' => 'foreign_key',
'conditions' => [
'Categorized.model' => 'Videos',
]
]);
public function getTopVideosByCategory($categorySlug)
{
return $this->Categories->find('all')
->where(['Categories.slug' => $categorySlug])
->contain([
'Videos' => function ($q) {
return $q
->limit(8)
->contain([
'Tags',
'Categories' // tried with and without this
])
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
]);
}
])
->first();
}
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'Categorized.model' in 'where clause'
I've tried a number of other ways including creating the join table's model, and a few others, but keep getting errors. I've tried with every option, and with limited number of options. I've tried using an actual Table class, and I've tried a pseudo one (like "Categorized" above).
I have to assume this is pretty standard, but can't find an example in the book, and I just can't seem to get it to work.
Edit:
I've also tried this:
//VideosTable
public function initialize(array $config)
{
$this->belongsToMany('Categories', [
'through' => 'Categorized',
'conditions' => [
'Categorized.model' => $this->alias(),
]
]);
}
public function getTopVideosByCategory($categorySlug)
{
return $this->find('all')
->matching('Categories', function ($q) use ($categorySlug) {
return $q
->where(['Categories.slug' => $categorySlug]);
})
->contain([
'Tags',
'Categories'
])
->limit(8)
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
])
->first();
}
But get this error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'Categorized.model' in 'on clause'

Since Videos and Categories is not a 1-1 o n-1 (hasOne or belongsTo), it is impossible to build a SQL expression that can include conditions for the other table. For those cases, CakePHP implements the matching() function. It works similar to contain() but what it does is using an INNER join to get the data from the external associations:
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#filtering-by-associated-data
You can also look an an example of using it here:
http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html#creating-the-finder-method

I ended up getting it to work like this:
class VideosTable extends Table
{
public function initialize(array $config)
{
$this->hasMany('Categorized', [
'foreignKey' => 'foreign_key',
'conditions' => [
'Categorized.model' => $this->alias(),
]
]);
}
public function getTopVideosByCategory($categorySlug)
{
return $this->find()
->matching(
'Categorized.Categories', function ($q) use ($categorySlug) {
return $q
->where(['Categories.slug' => $categorySlug]);
})
->limit(8)
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
])
->all();
}
I've up-voted José's answer, as it led me down the road of figuring it out, but will mark this as the answer, as I think it more-quickly helps users trying to figure this particular problem out.
José, if you want to append this (with any tweaks you see fit) to your answer, I'll change the marked answer to yours.

It seem that what you want is:
$this->belongsToMany('Categories', [
'through' => 'Categorized',
'conditions' => ['Categorized.model' => $this->alias()]
]);

Related

How to fetch associated belongsToMany entities with CakePHP3

I have Users and Courses table with belongsToMany relation. UserTable has
$this->belongsToMany('Courses', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'course_id',
'joinTable' => 'courses_users'
]);
and CoursesTable has
$this->belongsToMany('Users', [
'foreignKey' => 'course_id',
'targetForeignKey' => 'user_id',
'joinTable' => 'courses_users'
]);
Now, I want to fetch courses with user_id. In my CoursesController, I tried
public function myCourses()
{
$id = $this->Auth->user('id');
$courses = $this->Courses->find('all',
['contain' => ['Users'],
'condition' => ['Courses.user_id' => $id]
]);
$this->set('courses', $courses);
}
when I debug($courses) with this code, I got '(help)' => 'This is a Query object, to get the results execute or iterate it.' message. I'm searching information and trying to do it for many hours but I can't make it. How can I fetch Courses datas with user_id? Thanks in advance.
If it's a has-and-belongs-to-many (HABTM) association with a join table of courses_users, you shouldn't even have a user_id field in your Courses table.
So now that we've determined you can't do what you were trying (Courses.user_id), we can look at what you thought you were trying:
$courses = $this->Courses->find('all',
['contain' => ['Users'],
//'condition' => ['Courses.user_id' => $id]
]);
This says "find all courses and any users that are associated with those courses".
But what you really WANT (I believe) is: "find all courses that belong to this specific user".
To do that, you'll want to use an matching() instead.
According to the CakePHP book:
A fairly common query case with associations is finding records
‘matching’ specific associated data. For example if you have ‘Articles
belongsToMany Tags’ you will probably want to find Articles that have
the CakePHP tag. This is extremely simple to do with the ORM in
CakePHP:
$query = $articles->find();
$query->matching('Tags', function ($q) {
return $q->where(['Tags.name' => 'CakePHP']);
});
So in your case, it would be something like this:
$query = $courses->find();
$query->matching('Users', function ($q) use ($id) {
return $q->where(['Users.id' => $id]);
});

Why am I getting a “Not Associated” error even though the HABTM association is in place?

I'm making some tournament scoring software for my bike polo league. The goal is to match up teams in such a way that everyone gets a chance to play every other team before repeat matches start.
In my Match Entity class, I have a function called createMatches that should find all teams and associated matches. There is a HABTM relationship between matches and teams, with a join table. This relationship works fine - I've selected teams (contain matches) and matches (contain teams) in several controller methods, saved associations through forms, and so on. But even so, in this Entity function, I get the error "Teams is not associated with Matches. Could this be caused by using Auto-Tables? ...." and so on. Can anyone tell me what's wrong?
Here's the method in question.
public function createMatches($tournamentId) {
$team = TableRegistry::get('Teams', ['contain' => ['Matches']]);
$teams = $team->find('all', ['conditions' =>
['tournament_id' => $tournamentId],
'contain' =>
['Matches' =>
['fields' =>
['Matches.id']
]]]);
return $teams;
}
Here's the init function from Match.php:
public function initialize(array $config)
{
parent::initialize($config);
$this->table('matches');
$this->displayField('id');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Rounds', [
'foreignKey' => 'round_id',
'joinType' => 'INNER',
'className' => 'SwissRounds.Rounds'
]);
$this->belongsToMany('Teams', [
'foreignKey' => 'match_id',
'targetForeignKey' => 'team_id',
'joinTable' => 'matches_teams',
'className' => 'SwissRounds.Teams'
]);
}
Here's the init function from Team.php:
public function initialize(array $config)
{
parent::initialize($config);
$this->table('teams');
$this->displayField('name');
$this->primaryKey('id');
$this->hasMany('Players', [
'foreignKey' => 'team_id',
'className' => 'SwissRounds.Players'
]);
$this->belongsToMany('Matches', [
'foreignKey' => 'team_id',
'targetForeignKey' => 'match_id',
'joinTable' => 'matches_teams',
'className' => 'SwissRounds.Matches'
]);
}
I don't believe I've touched either of those functions - they were both generated by cake bake.
Error message is quite exact, but not descriptive. It says that Teams is not associated with Matches.
Well, I tested this with minimal setup, with only difference, that I did not have my tables inside plugins. And no errors detected. So, I am pretty sure that CakePHP cant find your table classes inside SwissRounds -plugin for some reason.
If your tables are in a SwissRounds plugin, you should use plugin notation for the associations: $this->belongsToMany('SwissRounds.Teams', [... and $this->belongsToMany('SwissRounds.Matches', [...

Eloquent automatically selects unwanted columns upon eager-loading model relationship

I am converting an internal API from HTML (back-end) processing to JSON (using Knockout.js) processing on the client-side to load a bunch of entities (vehicles, in my case).
The thing is our database stores sensitive information that cannot be revelead in the API since someone could simply reverse engineer the request and gather them.
Therefore I am trying to select specifically for every relationship eager-load the columns I wish to publish in the API, however I am having issues at loading a model relationship because it seems like Eloquent automatically loads every column of the parent model whenever a relationship model is eager loaded.
Sounds like a mindfuck, I am aware, so I'll try to be more comprehensive.
Our database stores many Contract, and each of them has assigned a Vehicle.
A Contract has assigned an User.
A Vehicle has assigned many Photo.
So here's the current code structure:
class Contract
{
public function user()
{
return $this->belongsTo('User');
}
public function vehicle()
{
return $this->belongsTo('Vehicle');
}
}
class Vehicle
{
public function photos()
{
return $this->hasMany('Photo', 'vehicle_id');
}
}
class Photo
{
[...]
}
Since I need to eager load every single relationship listed above and for each relationship a specific amount of columns, I need to do the following:
[...]
$query = Contract::join('vehicles as vehicle', 'vehicle.id', '=', 'contract.vehicle_id')->select([
'contract.id',
'contract.price_current',
'contract.vehicle_id',
'contract.user_id',
'contract.office_id'
]);
[...]
$query = $query->with(['vehicle' => function ($query) {
$query->select([
'id',
'trademark',
'model',
'registration',
'fuel',
'kilometers',
'horsepower',
'cc',
'owners_amount',
'date_last_revision',
'date_bollo_expiration',
'bollo_price',
'kilometers_last_tagliando'
]);
}]);
$query = $query->with(['vehicle.photos' => function ($query) {
$query->select([
'id',
'vehicle_id',
'order',
'paths'
])->where('order', '<=', 0);
}]);
$query = $query->with(['user' => function ($query) {
$query->select([
'id',
'firstname',
'lastname',
'phone'
]);
}]);
$query = $query->with(['office' => function ($query) {
$query->select([
'id',
'name'
]);
}]);
[...]
return $this->response->json([
'error' => false,
'vehicles' => $vehicles->getItems(),
'pagination' => [
'currentPage' => (integer) $vehicles->getCurrentPage(),
'lastPage' => (integer) $vehicles->getLastPage(),
'perPage' => (integer) $vehicles->getPerPage(),
'total' => (integer) $vehicles->getTotal(),
'from' => (integer) $vehicles->getFrom(),
'to' => (integer) $vehicles->getTo(),
'count' => (integer) $vehicles->count()
],
'banner' => rand(0, 2),
'filters' => (count($input) > 4),
'filtersHelpText' => generateSearchString($input)
]);
The issue is: if I do not eager load vehicle.photos relationship, columns are loaded properly. Otherwise, every single column of Vehicle's model is loaded.
Here's some pictures so you can understand:
Note: some information have been removed from the pictures since they are sensitive information.
You can set a hidden property on your models which is an array of column names you want to hide from being output.
protected $hidden = ['password'];

How to paginate joined many-to-many table with where clause in cakephp?

I have two tables that is joined by another one. tables are:
PostsTable:
$this->belongsToMany('Categories', [
'through' => 'CategoryPost'
]);
CategoriesTable:
$this->belongsToMany('Categories', [
'through' => 'CategoryPost'
]);
CategoryPostTable:
$this->belongsTo('Categories', [
'foreignKey' => 'category_id'
]);
$this->belongsTo('Posts', [
'foreignKey' => 'post_id'
]);
I want to show posts that are in a specific category. for example posts in "design" category.
the route is defined as:
$routes->connect('/blog/archive/:safe_name', ['controller' => 'Posts', 'action' => 'category'], ['pass' => ['safe_name']]);
The category action in Posts controller is defined like this:
class PostsController extends AppController
{
...
public function category($safe_name = null)
{
$this->paginate = [
'contain' => ['Photos', 'Categories']
];
$posts = $this->Posts->find()->matching('Categories', function ($q) {
return $q->where(['Categories.safe_name' => $safe_name]);
});
$this->set('posts', $this->paginate($posts));
$this->set('_serialize', ['posts']);
}
...
}
but what I get is:
Undefined variable: safe_name [APP/Controller\PostsController.php, line 188
could anyone help me about this problem! and how can I do that?
sorry for bad English.
and BTW my cakephp version is 3.0.
Closures in php do not inherit variables in a higher scope
The code responsible for the error message is this:
$posts = $this->Posts->find()->matching('Categories', function ($q) {
return $q->where(['Categories.safe_name' => $safe_name]);
});
Because within the closure, the variable $save_name isn't defined. To fix that error, use use
$posts = $this->Posts->find()->matching('Categories', function ($q) use ($safe_name) {
return $q->where(['Categories.safe_name' => $safe_name]);
});

CakePHP recursive not working with conditions

I have a problem with querying associated data from a Model in CakePHP. I wrote an example to show the behavior:
TestController.php:
class TestController extends AppController
{
public $uses = array(
'User',
'Upload',
'Detail'
);
public function test(){
$result = $this->Upload->find('all', array(
'recursive' => 2,
'conditions' => array('Detail.id' => 1)
));
print_r($result);
}
}
Upload.php:
class Upload extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
Detail.php:
class Detail extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
User.php:
class User extends AppModel {
public $hasOne = 'Detail';
public $hasMany = array(
'Upload' => array(
'className' => 'Upload',
'foreignKey' => 'user_id',
)
);
}
When I remove the condition I get back an array with Details included. But with the condition I get the following error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Detail.id' in 'where clause'
Looking at the SQL Queries it seems like he is not joining the tables correctly when I add the condition. Without the condition he is joining all three tables.
Is this a bug in CakePHP or am I doing anything wrong?
No, it is not a bug with CakePHP. It's simply the way it's designed, using conditions during a find on associated models will often create an invalid query. You should be using containable behavior or manually joining to use conditions on associated models.
Also, I suspect that you will not get the results you are looking for doing this way anyways. CakePHP by default uses left joins. Therefore, your results will not be limited by those associated with the desired Detail ID, but rather, it will get all uploads, all users associated with those uploads, and then only those details associated with those users that have the correct ID. The simplest way then to get what you're probably looking for is to do the query from the opposite direction:
$result = $this->Detail->find('all', array(
'recursive' => 2,
'conditions' => array('Detail.id' => 1)
));
EDIT: If you do want to do left joins, then make your query this way:
$result = $this->Upload->find('all', array(
'contain' => array('User' => array('Detail' => array('conditions' => array('Detail.id' => 1))),
));

Categories