<commit_before><?php

namespace notamedia\relation;

use yii\base\Behavior;
use yii\base\Exception;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use yii\helpers\ArrayHelper;
use Yii;

/**
 * Behavior for support relational data management.
 *
 * - Insert related models from POST array.
 * - Delete related models from database which not exist in POST array
 * - Skip related models which already exist in database with same attributes
 * - Rollback database changes, if relational model save/delete error occurred
 * - Support one-to-one, one-to-many and many-to-many relations
 *
 * This behavior uses getters for relational attribute in owner model, such getters must return `ActiveQuery` object.
 * If you use string values in ON condition in `ActiveQuery` object, then this behavior will throw exception.
 *
 * For support transactions method transactions() in owner model must be defined:
 * ```php
 * <?php
 *
 * public function transactions()
 * {
 *      return [
 *          'default' => static::OP_ALL
 *      ];
 * }
 * ```
 *
 * @property ActiveRecord $owner
 */
class RelationBehavior extends Behavior
{
    /**
     * @var array Relation attributes list.
     */
    public $relationalFields = [];

    /**
     * @var array Relation attributes data.
     */
    protected $relationalData = [];

    /**
     * Process owner-model before save event.
     *
     * @return bool
     *
     * @throws Exception
     */
    public function beforeSave()
    {
        $this->loadData();

        return $this->validateData();
    }

    /**
     * Process owner-model after save event. Save models.
     *
     * @throws Exception
     * @throws \yii\db\Exception
     */
    public function afterSave()
    {
        $this->saveData();
    }

    /**
     * Permission for this behavior to set relational attributes.
     *
     * {@inheritdoc}
     */
    public function canSetProperty($name, $checkVars = true)
    {
        return in_array($name, $this->relationalFields) || parent::canSetProperty($name, $checkVars);
    }

    /**
     * Setter for relational attributes. Called only if attribute is exist in POST array. Method check value format
     * and put it to $this->relationalData array.
     *
     * {@inheritdoc}
     */
    public function __set($name, $value)
    {
        if (in_array($name, $this->relationalFields)) {
            if (!is_array($value) && !empty($value)) {
                $this->owner->addError($name,
                    Yii::$app->getI18n()->format(
                        Yii::t('yii', '{attribute} is invalid.'),
                        ['attribute' => $this->owner->getAttributeLabel($name)],
                        Yii::$app->language
                    )
                );
            } else {
                $this->relationalData[$name] = ['data' => $value];
            }
        } else {
            parent::__set($name, $value);
        }
    }

