How to get only selected fields from rightJoin() and contain() in cakephp3 - cakephp

cakephp 3.2.3x
I am trying to show custom fields from right join which is contained (left joined) with an other table and it is not working. How ever contain custom fields is working.
Note 1: When I get rid of contain() my ORM works as I am expecting.
$properties = $this->find()
->select([
'Property.id', 'Property.company_id', 'Property.address1', 'Property.postcode',
'Tenancies.property_id', 'Tenancies.start_date', 'Tenancies.end_date', 'Tenancies.deposit_total',
])
->rightJoin(['Tenancies' => 'tenancy'],[
'Tenancies.property_id = Property.id',
'Tenancies.active = 1'
])
->contain([
'Tenancies.Tenants' => function($q) {
return $q
->select([
'Tenants.id', 'Tenants.stage', 'Tenants.tenancy_id', 'Tenants.holding_fee',
])
->where([
'active = 1',
]);
}
])
->where(['Property.active = 1', $conditions]);
Note 2: Generated sql is correct but it is not applying to the query.
'SELECT Property.id AS Property__id, Property.company_id AS
Property__company_id, Property.address1 AS Property__address1,
Property.postcode AS Property__postcode,
Tenancies.property_id AS Tenancies__property_id,
Tenancies.start_date AS Tenancies__start_date,
Tenancies.end_date AS Tenancies__end_date,
Tenancies.deposit_total AS Tenancies__deposit_total FROM
property Property RIGHT JOIN tenancy Tenancies ON
(Tenancies.property_id = Property.id AND Tenancies.active = 1) WHERE
(Property.active = 1 AND Property.company_id = :c0)'
The query should show me only 3 fields of 'Tenancies' but it retrieves all the Tenancies fields.
(int) 0 => [
'id' => (int) 102,
'company_id' => (int) 3,
'address1' => 'Grace Dieu Court',
'postcode' => 'LE11 4QH',
'tenancies' => [
(int) 0 => [
'id' => (int) 16,
'property_id' => (int) 102,
'landlord_id' => (int) 65,
'agent_id' => (int) 7,
'company_id' => (int) 3,
'bedroom' => (int) -1,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2015-05-08T09:30:41+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'active' => true,
...
... ## } The rest of all fileds
'tenants' => [
(int) 0 => [
'id' => (int) 16,
'stage' => (int) 7,
'tenancy_id' => (int) 16,
'holding_fee' => (float) 50
],
(int) 1 => [
'id' => (int) 17,
'stage' => (int) 7,
'tenancy_id' => (int) 16,
'holding_fee' => (float) 50
]
]
]
]
],

Related

Update to value in _joinData works but misses right index

