Saving hasMany associations with additional data

I have a CakePHP 5 app, with two tables - Titles and Series.

  • Series hasMany Titles,
  • Titles table has a series_id property
  • Titles also have series_number property

Imagine I have three titles, The Fellowship of the Ring, The Two Towers and The Return of the King. The titles are already in the DB, and I want to make a new Series. So I send some data to the endpoint like this:

{
    "name": "The Lord of the Rings",
    "titles": [
        { "id": 1, "series_number": 1 },
        { "id": 2, "series_number": 2 },
        { "id": 3, "series_number": 3 }
    ]
}

I want the new Series to update the existing titles, with the new series_id and the series_number property. I can’t just use _ids for this cause I have additional data to save.

My save endpoint looks like this:

    /**
     * Add method
     *
     * @return void
     * @throws \Cake\Http\Exception\InternalErrorException
     */
    public function add(): void
    {
        $this->request->allowMethod(['post']);

        $series = $this->Series->newEntity($this->request->getData(), ['associated' => ['Titles']]);
        if ($this->Series->save($series)) {
            $series = $this->Series->get($series->id, contain: ['Titles']);
        } else {
            throw new InternalErrorException('Could not save the series. Please try again later.');
        }

        $this->set(compact('series'));
        $this->viewBuilder()->setOption('serialize', 'series');
    }

However, when I process this request instead of updating the titles with ids 1,2,3 it just tries to create new ones. If I try to edit an existing series with titles, this request does work as the titles association is already contained on the entity I guess? But if I try to create a new series or update one that has no titles, it simply makes new ones.

Any help would be appreciated! Thanks.

Managed to get this working with an afterMarshal hook but it feels very hacky somehow? I would certainly be happy to find a more elegant solution!


    /**
     * @param \Cake\Event\EventInterface $event event
     * @param \Cake\Datasource\EntityInterface $entity event
     * @param \ArrayObject $data data
     * @param \ArrayObject $options options
     * @return void
     */
    public function afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $data, ArrayObject $options): void
    {
        if (!empty($data['titles'])) {
            $titleIds = array_map(fn($title) => $title['id'], $data['titles']);
            $titles = $this->Titles->find()->where(['Titles.id IN' => $titleIds]);
            $titles = $this->Titles->patchEntities($titles, $data['titles']);
            $entity->titles = $titles;
        }
    }

You might try setting Titles.id to be an accessible field

public function add(): void
    {
        $this->request->allowMethod(['post']);
        $series = $this->Series->newEmptyEntity();
        $series = $this->Series->patchEntity($series, $this->request->getData(), [
            'associated' => [
                'Titles' => [
                    'accessibleFields' => [
                        // turns insert into update
                        'id' => true
                    ],
                    // This turns off validation but you can edit or create a custom validation rule 
                    // to suit the situation
                    'validate' => false,
                ],
            ],);

        if ($this->Series->save($series)) {
            $series = $this->Series->get($series->id, contain: ['Titles']);
        } else {
            throw new InternalErrorException('Could not save the series. Please try again later.');
        }

        $this->set(compact('series'));
        $this->viewBuilder()->setOption('serialize', 'series');
    }

That did the trick, thanks!

1 Like