    /**
     * Load relational data from owner-model getter.
     *
     * - Create related ActiveRecord objects from POST array data.
     * - Load existing related ActiveRecord objects from database.
     * - Check ON condition format.
     * - Get ActiveQuery object from attribute getter method.
     *
     * Fill $this->relationalData array for each relational attribute:
     *
     * ```php
     * $this->relationalData[$attribute] = [
     *      'newModels' => ActiveRecord[],
     *      'oldModels' => ActiveRecord[],
     *      'activeQuery' => ActiveQuery,
     * ];
     * ```
     *
     * @throws Exception
     */
    protected function loadData()
    {
        /** @var ActiveQuery $activeQuery */
        foreach ($this->relationalData as $attribute => &$data) {

            $getter = "get" . ucfirst($attribute);
            $data['activeQuery'] = $activeQuery = $this->owner->$getter();
            $data['newModels'] = [];
            $class = $activeQuery->modelClass;

            if (!ArrayHelper::isAssociative($activeQuery->on) && !empty($activeQuery->on)) {
                Yii::$app->getDb()->getTransaction()->rollBack();
                throw new Exception('ON condition for attribute ' . $attribute . ' must be associative array');
            }

            $params = !ArrayHelper::isAssociative($activeQuery->on) ? [] : $activeQuery->on;

            if ($activeQuery->multiple) {
                if (empty($activeQuery->via)) {
                    // one-to-many
                    foreach ($activeQuery->link as $childAttribute => $parentAttribute) {
                        $params[$childAttribute] = $this->owner->$parentAttribute;
                    }

                    if (!empty($data['data'])) {
                        foreach ($data['data'] as $attributes) {
                            $data['newModels'][] = new $class(array_merge($params, $attributes));
                        }
                    }
                } else {
                    // many-to-many
                    if (!is_object($activeQuery->via[1])) {
                        throw new Exception('via condition for attribute ' . $attribute . ' cannot must be object');
                    }

                    $via = $activeQuery->via[1];
                    $junctionGetter = 'get' . ucfirst($activeQuery->via[0]);
                    $data['junctionModelClass'] = $junctionModelClass = $via->modelClass;
                    $data['junctionTable'] = $junctionModelClass::tableName();
                    list($data['junctionColumn'])  = array_keys($via->link);
                    list($data['relatedColumn']) = array_values($activeQuery->link);

                    $relatedColumn = $data['relatedColumn'];

                    // make sure what all model's ids from POST exists in database
                    $countManyToManyModels = $class::find()->where([$class::primaryKey()[0] => $data['data']])->count();
                    if ($countManyToManyModels != count($data['data'])) {
                        throw new Exception('Related records for attribute ' . $attribute . ' not found');
                    }

                    $data['oldModels'] = $this->owner->$junctionGetter()->all();

                    // create new junction models
                    foreach ($data['data'] as $relatedModelId) {
                        $junctionModel = new $junctionModelClass();
                        $junctionModel->$relatedColumn = $relatedModelId;
                        $data['newModels'][] = $junctionModel;
                    }
                }

            } elseif (!empty($data['data'])) {
                // one-to-one
                $data['newModels'][] = new $class($data['data']);
            }

            if (empty($activeQuery->via)) {
                $data['oldModels'] = $activeQuery->all();
            }
            unset($data['data']);
        }
    }

    /**
     * Validate relational models, return true only if all models successfully validated.
     *<commit_msg>many-to-many relations support<commit_after><?php

namespace notamedia\relation;

use yii\base\Behavior;
use yii\base\Exception;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use yii\helpers\ArrayHelper;
use Yii;

/**
 * Behavior for support relational data management.
 *
 * - Insert related models from POST array.
 * - Delete related models from database which not exist in POST array
 * - Skip related models which already exist in database with same attributes
 * - Rollback database changes, if relational model save/delete error occurred
 * - Support one-to-one, one-to-many and many-to-many relations
 *
 * This behavior uses getters for relational attribute in owner model, such getters must return `ActiveQuery` object.
 * If you use string values in ON condition in `ActiveQuery` object, then this behavior will throw exception.
 *
 * For support transactions method transactions() in owner model must be defined:
 * ```php
 * <?php
 *
 * public function transactions()
 * {
 *      return [
 *          'default' => static::OP_ALL
 *      ];
 * }
 * ```
 *
 * @property ActiveRecord $owner
 */
class RelationBehavior extends Behavior
{
    /**
     * @var array Relation attributes list.
     */
    public $relationalFields = [];

    /**
     * @var array Relation attributes data.
     */
    protected $relationalData = [];

    /**
     * Process owner-model before save event.
     *
     * @return bool
     *
     * @throws Exception
     */
    public function beforeSave()
    {
        $this->loadData();

        return $this->validateData();
    }

    /**
     * Process owner-model after save event. Save models.
     *
     * @throws Exception
     * @throws \yii\db\Exception
     */
    public function afterSave()
    {
        $this->saveData();
    }

    /**
     * Permission for this behavior to set relational attributes.
     *
     * {@inheritdoc}
     */
    public function canSetProperty($name, $checkVars = true)
    {
        return in_array($name, $this->relationalFields) || parent::canSetProperty($name, $checkVars);
    }

    /**
     * Setter for relational attributes. Called only if attribute is exist in POST array. Method check value format
     * and put it to $this->relationalData array.
     *
     * {@inheritdoc}
     */
    public function __set($name, $value)
    {
        if (in_array($name, $this->relationalFields)) {
            if (!is_array($value) && !empty($value)) {
                $this->owner->addError($name,
                    Yii::$app->getI18n()->format(
                        Yii::t('yii', '{attribute} is invalid.'),
                        ['attribute' => $this->owner->getAttributeLabel($name)],
                        Yii::$app->language
                    )
                );
            } else {
                $this->relationalData[$name] = ['data' => $value];
            }
        } else {
            parent::__set($name, $value);
        }
    }

