CakePHP: creating fixtures for aliased tables - cakephp

I am using a table to store optional data about a number of different types of objects. Thus the table has the following structure:
create table options
(
id int auto_increment
primary key,
object_id int not null,
object_model varchar(255) not null,
option_name varchar(255) not null,
option_value text null,
created datetime not null,
modified datetime not null
);
create index object_model
on options (object_model);
Optional values are found by a combination of both the ID of the object to look up and by the model from which that object comes. Here is how an example model, my Images model, uses it:
$this->hasMany('ImageOptions', [
'foreignKey' => 'object_id',
'className' => 'Options',
'dependent' => true,
])->setConditions(['ImageOptions.object_model LIKE' => $this->getAlias()]);
This system has worked fine for me in the past, but now that I'm running PHPUnit on my application, there seems to be some confusion. When saving a new image with metadata to save to this table, I get the message:
1) Visualize\Test\TestCase\Controller\ImagesControllerTest::testAddSubmit
Possibly related to Cake\Database\Exception: "SQLSTATE[42S02]: Base table or view not found: 1146 Table 'visualize_test.options' doesn't exist"
I can assure you that the table is there and works fine. I'm able to browse the page with no such error messages and even submit a new image without incident. But not in the test.
I assume this has something to do with the aliasing of the database table, but I'm unsure how to resolve it or work around it? Thanks in advance for the help!
Here also is my fixture for ImageOptions:
<?php
declare(strict_types=1);
namespace Visualize\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* OptionsFixture
*/
class ImageOptionsFixture extends TestFixture
{
/**
* Table
*
* #var string
*/
public $table = 'options';
/**
* Fields
*
* #var array
*/
// phpcs:disable
public $fields = [
'id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
'object_id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null],
'object_model' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_0900_ai_ci', 'comment' => '', 'precision' => null],
'option_name' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_0900_ai_ci', 'comment' => '', 'precision' => null],
'option_value' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'collate' => 'utf8mb4_0900_ai_ci', 'comment' => '', 'precision' => null],
'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''],
'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''],
'_indexes' => [
'object_model' => ['type' => 'index', 'columns' => ['object_model'], 'length' => []],
],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
],
'_options' => [
'engine' => 'InnoDB',
'collation' => 'utf8mb4_0900_ai_ci'
],
];
// phpcs:enable
/**
* Init method
*
* #return void
*/
public function init(): void
{
$this->records = [
[
'id' => 4,
'object_id' => 1,
'object_model' => 'Images',
'option_name' => 'this_value',
'option_value' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
'created' => '2021-09-03 10:34:42',
'modified' => '2021-09-03 10:34:42',
],
[
'id' => 5,
'object_id' => 1,
'object_model' => 'Images',
'option_name' => 'that_value',
'option_value' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
'created' => '2021-09-03 10:34:42',
'modified' => '2021-09-03 10:34:42',
],
[
'id' => 6,
'object_id' => 1,
'object_model' => 'Images',
'option_name' => 'the_other_value',
'option_value' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
'created' => '2021-09-03 10:34:42',
'modified' => '2021-09-03 10:34:42',
]
];
parent::init();
}
}
And an example of one test which is failing is here:

Related

CakePHP 4 multiple checkboxes

