Related
I'm attempting to ->find('all') & contain the hasMany() Schedules which belongsTo() Services within the Services controller index method. A Service can actually have zero to many Schedules. Testing and experience from the Services view method has lead me believe that the issue is due to a DB field that is using a keyword. It's a field that I cannot see a need for at this time. Unfortunately, I don't have the ability to change database structure.
I attempted ->contain() and specified the fields I wanted, including the primary fields of the Schedules contain. The results were the same as Attempt #1 listed below. I have successfully used that in another model.
I attempted ->selectAllExcept($tableObj, ['FieldToExclude'])->contain(). I also attempted a beforeFind() within the SchedulesTable.php after research here but the function never seemed to be called.
I'm able to circumvent the issue in the view method by specifying the fields in a find directly to the Services table. However, this is not a viable option within the index method. I can do a loop to get the related records, but that's timely and defeats the fantastic benefits of Cake. Contain is awesome and is exactly what I need here.
Are the belongsTo & hasMany set up incorrectly? Am I missing something in the coding of $query? I've read through the documentation and I know that I am missing what is probably obvious to so many. I know I am getting to the point of frustration that won't let me think clearly. ;) I've even stepped away for a day hoping that would help.
Thank you in advance for any help that can be provided. It will be greatly appreciated.
ServicesTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('OSCL');
$this->setDisplayField('DocNum');
$this->setPrimaryKey('DocNum');
$this->belongsTo('Locations', [
'className' => 'Locations',
'bindingKey' => 'Address',
'foreignKey' => 'BPShipCode',
'joinType' => 'INNER'
]
);
$this->hasMany('Schedules', [
'className' => 'Schedules',
'foreignKey' => 'SrcvCallID',
'bindingKey' => 'CallID'
]
);
}
And SchedulesTable.php
use Cake\Event\Event;
use ArrayObject;
class SchedulesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('SCL6');
$this->setDisplayField('Technician');
$this->setPrimaryKey(['SrcvCallID', 'Line']);
$this->belongsTo('Services', [
'className' => 'Services',
'foreignKey' => 'SrcvCallID',
'bindingKey' => 'CallID'
]
);
$this->hasOne('Employees', [
'className' => 'Employees',
'foreignKey' => 'empID',
'bindingKey' => 'Technician'
]
);
}
/*
Added to class after the original 3 attempts.
Re-attempted the queries. Function made no difference.
Debug & die within the function showed that the function was never called.
*/
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
{
return $query->selectAllExcept($this, ['Close']);
}
}
Attempt #1 returns the expected # of records but the schedules array is empty. I don't get any error messages and the query log doesn't show any attempts to access the Schedules table.
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->contain(['Schedules']);
Debug of $query->toArray(); The table has over 100 fields. I've removed many here for visual ease.
\src\Controller\ServicesController.php (line 124)
[
(int) 0 => object(App\Model\Entity\Service) {
'callID' => (int) 361893,
'subject' => 'Printer jam',
'customer' => 'C202044',
'custmrName' => 'Some Company',
'contctCode' => (int) 986,
'manufSN' => '',
'internalSN' => '16J151800861',
'createDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'createTime' => (int) 1612,
'closeDate' => null,
'closeTime' => null,
'DocNum' => (int) 316939,
'Series' => (int) 30,
'schedules' => [],
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Services'
},
]
Query Log
SELECT
Services.callID AS [Services__callID], Services.subject AS [Services__subject], Services.customer AS [Services__customer], Services.custmrName AS [Services__custmrName], ...
FROM
OSCL Services
WHERE
(
Services.status = 7
OR Services.status = -3
)
ORDER BY Services.DocNum DESC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY
SELECT
(
COUNT(*)
) AS [count]
FROM
OSCL Services
WHERE
(
Services.status = 7
OR Services.status = -3
)
Attempt #2 generates a SQL query error "Error: SQLSTATE[42000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'Close'". Makes sense because the default behavior for a model is to select all of the fields.
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->select($this->Services->Schedules);
Attempt #3. I tried to pass the table object to ->selectAllExcept() and exclude the problem field. The query builder builds the query incorrectly by using the fields from the Schedules table as if they were part of the Services table
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->selectAllExcept($this->Services->Schedules, ['Close'])
->contain(['Schedules']);
Query built by the query builder. SrcvCallID, Line & Technician are all fields of the Schedules table, not Services.
SELECT
Services.SrcvCallID AS [Services__SrcvCallID], Services.Line AS [Services__Line], Services.Technician AS [Services__Technician], ...
FROM
OSCL Services
WHERE
(Services.status = :c0 OR Services.status = :c1)
ORDER BY
Services.DocNum DESC
OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY
Here is an example of the expected results manually created from the view function.
[
(int) 0 => object(App\Model\Entity\Service) {
'callID' => (int) 361893,
'subject' => 'Printer jam',
'customer' => 'C202044',
'custmrName' => 'Some Company',
'contctCode' => (int) 986,
'manufSN' => '',
'internalSN' => '16J151800861',
'createDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'createTime' => (int) 1612,
'closeDate' => null,
'closeTime' => null,
'DocNum' => (int) 316939,
'Series' => (int) 30,
'schedules' => [
(int) 0 => object(App\Model\Entity\Schedule) {
'SrcvCallID' => (int) 361893,
'Line' => (int) 1,
'Technician' => (int) 243,
'StartDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-18 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'StartTime' => (int) 800,
'EndDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-18 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'EndTime' => (int) 900,
'U_NB_ChkInDate' => null,
'U_NB_ChkInTime' => null,
'U_NB_ChkOutDate' => null,
'U_NB_ChkOutTime' => null,
'SignData' => null,
'Duration' => (float) 1,
'DurType' => 'H',
'Sched_Closed' => 'N',
'U_NB_TechRate' => (float) 50,
'U_NB_FollowUp' => null,
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Schedules'
},
(int) 1 => object(App\Model\Entity\Schedule) {
'SrcvCallID' => (int) 361893,
'Line' => (int) 2,
'Technician' => (int) 243,
'StartDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'StartTime' => (int) 1600,
'EndDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'EndTime' => (int) 1630,
'U_NB_ChkInDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'U_NB_ChkInTime' => (int) 1600,
'U_NB_ChkOutDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'U_NB_ChkOutTime' => (int) 1630,
'SignData' => null,
'Duration' => (float) 30,
'DurType' => 'M',
'Sched_Closed' => 'N',
'U_NB_TechRate' => (float) 150,
'U_NB_FollowUp' => 'Y',
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Schedules'
}
],
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Services'
},
]
Again, thanks for any assistance.
Suppose I have this find():
$categories = $this->Pages->Categories->find('active')
->select(['title', 'slug', 'page_count'])
->order(['title' => 'ASC'])
->cache('widget_categories')
->toArray();
Now, I want the slug field will be the array key.
For now I use a (horrible) loop:
foreach ($categories as $k => $category) {
$categories[$category->slug] = $category;
unset($categories[$k]);
}
Example of debug for $categories:
########## DEBUG ##########
[
'first-page-category' => object(MeCms\Model\Entity\PagesCategory) {
'title' => 'First page category',
'slug' => 'first-page-category',
'page_count' => (int) 1,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'MeCms.Categories'
},
'sub-sub-page-category' => object(MeCms\Model\Entity\PagesCategory) {
'title' => 'Sub sub page category',
'slug' => 'sub-sub-page-category',
'page_count' => (int) 2,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'MeCms.Categories'
}
]
###########################
This creates two problems:
it's a horrible method;
the data is cached before changing the array keys, then the same cycle must be performed each time.
One possible solution is to use Cache::read() and Cache::write(). Example:
$categories = Cache::read('widget_categories');
if (empty($categories)) {
//Find...
//Loop for changing keys...
Cache::write('widget_categories', $categories);
}
What seems to me the best solution, however, it is to use formatResults() with combine() methods. Example:
$categories = $this->Pages->Categories->find('active')
->select(['title', 'slug', 'page_count'])
->order(['title' => 'ASC'])
->formatResults(function ($results) {
return $results->combine('slug', function ($row) {
return $row;
});
})
->cache('widget_categories')
->toArray();
It's correct? Is there a simpler method and/or makes the code more readable?
Thanks.
i am trying to save additional data to a belongsToMany join Table.
I followed the instructions here
But they are just in case the entities already exist, because an id is used it seems. But my entities should be new created and additional join data should be saved.
My save data looks like this. Everything is persisted fine, except the additional field 'type_keys'
(int) 0 => object(Cloud\Model\Entity\MediaObject) {
'media_object_type_id' => 'image',
'title' => '1482842705_1_749145',
'relative_path' => '/optional_images/1/1/',
'extension' => 'jpg',
'size' => (int) 142683,
'original_title' => 'logo.jpg',
'_joinData' => [
'type_key' => 'optional_image_1'
],
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'media_object_type_id' => true,
'title' => true,
'relative_path' => true,
'extension' => true,
'size' => true,
'original_title' => true,
'_joinData' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Cloud.MediaObjects'
}
Unfortunately just the joind ids in the join table are saved, but not the joinData field 'type_keys'
I would be happy if someone can give me a clue.
On further testing i found out that the join data gets overwritten when saving.
For your information: I am setting the media object join data in the beforeSave callback.
object(Cloud\Model\Entity\Touchpoint) {
'title' => 'test',
'user_id' => (int) 1,
'tp_image' => [
'name' => '',
'type' => '',
'tmp_name' => '',
'error' => (int) 4,
'size' => (int) 0
],
'optional_image_1' => [
'name' => 'logo.jpg',
'type' => 'image/jpeg',
'tmp_name' => '/tmp/phpMeTwuQ',
'error' => (int) 0,
'size' => (int) 142683
],
'created' => object(Cake\I18n\Time) {
'time' => '2016-12-27T13:19:03+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\Time) {
'time' => '2016-12-27T13:19:03+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'brand_id' => (int) 1,
'media_objects' => [
(int) 0 => object(Cloud\Model\Entity\MediaObject) {
'media_object_type_id' => 'image',
'title' => '1482844743_1_988232',
'relative_path' => '/optional_images/1/1/',
'extension' => 'jpg',
'size' => (int) 142683,
'original_title' => 'logo.jpg',
'_joinData' => object(Cake\ORM\Entity) {
'touchpoint_id' => (int) 8,
'media_object_id' => (int) 8,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'MediaObjectsTouchpoints'
},
'created' => object(Cake\I18n\Time) {
'time' => '2016-12-27T13:19:03+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\Time) {
'time' => '2016-12-27T13:19:03+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'id' => (int) 8,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [
'_joinData' => [
'type_key' => 'optional_image_1'
]
],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Cloud.MediaObjects'
}
],
'id' => (int) 8,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Cloud.Touchpoints'
}
So i know it gets overwritten, but i am not sure how to do this the right way?
Ok i found the solution now.
Since i saw an entity is created as _joinData i created an entity myself and did set the property in the entity myself, this way the _joinData does not get replaced, but just enriched with the ids.
$joinTable = TableRegistry::get('MediaObjectsTouchpoints');
$newMediaObject->_joinData = $joinTable->newEntity();
$newMediaObject->_joinData->type_key = 'something';
$entity->media_objects[] = $newMediaObject;
I have printed object and trying to print Tenats.stage
<?php foreach ($tenancies as $tenancy): ?>
<td><?= debug($tenancy); ?></td>
Print this =>
object(App\Model\Entity\Tenancy) {
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2016-03-18T15:57:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'tenants' => [],
'property' => object(Cake\ORM\Entity) {
'id' => (int) 4110,
'address1' => '119 Alan Moss Road',
'postcode' => 'le115ly',
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Properties'
},
'_matchingData' => [
'Tenants' => object(Cake\ORM\Entity) {
'stage' => (int) 2,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tenants'
}
],
I need to 'stage' value
Any help please
That's how to print _matchingData
<td><?= h($tenancy->_matchingData['Tenants']->stage); ?></td>
But if you specify the main parents field id (Tenancy.id) automaticaly your data will look much much better. for example my parent model is "Tenancy" Now I am getting Tenant.id and Tenancy.id and Property.id :
$tenancies = $this
->find()
->select([
'Tenancy.id', 'Tenancy.created', 'Tenancy.stage',
'Properties.id', 'Properties.address1', 'Properties.postcode',
'Tenants.stage',
])
->contain('Properties', function(\Cake\ORM\Query $query) {
return $query->where([
'Properties.active' => 1
]);
})
->contain([
'Tenants'
])
->matching('Tenants', function(\Cake\ORM\Query $query) {
return $query->where([
'Tenants.active' => 1
]);
})
->where([
'Tenancy.active' => 1,
$conditions
])
->order([
'Tenancy.created' => 'DESC',
'Tenants.tenancy_id'
]);
return $tenancies;
}
It prints the array with deep associations which is cool and I can get my tenants property like this:
<td><?= h($tenancy->tenants->stage); ?></td>
Prints=>
0 => object(App\Model\Entity\Tenancy) {
'id' => (int) 3923,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2016-03-19T13:12:32+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'stage' => (int) 2,
'tenants' => [
(int) 0 => object(Cake\ORM\Entity) {
'id' => (int) 8903,
'user_id' => (int) 15318,
'tenancy_id' => (int) 3923,
'needs_guarantor' => true,
'guarantor_id' => null,
'holding_fee' => (float) 50,
Result: Make sure always you write your query properly so you access your data nice and tidy.
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]);
}
]);