3

I have an application in CakePHP 4 and am having problems saving associated model data. I have read Saving Associated Data in the Cake documentation but it's very unclear how this applies especially in my use-case.

The application has 3 tables which are relevant to this question:

  • items
  • sizes
  • items_sizes_wanted

The application allows users to request items of clothing (items) and the form to input/save such an item has a dropdown of different sizes (sizes). Each size has a unique ID. A user can select one or more size when saving an item. The items_sizes_wanted table is supposed to hold one (or more) rows depending on the sizes the user selected, with the corresponding item ID. For example if they saved sizes 2, 3 and 4 for Item 999 there would be 3 rows in this table:

size_id | item_id
--------|---------
2       | 999
3       | 999
4       | 999

The code has been baked and the associations in the Table classes look ok:

// src/Model/Table/ItemsSizesWantedTable.php
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('items_sizes_wanted');

        $this->belongsTo('Items', [
            'foreignKey' => 'item_id',
            'joinType' => 'INNER',
        ]);
        $this->belongsTo('Sizes', [
            'foreignKey' => 'size_id',
            'joinType' => 'INNER',
        ]);
    }

The Entity class for the item also looks ok:

// src/Model/Entity/Item.php

// @property \App\Model\Entity\ItemsSizesWanted[] $items_sizes_wanted

protected $_accessible = [
    // ...
        'items_sizes_wanted' => true,
    // ...
];

In the form where the item gets saved (src/templates/Items/add.php) I have used the Form Helper and named it using dot notation:

<?php
// Note that $sizes is an array of key/value pairs from the
// 'sizes' table.
?>
<?= $this->Form->control('items_sizes_wanted.size_id', ['options' => $sizes, 'multiple' => 'multiple']) ?>

When rendered in the browser this produces a valid array syntax name. The <option>'s rendered inside all have valid ID's, i.e. the ones from the sizes table.

<select name="items_sizes_wanted[size_id]" multiple="multiple">

When I save the data in my Controller (src/Controller/ItemsController.php) using the following:

public function add()
{
    $item = $this->Items->newEmptyEntity();

    if ($this->request->is('post')) {
        $item = $this->Items->patchEntity($item, $this->request->getData());

        // Edit: some of the entity properties are manually set at this point, e.g.
        $item->item_status = 'Wanted';

        if ($this->Items->save($item)) {
            $this->Flash->success(__('Your item has been listed.'));
        }
    }
}

The data is saved correctly to the items table and the flash success message, "Your item has been listed." is displayed in the browser.

But - no data is written to items_sizes_wanted.

I'm unsure why this is. The linked docs don't specifically say how the Form Helper should be used, so I'm assuming my syntax for that form field is correct, but it might not be.

If I debug the entity after pressing Save using debug($item); die; in the Controller it has 'items_sizes_wanted' => [ ] even though I selected multiple size options using the form.

Please can somebody help as I'm lost as to what's going wrong here?

