Search code examples
cakephpsaveassociationshas-and-belongs-to-manycakephp-3.x

CakePHP 3.4 how to insert on a belongs to and has many relationship


On cakePHP 3.4, i have 3 tables with a belongs to and has many relationship: Ingredients, Products and IngredientsProducts:

class IngredientsTable extends Table
{
    public function initialize(array $config)
    {
        // Use through option because it looks like you 
        // have additional data on your IngredientsProducts table
        $this->belongsToMany('Products', [
            'through' => 'IngredientsProducts',
        ]);
    }
}

class ProductsTable extends Table
{
    public function initialize(array $config)
    {
        $this->belongsToMany('Ingredients', [
            'through' => 'IngredientsProducts',
        ]);
    }
 }

class IngredientsProductsTable extends Table
{
    public function initialize(array $config)
    {
        $this->belongsTo('Ingredients');
        $this->belongsTo('Products');
    }
} 

What I want to accomplish is that when I insert a new product, I would like to insert also the ingredient_id(s) and field qty of each ingredient that is related to that product, on the IngredientsProducts joiner table.

I've been reading the cookbook, and saw that when saving aditional data on the joiner table (in my case the field qty as stated below), you have to use the _joinData property, so my add view looks like this:

    <?php 

    $this->Form->create($product);
    // fields of the Products table
    echo $this->Form->control('name',array('class' => 'form-control'));
    echo $this->Form->control('retail_price');
    echo $this->Form->control('best_before');
    echo $this->Form->control('comments');

    // ingredient_id and qty fields of the ingredients_products table
    echo $this->Form->control('ingredients.0._joinData.ingredient_id',['options' => $ingredients);

    echo $this->Form->control('ingredients.0._joinData.qty');

    //and repetition of these last two as ingredients.1._joinData to ingredients.N._joinData

$this->Form->button(__('Save'));
    $this->Form->end();

    ?>

and on the controller the add method looks like this:

$product = $this->Products->newEntity();
        if ($this->request->is('post')) {

            $product = $this->Products->patchEntity($product, $this->request->getData(),[
                'associated' => ['Ingredients._joinData']
            ]);


            if ($this->Products->save($product)) {
                $this->Flash->success(__('The product has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The product could not be saved. Please, try again.'));
        }
        $this->set(compact('product'));
        $this->set('_serialize', ['product']);

However when I submit it doesn't save anything. The debug displays the following being posted:

object(App\Model\Entity\Product) {

    'name' => 'salada',
    'retail_price' => (float) 23,
    'best_before' => (int) 234,
    'comments' => 'wer',
    'directions' => 'werwewerer',
    'ingredients' => [
        (int) 0 => object(App\Model\Entity\Ingredient) {

            '_joinData' => object(Cake\ORM\Entity) {

                'ingredient_id' => (int) 4,
                'qty' => (float) 100,
                '[new]' => true,
                '[accessible]' => [
                    '*' => true
                ],
                '[dirty]' => [
                    'ingredient_id' => true,
                    'qty' => true
                ],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[invalid]' => [],
                '[repository]' => 'IngredientsProducts'

            },
            '[new]' => true,
            '[accessible]' => [
                '*' => true,
                'id' => false
            ],
            '[dirty]' => [
                '_joinData' => true
            ],
            '[original]' => [
                '_joinData' => [
                    'ingredient_id' => '4',
                    'qty' => '100'
                ]
            ],
            '[virtual]' => [],
            '[errors]' => [
                'name' => [
                    '_required' => 'This field is required'
                ]
            ],
            '[invalid]' => [],
            '[repository]' => 'Ingredients'

        },
        (int) 1 => object(App\Model\Entity\Ingredient) {

            '_joinData' => object(Cake\ORM\Entity) {

                'ingredient_id' => (int) 5,
                'qty' => (float) 200,
                '[new]' => true,
                '[accessible]' => [
                    '*' => true
                ],
                '[dirty]' => [
                    'ingredient_id' => true,
                    'qty' => true
                ],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[invalid]' => [],
                '[repository]' => 'IngredientsProducts'

            },
            '[new]' => true,
            '[accessible]' => [
                '*' => true,
                'id' => false
            ],
            '[dirty]' => [
                '_joinData' => true
            ],
            '[original]' => [
                '_joinData' => [
                    'ingredient_id' => '5',
                    'qty' => '200'
                ]
            ],
            '[virtual]' => [],
            '[errors]' => [
                'name' => [
                    '_required' => 'This field is required'
                ]
            ],
            '[invalid]' => [],
            '[repository]' => 'Ingredients'

        }
    ],
    '[new]' => true,
    '[accessible]' => [
        '*' => true,
        'id' => false
    ],
    '[dirty]' => [
        'name' => true,
        'retail_price' => true,
        'best_before' => true,
        'comments' => true,
        'directions' => true,
        'ingredients' => true
    ],
    '[original]' => [],
    '[virtual]' => [],
    '[errors]' => [],
    '[invalid]' => [],
    '[repository]' => 'Products'

} 

Does anybody happen to know how to make this work?

just in case here are the table structures on mySQL:

TABLE `Ingredients` (
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) NOT NULL,
  `measure_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Table products
TABLE `Products` (
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `retail_price` float NOT NULL,
  `best_before` int(11) NOT NULL,
  `comments` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



TABLE `ingredients_products` (
  `ingredient_id` int(11) NOT NULL,
  `product_id` int(11) NOT NULL,
  `qty` double NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Any help or directions would be kindly appreciated! Thanks


Solution

  • When saving fails, check the errors info

    Whenever saving an entity doesn't work, check the errors info:

    '[errors]' => [
        'name' => [
            '_required' => 'This field is required'
        ]
    ],
    

    Your validation rules define the Ingredients.name field as required, but it's not present in your form, hence saving fails.

    Supply the primary key on the target association

    Take a closer look at the examples in the docs for saving join data, the data structure looks a little different to yours.

    From your usage of ingredient_id I suspect that you want to link the product to existing ingredients, placing ingredient_id in the _joinData however is not how this works. Editing/linking existing records requires the primary key of the record to be present in either the special _ids key, or on the primary key field of the target association (Ingredients).

    So in your case where you want to store additional join data, you have to use the id property for the ingredient, that way the marshaller knows that it needs to load an existing record:

    echo $this->Form->control('ingredients.0.id', [
        // defining the type is required, as the input type
        // guessing only recognizes `_ids` fields, or fields
        // with `_id` appended as possible select types
        'type' => 'select'
        'options' => $ingredients
    ]);
    echo $this->Form->control('ingredients.0._joinData.qty');
    

    See also