Retrieve related data (hasMany) in CakePHP 3 - cakephp

Events hasMany TicketTypes
TicketTypes belogsTo Events
I am trying to retrieve all events with associated ticket types:
$query= $this->Events
->find()
->select(['id', 'name'])
->autoFields(false)
->contain(['TicketTypes' => function($q) {
return $q->select(['TicketTypes.id', 'TicketTypes.name']); }])
;
SQL query generated:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events
But what I expected is:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name`, TicketTypes.id AS `TicketTypes__id`, TicketTypes.name AS `TicketTypes__name` FROM events Events LEFT JOIN ticket_types TicketTypes ON Events.id = (TicketTypes.event_id)
This is how my models are configured:
class EventsTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'hasMany'=> ['TicketTypes']
]);
}
}
class TicketTypesTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'belongsTo' => ['Events']
]);
}
}
Here is the result of debugging my find query:
object(Cake\ORM\Query) {
'(help)' => 'This is a Query object, to get the results execute or iterate it.',
'sql' => 'SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events',
'params' => [],
'defaultTypes' => [
'Events.id' => 'integer',
'id' => 'integer',
'Events.name' => 'string',
'name' => 'string',
'Events.datetime_start' => 'datetime',
'datetime_start' => 'datetime',
'Events.datetime_end' => 'datetime',
'datetime_end' => 'datetime',
'Events.created' => 'datetime',
'created' => 'datetime',
'Events.modified' => 'datetime',
'modified' => 'datetime',
'Events.slug' => 'string',
'slug' => 'string',
'TicketTypes.id' => 'integer',
'TicketTypes.event_id' => 'integer',
'event_id' => 'integer',
'TicketTypes.name' => 'string',
'TicketTypes.description' => 'text'
],
'decorators' => (int) 0,
'executed' => false,
'hydrate' => true,
'buffered' => true,
'formatters' => (int) 0,
'mapReducers' => (int) 0,
'contain' => [
'TicketTypes' => [
'queryBuilder' => object(Closure) {
}
]
],
'matching' => [],
'extraOptions' => [],
'repository' => object(App\Model\Table\EventsTable) {
'registryAlias' => 'Events',
'table' => 'events',
'alias' => 'Events',
'entityClass' => 'App\Model\Entity\Event',
'associations' => [
(int) 0 => 'tickettypes'
],
'behaviors' => [],
'defaultConnection' => 'default',
'connectionName' => 'default'
}
}
And here is the result of debugging $query->all():
object(Cake\ORM\ResultSet) {
'items' => [
(int) 0 => object(App\Model\Entity\Event) {
'id' => (int) 101,
'name' => 'qwertyuiop',
'ticket_types' => [],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Events'
},
...
As you can see in this line 'ticket_types' => [] ticket types are not being returned by the query.
What can I do to retrieve TicketTypes data?
Thanks.

hasMany associations are being retrieved in a separate query
Your assumption about how the CakePHP ORM retrieves associated data is incorrect.
Unlike hasOne and belongsTo assocaitions which are using joins in the main query, hasMany and belongsToMany asociated data is being retrieved in a separate query, which is being filtered using foreign key values collected from the main query, which in your case would be the Events.id column values.
Look at the rest of the SQL log, you shound find a query similar to
SELECT
TicketTypes.id AS `TicketTypes__id`, ...
FROM
ticket_types TicketTypes
WHERE
TicketTypes.event_id IN (1,2,3, ...)
The results of that query are being stitched together with the main results, and returned in a single result set.
Foreign keys need to be selected
A second problem is that your containments select() call is missing the foreign key column (TicketTypes.event_id), which is required, as without it, the ORM cannot stitch the results together, and thus the ticket types will not be present in the results.
Quote from the docs:
When you limit the fields that are fetched from an association, you
must ensure that the foreign key columns are selected. Failing to
select foreign key fields will cause associated data to not be present
in the final result.
See also
Cookbook > Database Access & ORM > Retrieving Associated Data
Cookbook > Database Access & ORM > Query Builder > Passing Conditions to Contain

Related

cant stop null assciated models from beign returned in cakephp 3

I am trying to retrieve records from a primary and associated model where the associated model has a condition on it. The is a basic one to many relationship. The WpPosts is the primary and i want all associated rows from WpPostmetas that have the meta_value of lead_data from a data range. The below code does this but it also including null associated models to the WpPost output. The below data should not have been retrieved. How do i prevent the below data from being retrieved?
object(App\Model\Entity\WpPost) {
'ID' => (int) 1997,
'post_author' => (int) 1,
'post_date' => object(Cake\I18n\FrozenTime) {
'time' => '2018-09-27T12:08:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'post_date_gmt' => object(Cake\I18n\FrozenTime) {
'time' => '2018-09-27T12:08:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
.....
'comment_count' => (int) 0,
'wp_postmetas' => [], //see null value should be included
///query works but also included null associated models
$q2= $this->WpPosts->find()->contain('WpPostmetas', function ($q) {
return $q
->where(['meta_key LIKE' => '%lead_data%' ]);
})
->where(['WpPosts.post_date >=' => $searchDate ])
->order(['WpPosts.id' => 'DESC'])
->enableHydration(true);

Why does patchEntity() cut out image information? (CakePHP3)

Using the POST method $data = $this->request->getData(); , I get the archive:
[
'category_id' => '62',
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => '30',
'is_new' => '1',
'img' => [
'tmp_name' => 'D:\Web\OpenServer\userdata\temp\php70D9.tmp',
'error' => (int) 0,
'name' => 'IronMan.jpg',
'type' => 'image/jpeg',
'size' => (int) 131830
]
]
By preparing these data for the record in the database:
$product = $this->Products->patchEntity($product, $data);
But the patchEntity() method cuts out all the information about the image.
I get:
object(App\Model\Entity\Product) {
'category_id' => (int) 62,
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => (float) 30,
'is_new' => (int) 1,
'img' => '', // <--- empty :(
'[new]' => true,
'[accessible]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'img' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true
],
'[dirty]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'img' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
It can be fixed? Tell me at least about. Thank you.
When patching/creating an entity, the data is bein marshalled according to the respective columns data type, as you can see for other properties like price, which is converted from a string to a float.
Your img column is probably of the type string, causing the marshaller to convert the data accordingly (see \Cake\Database\Type\StringType::marshal()).
There are various ways to avoid that, for example using a different property name that doesn't map to an existing column, like img_upload, and then after moving the upload, manually set the resulting filesystem path to the img property and save that.
That could also be done in the beforeMarshal event in your ProductsTable class, so that the view template can continue to use the img property:
public function beforeMarshal(
\Cake\Event\Event $event,
\ArrayObject $data,
\ArrayObject $options
) {
if (isset($data['img'])) {
$data['img_upload'] = $data['img'];
unset($data['img']);
}
}
You could also create a custom database type for the img column, one which doesn't marshal the data to a string, but just passes it on:
namespace App\Database\Type;
use Cake\Database\Type;
class FileType extends Type
{
public function marshal($value)
{
return $value;
}
}
You'd have to assign the filesystem path anyways though, you'd basically just avoid using a separate/temporary property.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
I do not know how much this is correct, but in the end I did the following and everything works as I need:
In ProductsController:
public function add()
{
$product = $this->Products->newEntity();
if ($this->request->is('post')) {
$data = $this->request->getData();
$product = $this->Products->patchEntity($product, $data);
// If there is a picture that we checked (by the method of validationDefault, when calling patchEntity) and have already uploaded to the server in a temporary folder, then
if($product->img_upload['name']){
// We call the user method of processing the downloaded image
$product = $this->_customUploadImg($product);
// Leave only the name of the new file, adding it to the new property, with the name corresponding to the name of the table in the database
$product->img = $product->img_upload['name'];
}
// Delete an unnecessary property
unset($product->img_upload);
if ($this->Products->save($product)) {
// ...
}
// ...
}
In Product.php:
class Product extends Entity{
protected $_accessible = [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true,
// 'img' => true,
// Here we specify not 'img' as in the database table, but 'img_upload', in order for ProductsController not to delete our data file about the uploaded file when patchEntity was called.
'img_upload' => true,
];
}
In ProductsTable.php:
public function validationDefault(Validator $validator)
{
//...
$validator
->allowEmpty('img_upload', 'create')
->add('img_upload', [
'uploadError' => [
'rule' => 'uploadError',
'message' => 'Error loading picture'
],
'mimeType' => [
'rule' => ['mimeType', ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']],
'message' => 'Only image files are allowed to be uploaded: JPG, PNG и GIF'
],
'fileSize' => [
'rule' => ['fileSize', '<=', '2MB'],
'message' => 'The maximum file size should be no more than 2 MB'
]
]);
//...
}
In add.ctp:
echo $this->Form->create($product, ['type' => 'file']) ?>
// ...
echo $this->Form->control('img_upload', ['type' => 'file', 'label'=>'Product photo']);
//...
Thanks "ndm" and "mark"!

Mock auth cakephp 3

Currently I'm trying to test a controller which uses the authentication component to retrieve the user id. Since I'm fairly new to unit/integration testing, I have no idea how to make this working. Moreover, all the content that I could find for this particular problem is written for Cakephp version 2.
Controller function:
public function favorite(){
// Particular line where the problem is occurring:
$userId = $this->Auth->User()['id'];
// Get all favorite pictures of the user
$query = $this->UsersPictures->getFavoritePictures($userId);
$results = $query->all();
// Replace the link in the result set by a presigned url of Amazon
foreach($results as $result){
$result->picture->link = $this->Aws->getTokenizedItem($result->picture->link);
}
$this->set([
'success' => true,
'data' => [
'pictures' => $results
],
'_serialize' => ['success', 'data']
]);
}
Integration test:
public function testFavoriteShouldPass(){
$this->configRequest([
'headers' => [
'Accept' => 'application/json'
]
]);
$this->get('api/pictures/favorite/1.json');
$expected = [
'success' => true,
'data' => [
[
'user_id' => 1,
'picture_id' => 1,
'created' => '2016-04-03T20:35:40+0000',
'modified' => '2016-04-03T20:35:40+0000',
'picture' => [
'id' => 1,
'album_id' => 1,
'description' => 'Lorem ipsum dolor sit amet',
'link' => 'test',
'favorite' => true,
'created' => null,
'modified' => null,
'cover_photo' => true
]
]
]
];
$this->assertEquals($expected, $response);
}
My question is how can I insert a user with a default id of 1 for $this->Auth->User()['id']. I saw in other questions that I need to use something that looks like this:
$this->_controller->Auth
->staticExpects($this->any())
->method('user')
->will($this->returnValue([
'id' => 1,
'username' => 'admin',
'created' => '2013-05-08 00:00:00',
'modified' => '2013-05-08 00:00:00',
'email' => 'me#me.com',
]));
However, I read that staticExpects is deprecated from phpunit version 3.8 (I'm using 5.2). How should I mock this?
You can set the session data in your integration tests using $this->session(), for example (taken from cakephp book):
// Set session data
$this->session([
'Auth' => [
'User' => [
'id' => 1,
'username' => 'testing',
// other keys.
]
]
]);
You can actually find it in their docs: http://book.cakephp.org/3.0/en/development/testing.html#controller-integration-testing
You can find it in the section
Testing Actions That Require Authentication
If you want to use the same session data on each test you can use the setUp method of your integration test class, like so:
public function setUp()
{
parent::setUp();
// Set session data
$this->session([
'Auth' => [
'User' => [
'id' => 1,
'username' => 'testing',
// other keys.
]
]
]);
}
With thanks to azahar :), this worked for me:
public function controllerSpy($event)
{
parent::controllerSpy($event);
if (isset($this->_controller)) {
$this->_controller->Auth->setUser([
'id' => 1,
'username' => 'testtesttesttest',
'email' => 'john#doe.com',
'first_name' => 'John',
'last_name' => 'Doe',
'uuid' => 'wepoewoweo-ew-ewewpoeopw',
'sign_in_count' => 1,
'current_sign_in_ip' => '127.0.0.1',
'active' => true
]);
}
}

cakePHP 3: multi JOIN

I'm having trouble with relationships between tables with CakePHP 3.x. I am struggling to make realcionamento between tables with more than 2 levels realcionamento .
I will introduce the code of relationships and also the query I'm doing to make it more clear what behavior relations:
class CircuitosTable extends Table{
public function initialize(array $config)
{
$this->table('circuitos');
$this->addAssociations([
'belongsTo' => [
'Planostreinos' => [
'foreignKey' => 'id_plano_treino',
'joinType' => 'INNER',
'bindingKey' => 'id'
]
],
'hasMany' => [
'Atividades' => [
'className' => 'Atividades',
'foreignKey' => 'id_circuito',
'bindingKey' => 'id',
'joinType' => 'INNER',
'dependent' => false,
'cascadeCallbacks' => false,
'propertyName' => '_atividades'
],
]
]);
}
}
class AtividadesTable extends Table{
public function initialize(array $config)
{
$this->table('atividades');
$this->addAssociations([
'belongsTo' => [
'Tiposexercicios' => [
'foreignKey' => 'id_tipo_exercicio',
'joinType' => 'INNER',
'bindingKey' => 'id'
],
'Circuitos' => [
'foreignKey' => 'id_circuito',
'joinType' => 'INNER',
'bindingKey' => 'id'
]
]
]);
}
}
class TiposexerciciosTable extends Table{
public function initialize(array $config)
{
$this->table('tipos_exercicios');
$this->addAssociations([
'hasMany' => [
'Atividades' => [
'className' => 'Atividades',
'foreignKey' => 'id_tipo_exercicio',
'bindingKey' => 'id',
'joinType' => 'INNER',
'dependent' => false,
'cascadeCallbacks' => false,
'propertyName' => '_atividades'
]
]
]);
}
}
I'm doing this query:
$circuitos = $CircuitosTable->find('all', [
'conditions' => ['id_plano_treino' => $idPlano],
'contain' => ['Atividades'],
'joins' => [
[
"table" => "Tiposexercicios",
"alias" => "TipoExercicio",
"type" => "INNER",
"conditions" => ["TipoExercicio.id = Atividades.id_tipo_exericio"]
]
]
]);
The expected result:
Select * From Circuitos
JOIN Atividades on Atividades.id_circuito = Circuitos.id
JOIN Tiposexercicios on Tiposexercicios.id = Atividades.id_tipo_exercicio
The submitted query does not work properly. How should do?
hasMany associations are being queried using a separate query, so using contain() to include Atividades will not include the table in the main query.
It looks like you want to filter by associated data, so you can save yourself a lot of trouble by simply using Query::matching() or Query::innerJoinWith(). This will join in associated tables as needed.
The following would create a query similar to what you seem to be looking for:
$circuitos = $CircuitosTable
->find()
->innerJoinWith('Atividades.Tiposexercicios')
->where([
'Circuitos.id_plano_treino' => $idPlano
])
->group('Circuitos.id');
SELECT
-- ...
FROM
circuitos Circuitos
INNER JOIN atividades Atividades
ON Circuitos.id = (Atividades.id_circuito)
INNER JOIN tipos_exercicios Tiposexercicios
ON Tiposexercicios.id = (Atividades.id_tipo_exercicio)
WHERE
Circuitos.id_plano_treino = 123
GROUP BY
Circuitos.id
The grouping is required as you may otherwise receive duplicate results.
See also Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data
On a side note, the table key in the join definition is ment to hold the actual database table name, which, according to your table classes, is tipos_exercicios!

patchEntity appears to erase foreign keys

Here's the situation:
I have a table which has a composite key:
CREATE TABLE records (
id int(11) NOT NULL,
pacient_id int(11),
doctor_id int(11),
--
-- bunch of stuff that are null
--
PRIMARY KEY (id, pacient_id, doctor_id),
CONSTRAINT records_ibfk_1 FOREIGN KEY (pacient_id) REFERENCES pacients (id),
CONSTRAINT records_ibfk_2 FOREIGN KEY (doctor_id) REFERENCES doctors (id)
)
so far so good, the problem is when I try to add a record... I get this error
Cannot insert row, some of the primary key values are missing. Got (,
, ), expecting (id, pacient_id, doctor_id)
So I went to the add method in the controller and I started put two debugs. One debuging $this->request->data and other debuging $record itself (after the patchEntity) something like this:
public function add()
{
$record = $this->Records->newEntity();
if ($this->request->is('post')) {
$record = $this->Records->patchEntity($record, $this->request->data);
debug($record);
debug($this->request->data);die;
this code was generated with bake.
the result of debug is:
object(App\Model\Entity\Record) {
'others' => '',
'staging' => '',
'surgery' => '',
'medication' => '',
'alergy' => '',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'others' => true,
'staging' => true,
'surgery' => true,
'medication' => true,
'alergy' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Records'
}
and
[
'doctor_id' => '2',
'pacient_id' => '7',
'points_unusual_lost_weigh' => '',
'points_lost_weight' => '',
'lack_apetite' => '',
'dm' => '',
'has' => '',
'dcv' => '',
'drenal' => '',
'others' => '',
'chemotherapy' => '',
'radiotherapy' => '',
'metastasis' => '',
'staging' => '',
'surgery' => '',
'alcoholic' => '',
'smoker' => '',
'smoker_date' => [
'year' => '',
'month' => '',
'day' => ''
],
'intestine_habit' => '',
'medication' => '',
'alergy' => ''
]
see, after patchEntity there is none _id, but, I really don't know why it's not reaching there.
thanks for those who help!
-- EDITED --
answering the first question below from rrd: yes! I did and it looks like this: (here are my associations)
$this->belongsTo('Pacients', [
'foreignKey' => 'pacient_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Doctors', [
'foreignKey' => 'doctor_id',
'joinType' => 'INNER'
]);
second question from ndm: (here is my $_accessible variable)
protected $_accessible = [
'*' => true,
'id' => false,
'pacient_id' => false,
'doctor_id' => false,
];
Just as assumed, the primary key fields are set be non-accessible/mass-assignable.
Either modify the entity code and remove pacient_id and doctor_id, or set them to true, or use the accessibleFields option in the patchEntity call to override the behavior for the passed entity instance only.
See
Cookbook > Database Access & ORM > Entities > Mass Assignment
Cookbook > Database Access & ORM > Saving Data > Changing Accessible Fields
The debug output of the entity is a little misleading in showing only the *, as this suggests that all fields are accessible. This happens because the "empty" array entries are being filtered in order to have only those fields shown that are actually accessible, which however is obviously a problem when * is used, as the exceptions are lost, so there seems to be room for improvement.

Resources