I want to populate a checkbox group with the array JournalEntry[strategy][strategies_conditions]
I then want to have the checkboxes selected that are contained in JournalEntry[journal_entries_strategy_conditions]
I have the checkboxes displaying but not linked correctly to the correct fields of name and id
This is the code I have in the edit.php
<?php
echo $this->Form->control('journal_id', ['options' => $journals]);
echo $this->Form->control('ticket_number');
echo $this->Form->control('strategy_id', ['options' => $strategies, 'empty' => true, 'onChange' => 'getComboA(this)']);
debug($journalEntry);
?>
<div id="stategy-condition">
<?php echo $this->Form->control('journal_entries_strategy_conditions.strategies_condition_id',
[
'type' => 'select',
'multiple' => 'checkbox',
'options' => $journalEntry[strategy][strategies_conditions],
'selected' => $journalEntry[journal_entries_strategy_conditions]
]); ?>
</div>
With this I get this output, as you can see it is using the object for the <label> and <input value="1" is set to the id value
<div class="input select">
<label for="strategy-id">Strategy</label>
<input type="hidden" name="strategy_id" value="">
<div class="checkbox">
<label for="strategy-id-0">
<input type="checkbox" name="strategy_id[]" value="0" id="strategy-id-0">{
"id": 1,
"strategy_id": 5,
"name": "zxcasd",
"level": "0",
"created": "2020-08-01T21:21:11+00:00",
"modified": "2020-08-01T21:21:11+00:00"
}</label>
</div>
<div class="checkbox">
<label for="strategy-id-1">
<input type="checkbox" name="strategy_id[]" value="1" id="strategy-id-1">{
"id": 2,
"strategy_id": 5,
"name": "zxcasd",
"level": "0",
"created": "2020-08-01T21:21:11+00:00",
"modified": "2020-08-01T21:21:11+00:00"
}</label>
</div>
<div class="checkbox">
<label for="strategy-id-2">
<input type="checkbox" name="strategy_id[]" value="2" id="strategy-id-2">{
"id": 3,
"strategy_id": 5,
"name": "zxcad",
"level": "0",
"created": "2020-08-01T21:21:11+00:00",
"modified": "2020-08-01T21:21:11+00:00"
}</label></div></div>
Here is the data:
object(App\Model\Entity\JournalEntry) {
'id' => (int) 20,
'journal_id' => (int) 1,
'ticket_number' => 'sdfsdfsdf',
'strategy_id' => (int) 5,
'timeframe' => '',
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-25 20:51:42.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-25 20:51:42.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'journal_entries_strategy_conditions' => [
(int) 0 => object(App\Model\Entity\JournalEntriesStrategyCondition) {
'id' => (int) 8,
'journal_entry_id' => (int) 20,
'strategies_condition_id' => (int) 1,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-25 20:51:42.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-25 20:51:42.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
}
],
'strategy' => object(App\Model\Entity\Strategy) {
'id' => (int) 5,
'user_id' => (int) 1,
'name' => 'zxcasd',
'description' => 'zxcasd',
'one_hundred_trades' => (int) 0,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'strategies_conditions' => [
(int) 0 => object(App\Model\Entity\StrategiesCondition) {
'id' => (int) 1,
'strategy_id' => (int) 5,
'name' => 'zxcasd',
'level' => '0',
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'[new]' => false,
'[accessible]' => [
'strategy_id' => true,
'name' => true,
'level' => true,
'created' => true,
'modified' => true,
'strategy' => true
]
},
(int) 1 => object(App\Model\Entity\StrategiesCondition) {
'id' => (int) 2,
'strategy_id' => (int) 5,
'name' => 'zxcasd',
'level' => '0',
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
},
(int) 2 => object(App\Model\Entity\StrategiesCondition) {
'id' => (int) 3,
'strategy_id' => (int) 5,
'name' => 'zxcad',
'level' => '0',
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2020-08-01 21:21:11.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
}
],
},
'[new]' => false,
This the request date for edit.php
[
'journal_id' => '1',
'ticket_number' => 'test con',
'pair' => '',
'buy_sell' => '',
'personal_notes' => '',
'entry_date_time' => '',
'entry_price' => '',
'strategy_id' => '5',
'journal_entries_strategy_conditions' => [
'strategies_condition_id' => [
(int) 0 => '1'
]
],
'timeframe' => '',
'position_size' => '',
'sl' => '',
'tp' => '',
'market_conditions' => '',
'entry_toughts' => '',
'close_date_time' => '',
'close_price' => '',
'profit' => '',
'fees' => '',
'high_price' => '',
'low_price' => '',
'exit_thoughts' => '',
'feeling_before' => '',
'feeling_after' => ''
]
add.php
[
'journal_id' => '1',
'ticket_number' => 'sdfdsf',
'pair' => '',
'buy_sell' => '',
'personal_notes' => '',
'entry_date_time' => '',
'entry_price' => '',
'strategy_id' => '5',
'journal_entries_strategy_conditions' => [
(int) 0 => [
'strategies_condition_id' => '1'
],
(int) 1 => [
'strategies_condition_id' => '2'
]
],
'timeframe' => '',
'position_size' => '',
'sl' => '',
'tp' => '',
'market_conditions' => '',
'entry_toughts' => '',
'close_date_time' => '',
'close_price' => '',
'profit' => '',
'fees' => '',
'high_price' => '',
'low_price' => '',
'exit_thoughts' => '',
'feeling_before' => '',
'feeling_after' => ''
]
There is no selected option, the option to provide values for selection is named val or value, and like the options option, it expects a flat key => value array (there's an exception if you want to provide attributes for <option> elements, in that case you can use nested arrays with text and value keys).
For options the array key will be use for the <option> element's value attribute, and the array value will be used for the content of the element.
For value the array value will be used for matching against the options array's keys, ie AFAICT you'd need an array of strategies_condition_id.
Often times values for select controls are being prepared using the list finder, you'll see that in baked controllers. However since you have nested associations, that's not necessarily feasible, and you're possibly better of converting the data after the fact, which can easily be done using collections.
For example in your view template:
<div id="stategy-condition">
<?php echo $this->Form->control('journal_entries_strategy_conditions', [
'type' => 'select',
'multiple' => 'checkbox',
'options' =>
collection($journalEntry['strategy']['strategies_conditions'])
->combine('id', 'name')
->toArray(),
'value' =>
collection($journalEntry['journal_entries_strategy_conditions'])
->extract('strategies_condition_id')
->toArray()
]); ?>
</div>
The combine() call should build a collection like:
[
1 => 'zxcasd',
2 => 'zxcasd',
3 => 'zxcad',
]
and the extract() should result in a collection containing this:
[
0 => 1
]
which should result in the first checkbox being checked.
Cookbook > Collections > combine()
Cookbook > Collections > extract()
As far as your control name problem goes, there are many ways to solve this, you could for example use a custom template where you hardcode the input name:
echo $this->Form->control('journal_entries_strategy_conditions', [
'type' => 'select',
// ...
'templates' => [
'checkbox' =>
'<input
type="checkbox"
name="journal_entries_strategy_conditions[][strategies_condition_id]"
value="{{value}}"
{{attrs}}>',
]
]);
Cookbook > Views > Helpers > Form > Options for Control
Cookbook > Views > Helpers > Form > Customizing the Templates FormHelper Uses
Or transform the data before marshalling (patching), using the beforeMarshal callback in your JournalEntriesTable class:
public function beforeMarshal(
\Cake\Event\EventInterface $event,
\ArrayObject $data,
\ArrayObject $options
) {
if (isset($data['journal_entries_strategy_conditions']['strategies_condition_id']) {
$ids = $data['journal_entries_strategy_conditions']['strategies_condition_id'];
$conditions = [];
foreach ($ids as $id) {
$conditions[] = [
'strategies_condition_id' => $id,
];
}
$data['journal_entries_strategy_conditions'] = $conditions;
}
}
This would transform the data from:
'journal_entries_strategy_conditions' => [
'strategies_condition_id' => [
'1',
'2'
],
],
to:
'journal_entries_strategy_conditions' => [
[
'strategies_condition_id' => '1'
],
[
'strategies_condition_id' => '2'
]
],
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Or even create the individual checkboxes manually, where you have proper control over every aspect of every form control, including the name:
echo $this->Form->label('journal_entries_strategy_conditions');
echo $this->Form->hidden('journal_entries_strategy_conditions', ['value' => '']);
$selectedIds = collection($journalEntry['journal_entries_strategy_conditions'])
->extract('strategies_condition_id')
->toArray();
foreach ($journalEntry['strategy']['strategies_conditions'] as $index => $condition) {
echo $this->Form->control(
"journal_entries_strategy_conditions.{$index}.strategies_condition_id",
[
'type' => 'checkbox',
'hiddenField' => false,
'label' => $condition['name'],
'value' => $condition['id'],
'checked' => in_array($condition['id'], $selectedIds, true),
]
);
}
This would create name attributes like:
journal_entries_strategy_conditions[0][strategies_condition_id]
journal_entries_strategy_conditions[1][strategies_condition_id]
...
Cookbook > Views > Helpers > Form > Creating Select, Checkbox and Radio Controls

CakePHP 3: ->find('all') with contain not populating with associated data

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.

Cakephp 3.7 - Entity accessible property element changed

The $_accessible property for the db column of user_id key & value are being changed to (int)0 => false from 'user_id' => true and I don't know how or why. Therefore, the user_id in the form data is being ignored and is not part of the patched entity.
Specs: CakePHP 3.7 on Windows Server 2012 with MySQL 8.
The Warehouses entity was initially created via bake without the associated user_id, but with the associated company_id. I am & was able to save/edit records when company_id is populated. I then manually added the user_id field to DB and all related Warehouses & Users files.
I've got validation logic that works appropriately to determine if company_id or user_id is populated. But since the user_id field is being changed on the $_accessible property, my record is created without the user_id value when I am attempting to create such a record.
I have other tables in my app with similar setups working perfectly well (table.id, table.jobs_id, table.onsite_id) where job_id would be populated or onsite_is would be populated. But never both empty or both populated.
DB Table:
CREATE TABLE `warehouses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`company_id` int(11) NOT NULL DEFAULT '0',
`user_id` int(11) NOT NULL DEFAULT '0',
`created` datetime NOT NULL,
`create_user` int(10) unsigned NOT NULL DEFAULT '0',
`modified` datetime NOT NULL,
`modify_user` int(10) unsigned NOT NULL DEFAULT '0',
`costed` tinyint(1) NOT NULL DEFAULT '0',
`deleted` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `id` (`id`,`name`,`deleted`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
WarehousesTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('warehouses');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->hasMany('Inventory', ['foreignKey' => 'warehouse_id']);
$this->belongsTo('Companies', ['foreignKey' => 'company_id']);
$this->belongsTo('Users', ['foreignKey' => 'user_id']);
}
Warehouse.php
protected $_accessible = [
'name' => true,
'company_id' => true,
'user_id' > true,
'created' => true,
'create_user' => true,
'modified' => true,
'modify_user' => true,
'costed' => true,
'deleted' => true,
'inventory' => true
];
UsersTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('users');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Companies');
$this->setDisplayField('full_name');
$this->hasMany('Uploads', ['foreignKey' => 'user_id']);
$this->hasMany('Warehouses', ['foreignKey' => 'user_id']);
}
add.ctp
<div class="warehouses form">
<?php echo $this->Form->create($warehouse);?>
<fieldset>
<legend>Add Warehouse</legend>
<?php
echo $this->Form->control('name');
echo $this->Form->control('what_needed', array('type' => 'radio', 'options' => ['0' => 'Internal', 'C' => 'Customer', 'T' => 'Field Tech'], 'label' => 'Warehouse for internal, Customer or Field Tech?'));
echo $this->Form->control('company_id', array('empty' => 'Select a company', 'options' => $companies, 'label' => 'Company'));
echo $this->Form->control('user_id', array('empty' => 'Select a user', 'options' => $users, 'label' => 'User'));
echo $this->Form->control('costed', array('label'=>'Is Warehouse Costed?'));
echo "</fieldset>";
?>
</fieldset>
<?php
echo $this->Form->button(__('Submit'));
echo $this->Form->end();
?>
</div>
In the Warehouses controller add method, I've debugged the ->newEntity(), the ->request->getData() and the ->patchEntity(). That information is below.
Thanks in advance for any help. It is greatly appreciated.
debug($warehouse = $this->Warehouses->newEntity());
object(App\Model\Entity\Warehouse) {
'[new]' => true,
'[accessible]' => [
'name' => true,
'company_id' => true,
(int) 0 => false, //*why is this false? what happened to user_id*
'created' => true,
'create_user' => true,
'modified' => true,
'modify_user' => true,
'costed' => true,
'deleted' => true,
'inventory' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Warehouses'
}
debug($this->request->getData());
[
'name' => 'Blah Blah Trunk',
'what_needed' => 'T',
'company_id' => '',
'user_id' => '5',
'costed' => '0'
]
debug($this->Warehouses->patchEntity($warehouse, $this->request->getData()));
object(App\Model\Entity\Warehouse) {
'name' => 'Blah Blah Trunk',
'costed' => false,
'create_user' => (int) 2,
'modify_user' => (int) 2,
'[new]' => true,
'[accessible]' => [
'name' => true,
'company_id' => true,
(int) 0 => false, //*why is this false? what happened to user_id*
'created' => true,
'create_user' => true,
'modified' => true,
'modify_user' => true,
'costed' => true,
'deleted' => true,
'inventory' => true
],
'[dirty]' => [
'name' => true,
'costed' => true,
'create_user' => true,
'modify_user' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Warehouses'
}
Check your typo: 'user_id' > true
protected $_accessible = [
'name' => true,
'company_id' => true,
'user_id' > true, // <---------- you have typo here, must be 'user_id' => true
'created' => true,
'create_user' => true,
'modified' => true,
'modify_user' => true,
'costed' => true,
'deleted' => true,
'inventory' => true
];
You can easy detect errors in your code using some scripts, like cakephp/cakephp-codesniffer, phpstan,..
composer require --dev cakephp/cakephp-codesniffer
composer require --dev phpstan/phpstan
composer require --dev phpstan/phpstan-deprecation-rules
Add in your composer scripts like:
"scripts": {
"cs-check": "phpcs --colors -p --ignore=*.js --standard=vendor/cakephp/cakephp-codesniffer/CakePHP config/ src/ plugins/ tests/",
"cs-fix": "phpcbf --colors --ignore=*.js --standard=vendor/cakephp/cakephp-codesniffer/CakePHP config/ src/ plugins/ tests/",
"phpstan": "./vendor/bin/phpstan analyse src plugins --level=0",
"test": "phpunit --colors=always"
},
Run from terminal / console:
composer cs-check for check code style errors
composer cs-fix for quick fix some code style errors
composer phpstan for analysing code, start with --level 0

Controller doesn't get hasOne relationship data - Cakephp

I have a problem with related tables in CakePHP. I can't get the related table data include in the form.
I have two Entities. One of them is "Users" and the other one is "Subjects". Every User has a subject. Table "Subject" has foreign key idUser from Users table.
I added in UsersTable:
$this->hasOne('Subjects');
And I added in SubjectsTable:
$this->belongsTo('Users', [
'foreignKey' => 'idUser',
'joinType' => 'INNER'
]);
In the view (signup), I have this:
<div class="form-group">
<?php echo $this->Form->control('Subject.name',['label' => 'Asignatura','placeholder' => 'Ingrese asignatura','class' => 'form-control']) ?>
</div>
In the controller, I have this:
$user = $this->Users->patchEntity($user, $this->request->getData(),['associated' => 'Subjects']);
When I debug $user, I am getting this result:
\src\Controller\UsersController.php (line 113)
object(App\Model\Entity\User) {
'id' => '11111111',
'name' => 'Leo',
'firstlastname' => 'Messi',
'secondlastname' => 'Cuccittini',
'email' => 'leo.messi#gmail.com',
'password' => '$2y$10$E02nd/w89BDvgCyz36bQdeBbujOLrSdON1e6CD25aDYCP2VeLkNNm',
'role' => '2',
'[new]' => true,
'[accessible]' => [
'id' => true,
'name' => true,
'firstlastname' => true,
'secondlastname' => true,
'email' => true,
'password' => true,
'role' => true
],
'[dirty]' => [
'id' => true,
'name' => true,
'firstlastname' => true,
'secondlastname' => true,
'email' => true,
'password' => true,
'role' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Users'
}
So, I am not getting in the controller the data from Subject.
Any help, please.
Model
$this->hasOne('Subjects', [
'foreignKey' => 'userId'
]);
Controller:
$user = $this->User->get($id, ['contain' => ['Subjects']);
Entity/User.php
protected $_accessible = [
'subjects' => true
// ...
];
Form
https://book.cakephp.org/3.0/en/views/helpers/form.html#associated-form-inputs
Change: Subject.name to user.subject.name
<?php echo $this->Form->control('user.subject.name',['label' => 'Asignatura','placeholder' => 'Ingrese asignatura','class' => 'form-control']) ?>

Cakephp 3.x saving hasmany association

In my project I cannot able to save hasmany associated table in a single save. My association is given as follows
class ProductsTable extends Table
$this->hasMany('ProductImages', [
'foreignKey' => 'product_id',
'joinType' => 'INNER'
]);
class ProductImagesTable extends Table
$this->belongsTo('Products', [
'foreignKey' => 'product_id',
'joinType' => 'INNER'
]);
In my controller the code is as follows
debug($this->request->data); \src\Controller\Admin\ProductsController.php (line 81)
$product = $this->Products->patchEntity($product, $this->request->data, [
'associated' => [ 'ProductImages']
]);
debug($product);\src\Controller\Admin\ProductsController.php (line 86)
$this->Products->save($product);
The debug result is as follows
\src\Controller\Admin\ProductsController.php (line 81)
[
'name' => 'Lorem ipsum dolor',
'short_description' => ' Lorem ipsum dolor sit amet',
'description' => ' Lorem ipsum dolor sit amet ',
'sku' => 'NE132W',
'price' => '51',
'product_image' => [
(int) 0 => [
'default' => (int) 0,
'image_name' => 'product1',
'real_name' => '631-1478785843.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/631-1478785843.png'
],
(int) 1 => [
'default' => '1',
'image_name' => 'product2',
'real_name' => '140-1478785850.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/140-1478785850.png'
],
(int) 2 => [
'default' => (int) 0,
'image_name' => 'product3',
'real_name' => '416-1478785856.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/416-1478785856.png'
]
]
]
\src\Controller\Admin\ProductsController.php (line 86)
object(App\Model\Entity\Product) {
'name' => 'Lorem ipsum dolor',
'short_description' => ' Lorem ipsum dolor sit amet',
'description' => ' Lorem ipsum dolor sit amet, consectetur adipiscing elit. ',
'sku' => 'NE132W',
'price' => (float) 51,
'product_image' => [
(int) 0 => [
'default' => (int) 0,
'image_name' => 'product1',
'real_name' => '631-1478785843.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/631-1478785843.png'
],
(int) 1 => [
'default' => '1',
'image_name' => 'product2',
'real_name' => '140-1478785850.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/140-1478785850.png'
],
(int) 2 => [
'default' => (int) 0,
'image_name' => 'product3',
'real_name' => '416-1478785856.png',
'image_url' => 'http://cakephp-apps.com/mykipferl/img/Products/416-1478785856.png'
]
],
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'name' => true,
'short_description' => true,
'description' => true,
'sku' => true,
'price' => true,
'product_image' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
Somebody help me for this issue?
You have to use the appropriate property name for the association, which for hasMany associations is by default the plural, underscored variant of the association alias, so in your case product_images, not product_image.
See also
Cookbook > Database Access & ORM > Associations > HasMany Associations
Cookbook > Database Access & ORM > Saving Data > Saving HasMany Associations
Cookbook > Views > Helpers > Form > Field Naming Conventions

Resources