Andy
  • 5,142
  • 11
  • 58
  • 131
  • If `items_sizes_wanted` is an empty array in the patched entity, that's why it's not saving anything. What does your `->getData()` structure look like? I do note that in at least one place you've referenced "ItemSizesWanted" instead of "ItemsSizesWanted". – Greg Schmidt Oct 18 '21 at 17:14
  • The discrepancy between the spellings of `ItemsSizesWanted` is a typo; I've corrected that. If I do `debug($this->request->getData());` it's giving `'items_sizes_wanted' => [ 'size_id' => '20', ],` but this is wrong because I've selected multiple sizes using the form. It looks like it's only passing 1 size. By the time I get to `debug($item);` it has become any empty array as per the question. The only thing I was doing between `patchEntity` and `debug($item);` was setting some properties on the item, e.g. `$item->item_status = 'Wanted';`. I'll add that to the question to clarify. – Andy Oct 19 '21 at 11:05
  • Just want to clarify, when you're adding an item, you're adding just one at a time? Like, the user cilcks the item, and it goes to a page where they select the specific size they want of that item, and that's supposed to get added to their cart? – Greg Schmidt Oct 28 '21 at 01:48
  • If that's the case, please try naming your input controls like `items_sizes_wanted[0].size_id`, and ensure that your association from `Items` to `ItemsSizesWanted` does NOT have a `saveStrategy` of `replace`. (No explicit `saveStrategy` will default to `append`, which is what you want.) – Greg Schmidt Oct 28 '21 at 01:51
  • @GregSchmidt the use-case is an application where people can request items of clothing that they're looking for - it's quite unconventional and there isn't actually a shopping cart. What happens is the user goes to a page (`/items/add`) where they see a form to enter details about the clothing they want - the majority of these details are stored in the `Items` table. However - because clothes sizing is complex the user is allowed to select *one or more* Sizes (using the options from the `sizes` table) and on save those selections should go in `items_sizes_wanted`. – Andy Oct 28 '21 at 14:30
  • Perhaps `items_sizes_wanted.size_id[]` then? – Greg Schmidt Oct 28 '21 at 14:52
  • But I expect you'll need to do some pre-processing on the incoming data before patching the item entity with it. – Greg Schmidt Oct 28 '21 at 14:53
  • I never got to the bottom of it, I gave up because this was so tedious. In the end I just made it so the user could only select 1 size and stored it in the `items` table. I believe there are two problems (if anyone reads this in future): one is the naming of the form fields, the other is the bit that @GregSchmidt has alluded to about "need to do some pre-processing" although it's unclear what exactly. This last bit is what I find most tedious. I'm sure in older versions of Cake it made things like this simple and you didn't need to do anything providing the models were configured correctly. – Andy Oct 29 '21 at 11:29
  • Older versions of Cake didn't make this any simpler. The problem is that you're basically making a join table that has more than just the two ID fields in it, so each of those records needs to be populated with things like the user ID, but when you use a multi-select for one of the IDs there isn't any way to specify those other things in the form. That's what needs to be added via post-processing. – Greg Schmidt Oct 29 '21 at 14:04

3 Answers3

1

Disclaimer: I do not know CakePHP well, but I think I either know the solution, or can at least point you in the right direction.

The reason you're only getting the one selected size, instead of multiple, is because the generated input fields are named items_sizes_wanted[size_id], however, in order for PHP to parse multiple values into an array, they need to be named items_sizes_wanted[size_id][]. When the request parameter ends with [], then PHP will properly parse all request properties into an array.

For example: Here's var_dump($_POST); of a request containing the POST body of items_sizes_wanted[size_id][]=A&items_sizes_wanted[size_id][]=B&items_sizes_wanted[size_id][]=C

array (size=1)
  'items_sizes_wanted' => 
    array (size=1)
      'size_id' => 
        array (size=3)
          0 => string 'A' (length=1)
          1 => string 'B' (length=1)
          2 => string 'C' (length=1)

Compare that to a POST body of items_sizes_wanted[size_id]=A&items_sizes_wanted[size_id]=B&items_sizes_wanted[size_id]=C (notice the empty braces at the end of each have been removed):

array (size=1)
  'items_sizes_wanted' => 
    array (size=1)
      'size_id' => string 'C' (length=1)

This is the part where I'm less familiar with CakePHP. I looked over the code for CakePHP's FormHelper, and based on the template code, I think you need to change your form code in add.php to be something like this (reformatted for readability):

<?php
// Note that $sizes is an array of key/value pairs from the
// 'sizes' table.
?>
<?= 
$this->Form->control(
    'items_sizes_wanted.size_id', 
    [
        'options' => $sizes, 
        'multiple' => 'multiple'
        'type' => 'selectMultiple'
    ]
) 
?>

Based on the __call() method in FormHelper, You might also be able to write it like this:

$this->Form->selectMultiple(
    'items_sizes_wanted.size_id', 
    [
        'options' => $sizes, 
        'multiple' => 'multiple'
    ]
);

However, I'm not familiar with the nuances between creating a control($fieldName, $options) and inputType($fieldName, $options), so they might produce different outputs.

404 Not Found
  • 3,635
  • 2
  • 28
  • 34
  • I'll award the bounty to this with the caveat that I haven't actually tested it. In the end I gave up because it was proving so tedious to do. The part about the naming of the form fields is absolutely correctly - I actually managed to figure this out after posting the question. What's less clear is how you patch that data (e.g. using `patchEntity()`) such that it gets saved to the correct table. This answer is definitely along the right lines but I haven't tried it and probably never will. A framework that makes things like this difficult isn't fit for purpose. This should be incredibly easy – Andy Oct 29 '21 at 11:32
0

I'm currently working in a project in CakePHP 4.x. My project also have many to many associations and it saves ok in the tables, but CakePHP baked it quite differently from yours. Let me show you the differences, maybe it is of some help.

