R. Jordaan R. Jordaan - 2 months ago 12
SQL Question

How to check if an item belongs to a hasMany association before updating in CakePHP 3.2

What I am trying to do:

I have Estimates and Estimates have items "EstimateItems". When updating a Estimate the EstimateItems changed should update. (using patchEntity)

This is working with my current code, my only problem is that other users can edit the Estimate Items of other users when changing the primary key of a EstimateItem in the edit form, because when patching the existing EstimateItems CakePHP only looks at the primary key of the EstimateItem and doesn't take the association in consideration. Also it's still possible to edit the estimate_id of a EstimateItem while $protected estimate_id is set to false.

So what I need is CakePHP to validate that this EstimateItem belongs to the current association before updating or while trying to update.

I hope some one can tell me what I am doing wrong or what I am missing.


Current Query


UPDATE
estimate_items
SET
data = 'Test Query 1',
amount = 123456789,
tax_id = 3
WHERE
id = 3



Expected Query


UPDATE
estimate_items
SET
data = 'Test Query 1',
amount = 123456789,
tax_id = 3
WHERE
id = 3 AND estimate_id = 1


Current code:


Estimates -> Edit.ctp


<?php $this->Form->templates($formTemplates['default']); ?>
<?= $this->Form->create($estimate, ['enctype' => 'multipart/form-data']) ?>
<fieldset>
<legend><?= __('Offerte') ?></legend>

<?= $this->Form->input('reference', ['label' => __('#Referentie'), 'autocomplete' => 'off']) ?>
<?= $this->Form->input('client_id',
[
'type' => 'select',
'empty' => true,
'label' => __('Klant'),
'options' => $clients
]
)
?>

<?php

foreach($estimate->estimate_items as $key => $item){
?>
<div class="item">
<legend>Item</legend>
<?= $this->Form->hidden('estimate_items.'. $key .'.id') ?>
<?= $this->Form->input('estimate_items.'. $key .'.data', ['type' => 'text', 'label' => __('Beschrijving')]) ?>
<?= $this->Form->input('estimate_items.'. $key .'.amount', ['type' => 'text', 'label' => __('Bedrag'), 'class' => 'input-date']) ?>
<?= $this->Form->input('estimate_items.'. $key .'.tax_id',
[
'type' => 'select',
'empty' => true,
'label' => __('Belasting type'),
'options' => $taxes
]
)
?>
</div>
<?php
}

?>

<legend>Informatie</legend>
<?= $this->Form->input('date', ['type' => 'text', 'label' => __('Offerte datum'), 'autocomplete' => 'off']) ?>
<?= $this->Form->input('expiration', ['type' => 'text', 'label' => __('Verloop datum'), 'autocomplete' => 'off']) ?>
</fieldset>
<?= $this->Form->button(__('Save')); ?>
<?= $this->Form->end() ?>



Estimates Controller


namespace App\Controller;

use App\Controller\AppController;
use Cake\Event\Event;
use Cake\ORM\TableRegistry;

class EstimatesController extends AppController
{
public function edit($id){
$associated = ['EstimateItems'];

$estimate = $this->Estimates->get($id, ['contain' => $associated]);

$this->log($estimate);
if($this->request->is(['patch', 'post', 'put'])) {

$estimate = $this->Estimates->patchEntity($estimate, $this->request->data, [
'associated' => $associated
]);

$estimate->total = '0';
$this->log($estimate);
$this->log($this->request->data);

if($this->Estimates->save($estimate, ['associated' => $associated])){
$this->Flash->success(__('De offerte is bijgewerkt'));
return $this->redirect(['action' => 'index']);
}
}

$this->set('taxes', $this->Estimates->Taxes->find('list', [ 'keyField' => 'id', 'valueField' => 'tax_name' ]));
$this->set('clients', $this->Estimates->Clients->find('list', [ 'keyField' => 'id', 'valueField' => 'companyname' ]));
$this->set('estimate', $estimate);
}
}



EstimatesTable


<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\RulesChecker;
use Cake\ORM\Rule\IsUnique;

class EstimatesTable extends Table
{
public function initialize(array $config)
{
$this->addAssociations([
'hasOne' => ['Taxes'],
'belongsTo' => ['Companies', 'Clients'],
'hasMany' => ['EstimateItems' => [
'foreignKey' => 'estimate_id'
]]
]);

}

public function buildRules(RulesChecker $rules){

// A Node however should in addition also always reference a Site.
$rules->add($rules->existsIn(['estimate_id'], 'EstimateItems'));

return $rules;
}

}



EstimateItem Entity


<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class EstimateItem extends Entity
{
protected $_accessible = [
'*' => false,
'data' => true,
'amount' => true,
'tax_id' => true,
'unit_id' => true
];
}



EstimateItemsTable


<?php
namespace App\Model\Table;

use Cake\ORM\Entity;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\RulesChecker;
use Cake\ORM\Rule\IsUnique;
use Cake\ORM\Query;


class EstimateItemsTable extends Table
{

public function initialize(array $config)
{
$this->addAssociations([
'belongsTo' => ['Estimates' => ['foreignKey' => 'estimate_id']],
'hasOne' => ['Taxes' => ['foreignKey' => 'tax_id']]
]);
}



Estimate Entity


<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class Estimate extends Entity
{

/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array
*/
protected $_accessible = [
'*' => false,
'id' => false,
];
}

Answer

Markstory Replied to me on github with a solution credits to him: https://github.com/cakephp/cakephp/issues/9527

In Model/Table/EstimateItemsTable.php


<?php
namespace App\Model\Table;

use Cake\ORM\RulesChecker;
....
class EstimateItemsTable extends Table
{
....
public function buildRules(RulesChecker $rules){
        $rules->addUpdate(function($entity) {

          if (!$entity->dirty('estimate_id')) {
            return true;
          }
          return $entity->estimate_id == $entity->getOriginal('estimate_id');
        }, 'ownership', ['errorField' => 'estimate_id']);

        return $rules;
    }
}