0
votes

I am currently using CakePHP to serve a crud based api for some ticketing logic I wrote. I am running into an issue where I am attempting to change a belongsTo association and data within the new association and it is not persisting.

The controller doing the persisting looks like this:

<?php

class TasksController extends Cake\Controller\Controller
{
    public function initialize()
    {
        parent::initialize();
    }

    public function edit(array $ids): void
    {
        $validationErrors = [];

        $tasks = $this->Tasks->find('all')
            ->contain($this->setAssociations($query))
            ->where([$this->Model->getAlias().'.id IN' => $ids]);

        foreach ($tasks as $id => $task) {
            $this->Tasks->patchEntity($task, $this->request->getQuery()[$id], [
                'associated' => ['Asset']
            ]);
        }

        if ($this->Tasks->saveMany($tasks)) {
            $this->response = $this->response->withStatus(200);
        } else {
            // Handle errors
        }

        // Render json output for success / errors
        $this->set($this->createViewVars(['entities' => $tasks], $validationErrors));
    }
}

The association for an asset in the task table looks like this:

<?php

class TasksTable extends Cake\ORM\Table
{
    public function initialize(array $config)
    {
        $this->belongsTo('Asset', [
            'className' => 'Assets',
            'foreignKey' => 'asset_id',
            'joinType' => 'LEFT'
        ]);
    }
}

These build rules are attached to the asset table:

<?php
    
class AssetsTable extends Cake\ORM\Table
{
    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->isUnique(['number', 'group_id'], 'The Number you selected is in use'));
        $rules->add($rules->isUnique(['name', 'group_id'], 'The Name you selected is in use'));
    }
}

The body of the request I am sending looks like this:

{
    "421933": {
        "description": "This task was edited by the api!",
        "asset": {
            "id": "138499",
            "description": "This asset was edited by they api!",
            "name": "105",
            "number": "6"
        }
    }
}

Basically the name 105 and number 6 are being flagged as not being unique, because they are already set to those values on asset 138499. The query is is instead trying to edit name 105 and number 6 into the Asset entity that is presently associated with the Task entity (645163), which is triggering the isUnquie build rules to fail.

You can see this by printing the $tasks before the saveMany call in the above controller:

Array
(
    [0] => App\Model\Entity\Task Object
        (
            [id] => 421933
            [description] => This task was edited by the api!
            .....
            [asset] => App\Model\Entity\Asset Object
                (
                    [id] => 645163
                    [name] => 105
                    [description] => This asset was edited by they api!
                    [number] => 6
                    ....
                )
         
        )

)

It seems like this editing Asset 138499 as an association of Task 421933 should work as it is appears to be possible to save belongsTo associations in this fashion in the CakePHP docs, as documented here:

<?php

$data = [
    'title' => 'First Post',
    'user' => [
        'id' => 1,
        'username' => 'mark'
    ]
];

$articles = TableRegistry::getTableLocator()->get('Articles');
$article = $articles->newEntity($data, [
    'associated' => ['Users']
]);

$articles->save($article);

Is it possible to associate a belongsTo association and edit it in the same transaction? If so how should my request or code be structured differently?

Thanks!

1
I would start with debugging the entities after patching them, that might spill some clues. btw, there's also patchEntities() for patching multiple entities at once. ps. your saveMany() calls seem to reference only the last entity, that looks wrong and should make saving not work at all - please make sure that you're not modifying your code too much (if at all) when posting it here, as that might hide possible problems!ndm
@ndm Ok, I think I see what is going on. I am trying to assign asset 138494 to the task when there is another asset 645163 assigned to it as well as edit the values in the same call. saveMany is trying to overwrite the name and number field over asset 645163's values rather than replacing it with asset 138494, and this is triggering the isunique constraint. So the body in my 4th piece of code I posted is not properly triggering 138494 to be assigned to the task. It seems like this should be possible, looking at the doc: book.cakephp.org/3/en/orm/…space97
But that heading is specivid for a hasMany relationship. Do i need to do this differently for belongsTo? I updated the code in my answer per your suggestion, thanks!space97
That sounds a little bit too abstract to me without seeing the exact data/entities, but AFAIR, linking a 1:1 associated record (you don't have a hasMany association there) and editing it at the same time doesn't work, as the marshaller will only merge the data, while it would have to load the new record, replace the existing entity, and merge the data into the new entity for this to work properly.ndm
It certainly won't hurt knowing what you're talking about ;) Your association already is belongsTo, so I'm not sure what you mean by "changing" that?ndm

1 Answers

1
votes

As hinted in the comments, you're running into a limitation of the marshaller there, for which there is no overly startightforward workaround yet (you might want to open an issue over at GitHub, maybe someone want's to take up the task of adding support for this type of marshalling/saving).

The example in the book isn't really the best to go by here, in fact I'd personally remove/change it, it would only work when mass assigning the primary key is allowed (which by default for baked entities it isn't), and it would also only work when creating new records, ie when the entity that is to be saved is marked as "new". However doing this would introduce all sorts of quirks, like validation rules would see the data as "create" (new), application rules would see the entity as "update" (not new), but not receive any of the existing/original data, same for other saving stage callbacks, which would mess up stuff like auditing or the like, basically anything where you need the new data and the original data.

Long story short, even if you could get it somewhat working like with that example, you'd just introduce other problems further down the road, so better forget about that for now. The only workaround-ish thing that comes to my mind right now, would be comparing the given asset primary key, loading the existing asset entity manually, and replacing the already loaded one before patching in your data, something along the lines of this:

foreach ($tasks as $task) {
    $data = $this->request->getData($task->id);
    $assetId = (int)\Cake\Utility\Hash::get($data, 'asset.id');
    if ($task->asset->id !== $assetId) {
        $task->asset = $this->Tasks->Asset->get($assetId);
    }

    $this->Tasks->patchEntity($task, $data, [
        'associated' => ['Asset']
    ]);
}

Note that I've changed your getQuery() usage to getData(), as it seemed unlikely that you're passing that data via the query string.