/** * @param ActiveRecord $model * @param $relationName */ public function __construct(ActiveRecord $model, $relationName) { $this->owner = $model; $this->relationName = $relationName; $this->relation = $this->owner->getRelation($relationName); /** @var ActiveQuery $via */ $via = is_array($this->relation->via) ? $this->relation->via[1] : $this->relation->via; /** @var \yii\db\ActiveRecord $viaClass */ $this->viaTable = ($viaClass = $via->modelClass) ? $viaClass::tableName() : reset($via->from); $this->relationAttribute = $this->relation->link; foreach ($via->link as $viaAttribute => $ownerAttribute) { $this->condition[$viaAttribute] = $this->owner->{$ownerAttribute}; } }
public static function relation(ActiveRecord $model, $relation_name, $options = []) { /* @var ActiveRecord|YcmModelUtilTrait $model */ $relation = $model->getRelation($relation_name); $config = [$relation_name, 'widget', 'widgetClass' => Select2::className(), 'data' => RelationHelper::getSelectChoices($model, $relation_name), 'hideSearch' => false, 'options' => ['multiple' => $relation->multiple, 'placeholder' => 'Select...'], 'pluginOptions' => ['allowClear' => true]]; return ArrayHelper::merge($config, $options); }
/** * @param ActiveRecord $model * @param $relation_name * @return null|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface */ public static function getRelation(ActiveRecord $model, $relation_name) { $relation = null; foreach (explode('.', $relation_name) as $relation_subname) { $relation = $model->getRelation($relation_subname); $model = new $relation->modelClass(); } return $relation; }
/** * Get available relation choices * @param $relation_name * @return mixed */ public static function getSelectChoices(ActiveRecord $model, $relation_name) { $class = $model->className(); if (!isset(self::$relationsChoices[$class][$relation_name])) { self::$relationsChoices[$class][$relation_name] = []; $relation = $model->getRelation($relation_name, false); if ($relation) { self::$relationsChoices[$class][$relation_name] = ModelHelper::getSelectChoices(new $relation->modelClass()); } } return self::$relationsChoices[$class][$relation_name]; }
public function afterSave() { $data = []; if (isset($_POST[$this->owner->formName()])) { $data = $_POST[$this->owner->formName()]; } if ($data) { foreach ($data as $attribute => $value) { if (!in_array($attribute, $this->relations)) { continue; } if (is_array($value)) { $relation = $this->owner->getRelation($attribute); if ($relation->via !== null) { /** @var ActiveRecord $foreignModel */ $foreignModel = $relation->modelClass; $this->owner->unlinkAll($attribute, true); if (is_array(current($value))) { foreach ($value as $data) { if (isset($data[$foreignModel::primaryKey()[0]]) && $data[$foreignModel::primaryKey()[0]] > 0) { $fm = $foreignModel::findOne($data[$foreignModel::primaryKey()[0]]); $fm->load($data, ''); $this->owner->link($attribute, $fm); } } } else { foreach ($value as $fk) { if (preg_match('~^\\d+$~', $fk)) { $this->owner->link($attribute, $foreignModel::findOne($fk)); } } } } } else { $this->owner->unlinkAll($attribute, true); } } } }
/** * Loads a specific translation model * * @param string $language the language to return * * @return null|\yii\db\ActiveQuery|static */ private function getNewTranslation($language) { $translation = null; /** @var \yii\db\ActiveQuery $relation */ $relation = $this->owner->getRelation($this->relation); /** @var ActiveRecord $class */ $class = $relation->modelClass; //if ($translation === null) { $translation = new $class(); $translation->{key($relation->link)} = $this->owner->getPrimaryKey(); $translation->{$this->languageAttribute} = $language; //} return $translation; }
protected static function editableRelationConfig(ActiveRecord $model, $relation_name) { /* @var ActiveRecord|YcmModelUtilTrait $model */ $relation = $model->getRelation($relation_name); /* @var Module $module */ $module = \Yii::$app->getModule('ycm-utils'); /* @var ActiveRecord|YcmModelUtilTrait $relationModel */ $relationModel = \Yii::createObject($relation->modelClass); $modelChoices = ModelHelper::getSelectChoices($relationModel); /** * @todo #1 implement fill ajax loading with ajax mapping * @fixme #2 Relation is multiple, after live edit fix JS error `Cannot read property '[object Array]' of null` */ return ['attribute' => $relation->multiple ? $relation_name . $model->method_postfix_relation_ids : reset($relation->link), 'label' => ucfirst($relation_name), 'filter' => $modelChoices, 'value' => $relation->multiple ? function ($m) use($relation_name, $modelChoices, $model) { return implode(', ', array_map(function ($relation_id) use($modelChoices) { return $modelChoices[$relation_id]; }, array_values((array) $m->{$relation_name . $model->method_postfix_relation_ids}))); } : null, 'editableOptions' => ['inputType' => Editable::INPUT_SELECT2, 'size' => 'lg', 'options' => ['options' => ['multiple' => $relation->multiple], 'data' => $modelChoices, 'pluginOptions' => count($modelChoices) > $model->ajax_enable_threshold ? ['minimumInputLength' => 3, 'ajax' => ['url' => Url::to(['/ycm/model/choices', 'name' => $module->ycm->getModelName($relationModel)]), 'dataType' => 'json', 'processResults' => new JsExpression('function (results) { return results; }')]] : null], 'displayValueConfig' => !$relation->multiple ? $modelChoices : null]]; }
/** * Renders grid column for list value of via table data * @param ActiveRecord|bool $model * @param $attribute * @param array $options * @return array * @throws \Exception */ public static function viaListFormat($model, $attribute, $options = []) { $relation = $model->getRelation($attribute); $relationClass = $relation->modelClass; $columns = Yii::$app->db->getTableSchema($relationClass::tableName())->columnNames; $titles = array_intersect(['title', 'name', 'username'], $columns); if (!($title = reset($titles))) { throw new \Exception(Yii::t('app', 'Relation does not have any title column')); } return array_merge(['attribute' => $attribute, 'format' => 'raw', 'value' => !$model->isNewRecord ? implode(', ', $relation->select($title)->column()) : function ($data) use($attribute, $title) { return implode(', ', $data->getRelation($attribute)->select($title)->column()); }, 'filter' => false], $options); }
/** * @param array $formFields * @param \yii\db\ActiveRecord $model * @param string $relation * @param array $hiddenAttributes * @param array $safeAttributes * @param bool $multiple true for multiple values inputs, usually used for search forms * @return array * @throws InvalidConfigException */ protected static function addRelationField($formFields, $model, $relation, $hiddenAttributes, $safeAttributes, $multiple = false) { $activeRelation = $model->getRelation(Html::getAttributeName($relation)); if (!$activeRelation->multiple) { // validate foreign keys only for hasOne relations $isHidden = false; foreach ($activeRelation->link as $left => $right) { if (!in_array($right, $safeAttributes)) { return $formFields; } if (isset($hiddenAttributes[$right])) { $formFields[$relation] = Html::activeHiddenInput($model, $right); unset($hiddenAttributes[$right]); $isHidden = true; } } if ($isHidden) { return $formFields; } } if (!Yii::$app->user->can($activeRelation->modelClass . '.read')) { return $formFields; } if (count($activeRelation->link) > 1) { throw new InvalidConfigException('Composite key relations are not supported by ' . get_called_class()); } if ($activeRelation->multiple) { if (($field = static::getHasManyRelationField($model, $relation, $activeRelation)) !== null) { $formFields[$relation] = $field; } } else { if (($field = static::getHasOneRelationField($model, $relation, $activeRelation, $multiple)) !== null) { $formFields[$relation] = $field; } } return $formFields; }
/** * @param ActiveRecord $model * @param string $relationName * @return string * @throws \Exception */ protected static function getNameAttribute($model, $relationName) { /** @var ActiveRecord $class */ $class = $model->getRelation($relationName)->modelClass; $attributes = Yii::$app->db->getTableSchema($class::tableName())->columnNames; if (!($existingNames = array_intersect(['name', 'title', 'username'], $attributes))) { throw new \Exception("Relation does not have name attribute: " . $class); } return reset($existingNames); }
/** * @return \yii\db\ActiveQuery|\yii\db\ActiveQueryInterface */ protected function getUploadRelation() { return $this->owner->getRelation($this->uploadRelation); }
/** * Loads a specific relation with $data. * incremental: existing subitems are neither removed nor unlinked. * non-incremental: existing (loaded) subitems are unlinked and/or deleted. * * @param ActiveRecord $model model * @param string $relationName the relation's name * @param array $data data to load, including relational data * @param array $config configuration array * @internal */ private function setRelation(&$model, $relationName, &$data, &$config) { if (!$model->hasProperty($relationName)) { throw new \yii\base\UnknownPropertyException(sprintf('model {%s} has no relation {%s}', $model->className(), $relationName)); } $relation =& $config[self::RELATIONS][$relationName]; $formName = ArrayHelper::getValue($relation, self::FORMNAME, false); $scenario = ArrayHelper::getValue($relation, self::SCENARIO, $this->defaultScenario); $incremental = ArrayHelper::getValue($relation, self::INCREMENTAL, $this->defaultIncremental); $delete = ArrayHelper::getValue($relation, self::DELETE, $this->defaultDelete); $relationData = $formName == false ? $data : $data[$formName]; $rel = $model->getRelation($relationName); $pattern = new $rel->modelClass(); $recursive = !$pattern->hasMethod('isActiveDocument'); $models = null; $relation[self::REMOVE] = []; $relation[self::LINK] = []; // relation is a collection or a single component if ($rel->multiple) { $models = []; if ($incremental) { // loop through array data and load sub models foreach ($relationData as $key => $value) { $m = $this->loadModel($rel->modelClass, $scenario, $value, $relation, $recursive); $models[] = $m; } } else { $sort = ArrayHelper::getValue($relation, self::SORTABLE, null); if ($sort !== null) { $index = 0; foreach ($relationData as $key => &$value) { $relationData[$key][$sort] = $index++; } } // loop through relation data, load data and detect removable sub models foreach ($model->{$relationName} as $item) { $keys = $item->getPrimaryKey(true); // try to find subitem in data reset($relationData); $found = false; foreach ($relationData as $key => &$value) { // normalize if (!empty($formName)) { $value = $value[$formName]; } $modelKeys = array_intersect_key($value, $keys); if (count(array_diff_assoc($modelKeys, $keys)) == 0) { $m = $this->loadExistingModel($item, $scenario, $value, $relation, $recursive); $models[] = $m; $found = true; // processed, so remove from data array unset($relationData[$key]); break; } } // we have an existing item, but it was not loaded by $data, so mark for remove. if (!$found) { $relation[self::REMOVE][] = $item; } } // everything left in $relationData is new model data // model might be existing, but not linked foreach ($relationData as $key => $value) { // normalize if (!empty($formName)) { $value = $value[$formName]; } $m = $this->loadModel($rel->modelClass, $scenario, $value, $relation, $recursive); $models[] = $m; $relation[self::LINK][$this->serializeKey($model)][] = $m; } } } else { // relation is a single component $oldItem = $model->{$relationName}; $models = $this->loadModel($rel->modelClass, $scenario, $value, $relation, $recursive); if (!$incremental) { if ($oldItem !== null) { $keys = $oldItem->getPrimaryKey(true); if ($models !== null) { $modelKeys = $models->getPrimaryKey(true); if (count(array_diff_assoc($keys, $modelKeys)) !== 0) { $relation[self::REMOVE][] = $oldItem; } } else { $relation[self::REMOVE][] = $models; } } } } if ($models !== null) { $model->populateRelation($relationName, $models); } }
/** * @inheritdoc */ public function getRelation($name, $throwException = true) { if (isset($this->relations[$name])) { return $this->relations[$name](); } return parent::getRelation($name, $throwException); }
/** * @param ActiveRecord $model * @param array $with * @return ActiveRelationInterface[] */ private function normalizeRelations($model, $with) { $relations = []; foreach ($with as $name => $callback) { if (is_integer($name)) { $name = $callback; $callback = null; } if (($pos = strpos($name, '.')) !== false) { // with sub-relations $childName = substr($name, $pos + 1); $name = substr($name, 0, $pos); } else { $childName = null; } if (!isset($relations[$name])) { $relation = $model->getRelation($name); $relation->primaryModel = null; $relations[$name] = $relation; } else { $relation = $relations[$name]; } if (isset($childName)) { $relation->with[$childName] = $callback; } elseif ($callback !== null) { call_user_func($callback, $relation); } } return $relations; }
/** * Loads posted data into model relation models * @param ActiveRecord $model * @param $relationName * @param $data * @param array $requiredData * @return array */ public static function loadRelation(ActiveRecord $model, $relationName, $data, $requiredData = []) { $relationClass = $model->getRelation($relationName)->modelClass; return static::loadMultiple(@$data[(new $relationClass())->formName()], $relationClass, $model->{$relationName}, $requiredData); }
/** * For each related model, try to save it first. * If set in the owner model, operation is done in a transactional way so if one of the models should not validate * or be saved, a rollback will occur. * This is done during the before validation process to be able to set the related foreign keys. * @param ActiveRecord $model * @param ModelEvent $event * @return bool */ public function _saveRelatedRecords(ActiveRecord $model, ModelEvent $event) { if ($model->isNewRecord && $model->isTransactional($model::OP_INSERT) || !$model->isNewRecord && $model->isTransactional($model::OP_UPDATE) || $model->isTransactional($model::OP_ALL)) { $this->_transaction = $model->getDb()->beginTransaction(); } try { foreach ($this->relations as $relationName) { if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing... $relation = $model->getRelation($relationName); if (!empty($model->{$relationName})) { if ($relation->multiple === false) { // Save Has one relation new record $pettyRelationName = Inflector::camel2words($relationName, true); $this->_saveModelRecord($model->{$relationName}, $event, $pettyRelationName, $relationName); } else { // Save Has many relations new records /** @var ActiveRecord $relationModel */ foreach ($model->{$relationName} as $i => $relationModel) { $pettyRelationName = Inflector::camel2words($relationName, true) . " #{$i}"; $this->_validateRelationModel($pettyRelationName, $relationName, $relationModel, $event); } } } } } if (!$event->isValid) { throw new Exception("One of the related model could not be validated"); } } catch (Exception $e) { if ($this->_transaction instanceof Transaction && $this->_transaction->isActive) { $this->_transaction->rollBack(); // If anything goes wrong, transaction will be rolled back } $event->isValid = false; // Stop saving, something went wrong return false; } return true; }
/** * @inheritdoc */ public function init() { parent::init(); $this->relation = $this->model->getRelation($this->attribute); }
/** * Returns queries that contain necessary joins and condition * to select only those records which are related directly or indirectly * with the current user. * @param ActiveRecord $model must have the AuthorizerBehavior attached * @param array $relations list of model relations to check, supports dot notation for indirect relations * @param IdentityInterface $user if null, Yii::$app->user->identity will be used * @param array $baseConditions * @param array $baseParams * @return ActiveQuery[] */ public function getCompositeRelatedUserQuery($model, array $relations, $user, $baseConditions = [], $baseParams = []) { $schema = $model->getDb()->getSchema(); $userPk = array_map([$schema, 'quoteSimpleColumnName'], $user::primaryKey()); $result = []; if (count($userPk) > 1) { throw new InvalidCallException('Composite primary key in User model is not supported.'); } else { $userPk = reset($userPk); } $mainQuery = $model->find(); if (empty($mainQuery->from)) { $mainQuery->from = [$model->tableName() . ' t']; } $mainQuery->distinct = true; foreach ($relations as $relationName) { if (($pos = strpos($relationName, '.')) === false) { $relation = $model->getRelation($relationName); if (!$relation->multiple) { $query = $mainQuery; } else { $query = $model->find(); if (empty($query->from)) { $query->from = [$model->tableName() . ' t']; } } $query->innerJoinWith([$relationName => function ($query) use($relation, $relationName) { /** @var ActiveRecord $modelClass */ $modelClass = $relation->modelClass; return $query->from([$modelClass::tableName() . ' ' . $relationName]); }]); $column = $schema->quoteSimpleTableName($relationName) . '.' . $userPk; $query->orWhere($column . ' IS NOT NULL AND ' . $column . ' = :current_user_id'); $query->addParams([':current_user_id' => $user->getId()]); if ($relation->multiple) { $query->andWhere($baseConditions, $baseParams); $result[] = $query; } } else { $userRelationName = substr($relationName, $pos + 1); $relationName = substr($relationName, 0, $pos); $relation = $model->getRelation($relationName); /** @var ActiveRecord $relationModel */ $relationModel = new $relation->modelClass(); $userRelation = $relationModel->getRelation($userRelationName); $userQuery = $relationModel->find(); if (empty($userQuery->from)) { $userQuery->from = [$relationModel->tableName() . ' t']; } $userQuery->distinct(); $userQuery->select($this->quoteColumn('t', $relationModel::primaryKey(), $schema)); //$userQuery->innerJoinWith($userRelationName); $userQuery->innerJoinWith([$userRelationName => function ($query) use($userRelation, $userRelationName) { /** @var ActiveRecord $modelClass */ $modelClass = $userRelation->modelClass; return $query->from([$modelClass::tableName() . ' ' . $userRelationName]); }]); $userQuery->andWhere($schema->quoteSimpleTableName($userRelationName) . '.' . $userPk . ' = :current_user_id'); $command = $userQuery->createCommand($model->getDb()); $query = $model->find(); if (empty($query->from)) { $query->from = [$model->tableName() . ' t']; } $query->distinct(); //$query->innerJoinWith($relationName); $query->innerJoinWith([$relationName => function ($query) use($relation, $relationName) { /** @var ActiveRecord $modelClass */ $modelClass = $relation->modelClass; return $query->from([$modelClass::tableName() . ' ' . $relationName]); }]); $fk = $this->quoteColumn($relationName, $relationModel::primaryKey(), $schema); $query->orWhere('COALESCE(' . (is_array($relationModel::primaryKey()) ? 'ROW(' . $fk . ')' : $fk) . ' IN (' . $command->getSql() . '), false)'); $query->addParams([':current_user_id' => $user->getId()]); $query->andWhere($baseConditions, $baseParams); $result[] = $query; } } $mainQuery->andWhere($baseConditions, $baseParams); $result[] = $mainQuery; return $result; }