    /**
     * Load relational data from owner-model getter.
     *
     * - Create related ActiveRecord objects from POST array data.
     * - Load existing related ActiveRecord objects from database.
     * - Check ON condition format.
     * - Get ActiveQuery object from attribute getter method.
     *
     * Fill $this->relationalData array for each relational attribute:
     *
     * ```php
     * $this->relationalData[$attribute] = [
     *      'newModels' => ActiveRecord[],
     *      'oldModels' => ActiveRecord[],
     *      'activeQuery' => ActiveQuery,
     * ];
     * ```
     *
     * @throws Exception
     */
    protected function loadData()
    {
        /** @var ActiveQuery $activeQuery */
        foreach ($this->relationalData as $attribute => &$data) {

            $getter = "get" . ucfirst($attribute);
            $data['activeQuery'] = $activeQuery = $this->owner->$getter();
            $data['newModels'] = [];
            $class = $activeQuery->modelClass;

            if (!ArrayHelper::isAssociative($activeQuery->on) && !empty($activeQuery->on)) {
                Yii::$app->getDb()->getTransaction()->rollBack();
                throw new Exception('ON condition for attribute ' . $attribute . ' must be associative array');
            }

            $params = !ArrayHelper::isAssociative($activeQuery->on) ? [] : $activeQuery->on;

            if ($activeQuery->multiple) {
                if (empty($activeQuery->via)) {
                    // one-to-many
                    foreach ($activeQuery->link as $childAttribute => $parentAttribute) {
                        $params[$childAttribute] = $this->owner->$parentAttribute;
                    }

                    if (!empty($data['data'])) {
                        foreach ($data['data'] as $attributes) {
                            $data['newModels'][] = new $class(array_merge($params, $attributes));
                        }
                    }
                } else {
                    // many-to-many
                    if (!is_object($activeQuery->via[1])) {
                        throw new Exception('via condition for attribute ' . $attribute . ' cannot must be object');
                    }

                    $via = $activeQuery->via[1];
                    $junctionGetter = 'get' . ucfirst($activeQuery->via[0]);
                    $data['junctionModelClass'] = $junctionModelClass = $via->modelClass;
                    $data['junctionTable'] = $junctionModelClass::tableName();
                    list($data['junctionColumn'])  = array_keys($via->link);
                    list($data['relatedColumn']) = array_values($activeQuery->link);

                    $relatedColumn = $data['relatedColumn'];

                    // make sure what all model's ids from POST exists in database
                    $countManyToManyModels = $class::find()->where([$class::primaryKey()[0] => $data['data']])->count();
                    if ($countManyToManyModels != count($data['data'])) {
                        throw new Exception('Related records for attribute ' . $attribute . ' not found');
                    }

                    $data['oldModels'] = $this->owner->$junctionGetter()->all();

                    // create new junction models
                    foreach ($data['data'] as $relatedModelId) {
                        $junctionModel = new $junctionModelClass();
                        $junctionModel->$relatedColumn = $relatedModelId;
                        $data['newModels'][] = $junctionModel;
                    }
                }

            } elseif (!empty($data['data'])) {
                // one-to-one
                $data['newModels'][] = new $class($data['data']);
            }

            if (empty($activeQuery->via)) {
                $data['oldModels'] = $activeQuery->all();
            }
            unset($data['data']);
        }

//        var_dump($this->owner->getErrors());die();
    }

    /**
     * Validate relational models, return true only if all models successfully validated.
     *
{'fraction_non_alphanumeric': 0.13408261149688597, 'fraction_numerical': 0.0008590450282768988, 'mean_word_length': 1.836548223350254, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 115, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4360276', 'n_tokens_mistral': 3672, 'n_tokens_neox': 3501, 'n_words': 887}