I'll "translate" the names of my entities, tables, etc., to the ones used in your question, ok?

First, a brief: in my project, cake didn't bake models (entity and table) for the relational table. The relational table don't have its own models, and is only refered to in the initialize method from the ItemsTable and WantedSizesTable. There are also minor changes in the Item and WantedSize entities and in the view.

Second, your entity names doesn't comply with Cake's naming conventions, which can lead to many issues. This can even be the cause to the problems you're enduring now. I have changed some names to comply with them, but I'd suggest to you to read it thoroughly: https://book.cakephp.org/4/en/intro/conventions.html.

Third and more important, lets start.

My many-to-many relational mySQL tables doesn't have their own Table models. My SQL does indeed have a items_wanted_sizes table, but the CakePHP project does NOT have corresponding models called ItemsWantedSizesTable nor ItemsWantedSizes. It does have ItemsTable and WantedSizesTable tables and Item and WantedSize entities, and it's all.

Let's see the Table Models. The relational mySQL table items_wanted_sizes is refered only in the tables initialize method of both table models in PHP, like this:

// ItemsTable.php
public function initialize(array $config): void
{
    parent::initialize($config);

    $this->setTable('items');
    $this->setDisplayField('item_name');
    $this->setPrimaryKey('id');
    
    // ...
    // Other associations...
    // ...
    
    // The relational mysql table only shows here:
    $this->belongsToMany('WantedSizes', [
        'foreignKey' => 'item_id',  // Item Id field from the relational table
        'targetForeignKey' => 'wanted_size_id', // Size Id field from the relational table
        'joinTable' => 'items_wanted_sizes',
    ]);
}

The same happens on WantedSizesTable:

// WantedSizesTable.php
public function initialize(array $config): void
{
    parent::initialize($config);

    $this->setTable('wanted_sizes');
    $this->setDisplayField('wanted_size_name');
    $this->setPrimaryKey('id');
    
    // ...
    // Other associations...
    // ...
    
    // The relational mysql table only shows here:
    $this->belongsToMany('Items', [
        'foreignKey' => 'wanted_size_id',  // Size Id fieldname from the relational table
        'targetForeignKey' => 'item_id', // Item Id fieldname from the relational table
        'joinTable' => 'items_wanted_sizes',
    ]);
}

Regarding to the entities models, I also don't have a relational entity model. Both Item and WantedSize entity models refer to each other, but, contrary to your case, they don't refer to the relational table (only to each other):

// src/Model/Entity/Item.php

// @property \App\Model\Entity\WantedSize[] $wanted_sizes // NOT item_wanted_sizes

protected $_accessible = [
    // ...
        'wanted_sizes' => true, // NOT item_wanted_sizes
    // ...
];

Same in WantedSize:

// src/Model/Entity/WantedSize.php

// @property \App\Model\Entity\Item[] $items // NOT item_wanted_sizes

protected $_accessible = [
    // ...
        'items' => true, // NOT item_wanted_sizes
    // ...
];

Now we saw our models, lets jump the add (or edit) action view. With the associations correctly set, I only needed to do this:

// src/templates/Items/add.php

echo $this->Form->control('wanted_sizes._ids', ['options' => $wantedSizes]);

I didn't even needed to tell FormHelper it's a multiselect, because it is in the table configurations.

The HTML generated is quite different from yours (like 404 also answered above):

<select name="wanted_sizes[_ids][]" multiple="multiple" id="wanted-sizes-ids">
<option value="1">Some wanted size...</option>
<!-- ... -->
</select>

This worked perfectly fine for me, saving data in the relational table in mysql.

César Rodriguez
  • 296
  • 5
  • 16
0

In Cakephp4 one thing to check. If the entities are not showing the associated data after being patched prior to saving. You can test by dumping the entity after its patched in the controller.The associated data should show there.

$discount = $this->Discounts->patchEntity($discount, $this->request->getData());
dd($discount);

Check the Entity. Is the associated data in the $_accessible array? The fields that you update need to be in this array but also the associated models/tables.

class Discount extends Entity
{

    protected $_accessible = [
...
        'products' => true,
...
    ];
}

https://api.cakephp.org/4.0/class-Cake.ORM.Entity.html#$_accessible

Jubbs
  • 11
  • 3
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 13 '22 at 23:01