I'm finally finding out how to update a join table after a lot of work, but I found something that I definitely cannot solve. I added manually a set of checkboxes to update an item on my join table and work almost perfectly, as long as they're part of my legacy database. When I add new items to the join table with lower id, then the wrong item on my join table is updated.
Here is how I have my controller:
public function edit($id)
{
$brigada = $this->Brigadas
->findById($id)
->contain(['Tags', 'Voluntarios'])
->firstOrFail();
if ($this->request->is(['patch', 'post', 'put'])) {
// https://github.com/cakephp/cakephp/issues/7864
if (!empty($this->request->data['voluntarios']['_ids'])) {
$vol = [];
$ids = [];
foreach ($this->request->data['voluntarios']['_ids'] as $key=>$locationId) {
$vol['_ids'][$key] = $locationId;
$vol[] = [
'_joinData' => [
'lider' => isset($this->request->data['voluntarios'][$key]['_joinData']['lider']) ? $this->request->data['voluntarios'][$key]['_joinData']['lider'] : false
]
];
}
}
if (isset($vol)){
$this->request->data['voluntarios'] = $vol;
}
$this->Brigadas->patchEntity($brigada, $this->request->data, ['associated' => ['Voluntarios._joinData', 'Participaciones', 'Participaciones._joinData']]);
if ($this->Brigadas->save($brigada)) {
$this->Flash->success(__('Your team data has been updated.'));
return $this->redirect(['action' => 'view', $id]);
}
$this->Flash->error(__('Unable to update your team data.'));
}
}
This is how I iterated my form. Noticed that I added an extra hidden form for id just to try changing the id numbers. But the result is exactly the same in the end.
foreach($all_vol as $key=>$voluntario){
echo $this->Form->control('voluntarios.'.$key.'.id', ['hidden' => true]);
echo $this->Form->control('voluntarios.'.$key.'._joinData.lider', [
'label' => $voluntario->full_name,
'default'=>'0',
]);
}
Here is the request sent by the form.
[
'_ids' => [
(int) 0 => '1',
(int) 1 => '2',
(int) 2 => '184',
(int) 3 => '303',
(int) 4 => '1838',
(int) 5 => '1873',
(int) 6 => '2553',
(int) 7 => '4467',
(int) 8 => '4973',
(int) 9 => '5148',
(int) 10 => '5493',
(int) 11 => '6307',
(int) 12 => '7055',
(int) 13 => '7467',
(int) 14 => '7561',
(int) 15 => '7567',
(int) 16 => '7864'
],
(int) 0 => [
'id' => '184',
'_joinData' => [
'lider' => '0'
]
],
(int) 1 => [
'id' => '303',
'_joinData' => [
'lider' => '0'
]
],
(int) 2 => [
'id' => '1838',
'_joinData' => [
'lider' => '0'
]
],
(int) 3 => [
'id' => '1873',
'_joinData' => [
'lider' => '0'
]
],
(int) 4 => [
'id' => '2553',
'_joinData' => [
'lider' => '0'
]
],
(int) 5 => [
'id' => '4467',
'_joinData' => [
'lider' => '0'
]
],
(int) 6 => [
'id' => '4973',
'_joinData' => [
'lider' => '0'
]
],
(int) 7 => [
'id' => '5148',
'_joinData' => [
'lider' => '0'
]
],
(int) 8 => [
'id' => '5493',
'_joinData' => [
'lider' => '0'
]
],
(int) 9 => [
'id' => '6307',
'_joinData' => [
'lider' => '0'
]
],
(int) 10 => [
'id' => '7055',
'_joinData' => [
'lider' => '0'
]
],
(int) 11 => [
'id' => '7467',
'_joinData' => [
'lider' => '0'
]
],
(int) 12 => [
'id' => '7561',
'_joinData' => [
'lider' => '0'
]
],
(int) 13 => [
'id' => '7567',
'_joinData' => [
'lider' => '0'
]
],
(int) 14 => [
'id' => '7864',
'_joinData' => [
'lider' => '0'
]
],
(int) 15 => [
'id' => '1',
'_joinData' => [
'lider' => '1'
]
],
(int) 16 => [
'id' => '2',
'_joinData' => [
'lider' => '1'
]
]
]
I added items with id 1 and 2 to the bottom and turned them into 'lider', but when they're passed to patchEntity, two different items are selected as 'lider'. Here is the request after editing it manually.
[
(int) 0 => [
'id' => '1',
'_joinData' => [
'lider' => '1'
]
],
(int) 1 => [
'id' => '2',
'_joinData' => [
'lider' => '1'
]
],
(int) 2 => [
'id' => '184',
'_joinData' => [
'lider' => '0'
]
],
(int) 3 => [
'id' => '303',
'_joinData' => [
'lider' => '0'
]
],
(int) 4 => [
'id' => '1838',
'_joinData' => [
'lider' => '0'
]
],
(int) 5 => [
'id' => '1873',
'_joinData' => [
'lider' => '0'
]
],
(int) 6 => [
'id' => '2553',
'_joinData' => [
'lider' => '0'
]
],
(int) 7 => [
'id' => '4467',
'_joinData' => [
'lider' => '0'
]
],
(int) 8 => [
'id' => '4973',
'_joinData' => [
'lider' => '0'
]
],
(int) 9 => [
'id' => '5148',
'_joinData' => [
'lider' => '0'
]
],
(int) 10 => [
'id' => '5493',
'_joinData' => [
'lider' => '0'
]
],
(int) 11 => [
'id' => '6307',
'_joinData' => [
'lider' => '0'
]
],
(int) 12 => [
'id' => '7055',
'_joinData' => [
'lider' => '0'
]
],
(int) 13 => [
'id' => '7467',
'_joinData' => [
'lider' => '0'
]
],
(int) 14 => [
'id' => '7561',
'_joinData' => [
'lider' => '0'
]
],
(int) 15 => [
'id' => '7567',
'_joinData' => [
'lider' => '0'
]
],
(int) 16 => [
'id' => '7864',
'_joinData' => [
'lider' => '0'
]
],
'_ids' => [
(int) 0 => '1',
(int) 1 => '2',
(int) 2 => '184',
(int) 3 => '303',
(int) 4 => '1838',
(int) 5 => '1873',
(int) 6 => '2553',
(int) 7 => '4467',
(int) 8 => '4973',
(int) 9 => '5148',
(int) 10 => '5493',
(int) 11 => '6307',
(int) 12 => '7055',
(int) 13 => '7467',
(int) 14 => '7561',
(int) 15 => '7567',
(int) 16 => '7864'
]
]
In the end, it doesn't matter whether I edit manually or not, since I get this on my patchEntity call. Notice that 'id' isn't neither 1 or 2:
'_joinData' => object(App\Model\Entity\Participacione) {
'id' => (int) 981,
'brigada_id' => (int) 222,
'voluntario_id' => (int) 7567,
'lider' => true,
'[new]' => false,
'[accessible]' => [
'brigada_id' => true,
'voluntario_id' => true,
'lider' => true,
'brigada' => true,
'voluntario' => true
How can I solve this? I've posted other questions without many answers, but I'll keep having faith in humanity to sort this out. Thanks in advance for reading my question!
Surprisingly I managed to solve it by using a sort attribute in my table. Like this:
$this->belongsToMany('Voluntarios', [
'foreignKey' => 'brigada_id',
'targetForeignKey' => 'voluntario_id',
'joinTable' => 'participaciones',
'saveStrategy' => 'replace',
'through' => 'Participaciones',
'sort' => ['Voluntarios.id' => 'ASC'],
]);
I feel like everything is pretty forced, but I couldn't find a nicer way to do it. I'm noticing a few bugs by the time I delete a starred value, but it's a step in the right direction.

CakePHP 3: issue saving hasMany associations

I'm having trouble saving and updating hasMany associations. It seems that Cake can't patch the entity correctly somehow.
ProductGroups Table:
$this->hasMany('ProductIdentities', [
'foreignKey' => 'product_group_id',
'saveStrategy' => 'replace'
]);
ProductIdentities Table:
$this->belongsTo('ProductGroups', [
'foreignKey' => 'product_group_id'
]);
ProductGroupsController:
// patch entity
$productGroup = $this->ProductGroups->patchEntity($productGroup, $this->request->data, [
'associated' => [
'StaffMembers' => ['onlyIds' => true],
'ProductIdentities' => ['onlyIds' => true]
]
]);
$this->request->data:
[
'group_name' => 'Creative and Performing Arts',
'active' => '1',
'product_type_id' => '2',
'staff_members' => [
'_ids' => [
(int) 0 => '103',
(int) 1 => '11',
(int) 2 => '3'
]
],
'product_identities' => [
'_ids' => [
(int) 0 => '1760',
(int) 1 => '1762',
(int) 2 => '1763',
(int) 3 => '1764'
]
]
]
edit.ctp
<?php
echo $this->Form->input('group_name',
['class' => 'form-control']);
echo $this->Form->input('active',
['class' => 'form-control']);
echo $this->Form->input('product_type_id',
['class' => 'form-control']);
echo $this->Form->input('staff_members._ids',
['options' => $staffMembers,
'class' => 'form-control height-200']);
echo $this->Form->input('product_identities._ids',
['options' => $productIdentities,
'class' => 'form-control height-500']);
?>
If a productGroup is not associated with any productIdentities, it saves fine. However if you want to select more productIdentities or unselect some existing ones, the data won't be saved. Instead, Cake will create new records in product_identites table.
After doing a debug, I could see that Cake did not patch entity correctly. See below:
'product_identities' => [
(int) 0 => object(App\Model\Entity\ProductIdentity) {
(int) 0 => '1760',
(int) 1 => '1762',
(int) 2 => '1763',
(int) 3 => '1764',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
(int) 0 => true,
(int) 1 => true,
(int) 2 => true,
(int) 3 => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'ProductIdentities'
}
],
The onlyIds option is supposed to stop Cake from creating new records but obviously it didn't work.
And I've formatted my data into the format suggested in Cake's documentation so I'm very confused.
$data = [
'title' => 'My new article',
'body' => 'The text',
'user_id' => 1,
'comments' => [
'_ids' => [1, 2, 3, 4]
]
];
http://book.cakephp.org/3.0/en/orm/saving-data.html#converting-hasmany-data
Any comments/help is appreciated.

CakePHP 3: Collection: listNested

I want to flatten a tree structure with CakePHP's listNested function as explained here: http://book.cakephp.org/3.0/en/core-libraries/collections.html#Cake\Collection\Collection::listNested
I've tried the given example, but I don't get the expected result as in the example. What am I doing wrong or is this a bug in the listNested function?
My code:
$data = [
[
'id' => 1,
'parent_id' => null,
'name' => 'Birds',
'children' => [
['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds', 'children' => []],
['id' => 3, 'parent_id' => 1, 'name' => 'Eagle', 'children' => []],
['id' => 4, 'parent_id' => 1, 'name' => 'Seagull', 'children' => []],
]
],
[
'id' => 6,
'parent_id' => null,
'name' => 'Fish',
'children' => [
['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish', 'children' => []],
]
]
];
$collection = new Collection($data);
$flatList = $collection->listNested();
debug($flatList->toArray());
My result:
[
(int) 0 => [
'id' => (int) 5,
'parent_id' => (int) 6,
'name' => 'Clown Fish',
'children' => []
],
(int) 1 => [
'id' => (int) 6,
'parent_id' => null,
'name' => 'Fish',
'children' => [
(int) 0 => [
'id' => (int) 5,
'parent_id' => (int) 6,
'name' => 'Clown Fish',
'children' => []
]
]
],
(int) 2 => [
'id' => (int) 4,
'parent_id' => (int) 1,
'name' => 'Seagull',
'children' => []
]
]
Expected result:
[
['id' => 1, 'parent_id' => null, 'name' => 'Birds'],
['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds'],
['id' => 3, 'parent_id' => 1, 'name' => 'Eagle'],
['id' => 4, 'parent_id' => 1, 'name' => 'Seagull'],
['id' => 6, 'parent_id' => null, 'name' => 'Fish'],
['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish']
]

How to retrieve only those records that have at least one associated record?

The data should show only the tenancies that contains data. In normal query normally I do this be right join.
I know contain() is left join on default but can contain be right join at all?
My query:
$properties = $this->find()
->select([
'Property.id', 'Property.company_id', 'Property.address1', 'Property.postcode',
])
->contain([
'Tenancies' => function($q) {
return $q
->select([
'Tenancies.id','Tenancies.property_id','Tenancies.created',
'Tenancies.stage', 'Tenancies.landlord_offer_sent', 'Tenancies.offer_letter_contract_id',
])
->contain([
'Tenants' => function($q) {
return $q
->select([
'Tenants.id', 'Tenants.stage', 'Tenants.tenancy_id', 'Tenants.holding_fee',
])
->where([
'active = 1',
]);
}
])
->where([
'Tenancies.active = 1',
]);
}
])
->where(['Property.active = 1', $conditions])
->toArray();
e.g. I need to get read of nodes that tenancies that are null 'tenancies' => [], and it should show only node 1.
Prints=>
(int) 0 => object(App\Model\Entity\Property) {
'id' => (int) 95,
'company_id' => (int) 3,
'address1' => '40 Arthur Street',
'postcode' => 'LE11 3AY',
'tenancies' => [],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Property'
},
(int) 1 => object(App\Model\Entity\Property) {
'id' => (int) 102,
'company_id' => (int) 3,
'address1' => 'Grace Dieu Court',
'postcode' => 'LE11 4QH',
'tenancies' => [
(int) 0 => object(App\Model\Entity\Tenancy) {
'id' => (int) 16,
'property_id' => (int) 102,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2015-05-08T09:30:41+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'stage' => (int) 6,
'landlord_offer_sent' => false,
'offer_letter_contract_id' => (int) 37,
'tenants' => [
(int) 0 => object(Cake\ORM\Entity) {
'id' => (int) 16,
'stage' => (int) 7,
'tenancy_id' => (int) 16,
'holding_fee' => (float) 50,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tenants'
},
(int) 1 => object(Cake\ORM\Entity) {
...
...
...
I have tried inner join 'joinType' => 'INNER' but no luck:
class TenancyTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('tenancy');
$this->displayField('id');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Properties', [
'foreignKey' => 'property_id',
'className' => 'property',
'joinType' => 'INNER'
]);
Not possible via containments for hasMany associations
This cannot be done with containments, as hasMany ones are being retrieved via a separate query. Instead you'll have to add a proper join yourself, an INNER join (more portable than a RIGHT join) could do it if you want to restrict the results to only those that have associated Tenancies.
A simple Query::innerJoinWith() without conditions, and some grouping to avoid duplicates
$properties = $this
->find()
// ...
->innerJoinWith('Tenancies')
->group('Property.id');
... and you're done, this will add a join like
INNER JOIN tenancies Tenancies ON Property.id = Tenancies.property_id
innerJoinWith() works for all other types of associations too
For belongsTo and hasOne associations however, it could be done via containments, as for these types of associations, all data is being retrieved in the same query. To filter by those that have an associated record, just change the join type to INNER, like
$this
->find()
->contain([
'BelongsToAssociated' => [
'joinType' => 'INNER'
]
]);
See also
Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Cookbook > Database Access & ORM > Query Builder > Using innerJoinWith
This worked when I simply changed contains to matching.
It seems to be possible, see Filtering By Associated Data

CakePHP filter a deep associated find

I have a deep associated find and one association is retrieving too many none related records for modules_employees.
I should see only one record for modules_employees under the course_modules but it retrieves many because they can be many with course_modules_id but only one with courses_employee_id.
The modules_employees table
'id' => (int) 18,
'courses_employee_id' => (int) 31,
'course_module_id' => (int) 7,
'completed_on' => null,
CoursesEmployee->course->course_modules->modules_employees
CoursesEmployeesController.php
public function player($id = null)
{
$coursesEmployee = $this->CoursesEmployees->get($id, [
'contain' =>
[
'Employees',
'Courses',
'CourseModules',
'Courses.CourseModules',
'Courses.CourseModules.ModulesEmployees',
'Courses.CourseFiles'
]
]);
$this->set('coursesEmployee', $coursesEmployee);
debug($coursesEmployee);
$this->set('_serialize', ['coursesEmployee']);
}
The current find object, you will see one of the course_modules has two modules_employees when I should have one.
object(App\Model\Entity\CoursesEmployee) {
'id' => (int) 31,
'employee_id' => (int) 3,
'course_id' => (int) 3,
'course_module_id' => (int) 7,
'course_module' => object(App\Model\Entity\CourseModule) {
'id' => (int) 7,
'course_id' => (int) 3,
'name' => 'Module 2',
},
'course' => object(App\Model\Entity\Course) {
'id' => (int) 3,
'name' => 'Treacys Hotel Induction Training',
'course_files' => [
(int) 0 => object(App\Model\Entity\CourseFile) {
'id' => (int) 2,
'name' => 'Manual_Handling_doc.txt',
'type' => 'doc',
}
],
'course_modules' => [
(int) 0 => object(App\Model\Entity\CourseModule) {
'id' => (int) 6,
'course_id' => (int) 3,
'name' => 'Module 1',
'module_order' => (int) 1,
'modules_employees' => [
(int) 0 => object(App\Model\Entity\ModulesEmployee) {
'id' => (int) 1,
'courses_employee_id' => (int) 0,
'course_module_id' => (int) 6,
'started_on' => object(Cake\I18n\Time) {
'time' => '2015-09-08T04:16:16+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'completed_on' => object(Cake\I18n\Time) {
'time' => '2015-09-09T08:22:16+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'completed' => true,
'deleted' => null,
'[new]' => false,
'[accessible]' => [
'employee_id' => true,
'module_id' => true,
'started_on' => true,
'completed_on' => true,
'completed' => true,
'employee' => true,
'module' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'ModulesEmployees'
}
],
'[repository]' => 'CourseModules'
},
(int) 1 => object(App\Model\Entity\CourseModule) {
'id' => (int) 7,
'course_id' => (int) 3,
'name' => 'Module 2',
'module_order' => (int) 2,
'modules_employees' => [
(int) 0 => object(App\Model\Entity\ModulesEmployee) {
'id' => (int) 2,
'courses_employee_id' => (int) 31,
'course_module_id' => (int) 7,
'started_on' => object(Cake\I18n\Time) {
'time' => '2015-09-17T00:00:00+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'completed_on' => null,
'[repository]' => 'ModulesEmployees'
},
(int) 1 => object(App\Model\Entity\ModulesEmployee) {
'id' => (int) 18,
'courses_employee_id' => (int) 32,
'course_module_id' => (int) 7,
'started_on' => object(Cake\I18n\Time) {
'time' => '2015-09-17T00:00:00+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'completed_on' => null,
'[repository]' => 'ModulesEmployees'
}
],
'[repository]' => 'CourseModules'
},
],
},
'employee' => object(App\Model\Entity\Employee) {
'id' => (int) 3,
'user_id' => (int) 4,
},
'[repository]' => 'CoursesEmployees'
}
You should look into matching
http://book.cakephp.org/3.0/en/orm/query-builder.html#filtering-by-associated-data
$query = $this->CoursesEmployees->findById($id)
->contain(['Your_Models_You_Wanna_Contain'])
->matching('Courses.CourseModules.ModulesEmployees', function ($q) use ($id) {
return $q->where(['courses_employee_id' => $id]);
});
if nothing matches then you wont get a CourseEmployee back aswell, if you would still need that you could also use contain:
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#passing-conditions-to-contain
$query = $this->CoursesEmployees->findById($id)->contain([
'Courses.CourseModules.ModulesEmployees' => function ($q) use ($id) {
return $q
->where(['courses_employee_id' => $id]);
}
]);

Resources