EvgenyGavrilov/yii2-many-to-many

View on GitHub
src/ManyToManyBehavior.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php

namespace EvgenyGavrilov\behavior;

use yii\base\Behavior;
use yii\base\UnknownPropertyException;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use yii\db\Query;

/**
 * Behavior of relation update in junction table
 *
 * `To update the related data`, the relation with the junction table should be described  in the model.
 *
 * Behavior doesn't use transaction. Use this method to do
 * [transactions()](http://www.yiiframework.com/doc-2.0/yii-db-activerecord.html#transactions%28%29-detail) in the model or other way.
 *
 * Usage
 * ------------
 *
 * ```php
 * class User extends ActiveRecord
 * {
 *      public function behaviors()
 *      {
 *          return [
 *              'crossTable' => [
 *                  'class' => 'common\behaviors\ManyToMany'
 *              ]
 *          ];
 *      }
 *
 *      public function getGroups()
 *      {
 *          return $this->hasMany(Group::className(), ['id' => 'group_id'])->viaTable('user_group', ['user_id' => 'id']);
 *      }
 * }
 *
 * $model = User::find(1);
 * // Add new or update relation
 * $model->setRelated('groups', [1]);
 * // or
 * $model->setRelated('groups', [1, 2]);
 * $model->save();
 *
 * // Add or update the data with the remove old relations
 * $model->setRelated('groups', [1, 2], true);
 * $model->save();
 *
 * // Delete all relations
 * $model->setRelated('groups', [], true);
 * $model->save();
 * ```
 *
 * @package common\behaviors
 * @author scorpion
 */
class ManyToManyBehavior extends Behavior
{
    /**
     * Object parent.
     * @var ActiveRecord
     */
    public $owner;

    /**
     * Collection of data on junction table
     * @var mixed[]
     */
    private $_related = [];

    /**
     * Delete all records.
     * @param string $name
     */
    private function deleteAll($name)
    {
        $db = $this->owner->getDb();
        $primaryKeyValue = $this->owner->getPrimaryKey();
        $meta = $this->_related[$name]['meta'];
        $db
            ->createCommand()
            ->delete(
                $meta['tableName'],
                [
                    $meta['foreignKey'] => $primaryKeyValue,
                ]
            )
            ->execute();
    }

    /**
     * @param string $name
     * @return array
     */
    private function getIds($name)
    {
        $primaryKeyValue = $this->owner->getPrimaryKey();
        $meta = $this->_related[$name]['meta'];
        $ids = $this->_related[$name]['ids'];
        $query = new Query();
        $res = $query
            ->from($meta['tableName'])
            ->select($meta['remoteKey'])
            ->where([
                $meta['foreignKey'] => $primaryKeyValue,
                $meta['remoteKey'] => $ids
            ])->column($this->owner->getDb());
        return array_diff($ids, $res);
    }

    /**
     * Check the existence of relation
     * @param string $name
     * @return bool
     */
    public function issetRelation($name)
    {
        $getter = 'get' . $name;
        return (method_exists($this->owner, $getter) && $this->owner->$getter() instanceof ActiveQuery);
    }

    /**
     * Get meta data of the junction table
     *
     * The array consists of three keys:
     *
     * 1. tableName - the name of the junction table
     * 2. foreignKey - name field, make a relation with the current model
     * 3. remoteKey - name field connecting two tables
     *
     * @param string $name
     * @return mixed[]
     */
    private function getRelationMeta($name)
    {
        $query = $this->owner->getRelation($name);
        $remoteKey = array_values($query->link);
        $remoteKey = reset($remoteKey);
        $foreignKey = array_keys($query->via->link);
        $foreignKey = reset($foreignKey);
        return [
            'foreignKey' => $foreignKey,
            'remoteKey' => $remoteKey,
            'tableName' => reset($query->via->from),
        ];
    }

    /**
     * @inheritdoc
     */
    public function events()
    {
        return [
            ActiveRecord::EVENT_AFTER_INSERT => 'updateRelations',
            ActiveRecord::EVENT_AFTER_UPDATE => 'updateRelations',
        ];
    }

    /**
     * Add new data for the junction table
     *
     * If flag 'deleteOld' is set on the true, old relations are deleted.
     * If flag 'deleteOld' is set on the false, then only new relations are added
     *
     * @param string $name
     * @param int[] $ids
     * @param bool $deleteOld
     * @throws UnknownPropertyException
     */
    public function setRelated($name, array $ids, $deleteOld = false)
    {
        if (!$this->issetRelation($name)) {
            throw new UnknownPropertyException('Setting unknown property: ' . get_class($this->owner) . '::' . $name);
        }
        $this->_related[$name] = [
            'deleteOld' => $deleteOld,
            'ids' => $ids,
            'meta' => $this->getRelationMeta($name),
        ];
    }

    /**
     * Junction table update
     */
    public function updateRelations()
    {
        foreach ($this->_related as $nameRelation => $data) {
            $db = $this->owner->getDb();
            $meta = $data['meta'];
            $primaryKeyValue = $this->owner->getPrimaryKey();
            if ($data['deleteOld']) {
                $this->deleteAll($nameRelation);
                $ids = $data['ids'];
            } else {
                $ids = $this->getIds($nameRelation);
            }
            foreach ($ids as $id) {
                $db->createCommand()->insert(
                    $meta['tableName'],
                    [
                        $meta['foreignKey'] => $primaryKeyValue,
                        $meta['remoteKey'] => $id
                    ]
                )->execute();
            }
        }
    }
}