商品(りんご、ばなな、メロン)テーブルがあって、単品の商品とかセット商品とかをうまく組み合わせたい。
例えば。。

フルーツ盛り合わせ(りんごx1、ばななx2、メロンx1) とか

りんご盛り(りんごx20) とか

などなどとにかく複雑なものがあとから来ても大丈夫な設計を考えて、以下のような感じにしてみた。

数量のnumがポイント

数量のnumがポイント

productsテーブルをproductsテーブル自身と多対多で繋いで中間のテーブルはproducts_componentsとするで、数量は中間テーブルに ‘num’ として持っていたら何にでもなりそうな気がする。

具体的なCakePHPの話。

Modelは ProductsTable.phpを中心に以下のアソシエーションを貼る。

[Products]
    belongsToMany :  [Components]
    hasMany : [ProductsComponents]

ProductsTable

以下のように2つのアソシエーションを指定。

        $this->hasMany('ProductsComponents', [
            'className'=>'ProductsComponents',
            'foreignKey'=>'product_id'
        ]);

        $this->belongsToMany('Components', [
            'joinTable' => 'products_components',
            'className'=>'Components',
            'foreignKey'=>'product_id',
            'targetForeignKey' => 'component_id'
        ]);

(細かい個別の条件によるとおもうのですが、途中はまって’targetForeignKey’ が無いとうまく動かないケースがあって入れてますが実際はいらないかも><..)

belongsToMany : Components

これは’Components’って名前をつけているけど実態は ’Products’テーブルで、
Productsが、ProductsをbelongsToManyとするとおかしくなったので別名に。
実態はこんな感じ。

class ComponentsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);
        $this->setTable('products');
        $this->setDisplayField('name');
        $this->setPrimaryKey('id');

        $this->belongsToMany('Products', [
            'joinTable' => 'products_components',
            'className'=> 'Products',
            'foreignKey'=>'component_id',
            'targetForeignKey' => 'product_id'
        ]);
    }
}

hasMany : ProductsComponents

これは普通に定義のみ(あとでバリデーションとかetc入れる予定)

<?php
class ProductsComponentsTable extends Table
{

    public function initialize(array $config)
    {
        parent::initialize($config);
        $this->setTable('products_components');
        $this->setPrimaryKey('id');
    }
}

フォームとコントローラーでの保存とか。

基本belongsToMany(多対多)のばあいは、FormHelperでチェックボックスがだせるのですが、numを入力させたりがあるのでフォームは直に書いて行きます。

まずは belongsToManyのComponents用のフォーム
これはCakePHPが書き出すものを真似る感じで。最初のvalueが空のはチェックが無かった場合に値がUndefindにならない用。

<input type="hidden" name="components[_ids]" value="">
<input type="hidden" name="components[_ids][]" value="1"> アイテム1
<input type="hidden" name="components[_ids][]" value="2"> アイテム2
<input type="hidden" name="components[_ids][]" value="3"> アイテム3

で、もう一つそれぞれに対してnum用のフィールドが必要で、以下のように、‘productsComponents’ で指します。(と言うのは、上の多対多はProductsテーブルのsaveでまとめて保存できたが、中間テーブルに保存するnumがsaveの中では、一回で保存できなかったため、分けています。(afterSaveとかでどうにかとも考えたのですが、Controllerに書いて見通しが良いほうが簡単そうだったので。)

<input type="text" name="productsComponents[{component_id}][num]" value="1">個

component_id の部分は、セットに含まれるproductsのid (=componentsテーブルのid)
で、2つのフォームを合わせて以下のようにフォームを生成。

<input type="hidden" name="components[_ids]" value="">
<input type="hidden" name="components[_ids][]" value="1"> りんご x 
<input type="text" name="productsComponents[1][num]" value="1">個

保存のとこ。(Controller)

基本的には belongsToMany : Componentsのところは何も考えず、Productsをsave()すれば勝手に保存される。(削除してのも消してくれる)

で、中間テーブルのnumの保存方法は一旦Productsを保存してそのあと中間テーブルのnumを保存する手順にしています。

 $product = $this->Products->patchEntity($product, $this->request->getData(),['associated' =>  ['Suppliers','Statuses','Components']]);
            $connection = ConnectionManager::get('default');
            $connection->begin();
            if ($this->Products->save($product)) {
                if(!empty($product->productsComponents)){
                    foreach($product->productsComponents as $component_id => $item){
                        $query = $this->Products->ProductsComponents->query();
                        $query->update()
                            ->set(['num' => (int)$item['num']])
                            ->where(['product_id' => $id,'component_id'=>$component_id])
                            ->execute();
                    }
                }
                $this->Flash->success(__('The product has been saved.'));
                $connection->commit();
                return $this->redirect(['action' => 'index']);
            }else{
                //error
            }