Created
March 3, 2016 17:40
-
-
Save RangelReale/f2a47f34cd8aeabad2dd to your computer and use it in GitHub Desktop.
Nested model behavior for Yii2 (pull request 11015)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\components; | |
use yii\base\Object; | |
use yii\base\InvalidConfigException; | |
class NestedModelAttribute extends Object | |
{ | |
/** | |
* @var NestedModelBehavior | |
*/ | |
public $behavior; | |
/** | |
* @var string | |
*/ | |
public $originalRelation; | |
/** | |
* @var string | |
*/ | |
public $targetRelation; | |
/** | |
* @var boolean | |
*/ | |
public $isArray = false; | |
/** | |
* @var boolean | |
*/ | |
public $clearOnSet = false; | |
/** | |
* @var array|false|Closure($behavior, $targetRelation, $index) | |
*/ | |
public $newItem = false; | |
/** | |
* @var Closure($behavior, $targetRelation) | |
*/ | |
public $clearBeforeSave; | |
/** | |
* @var Closure($behavior, $targetRelation, $index, $model) | |
*/ | |
public $processSaveModel; | |
protected $_value; | |
public function validate() | |
{ | |
if (!isset($this->_value)) | |
return true; | |
$isValid = true; | |
if ($this->isArray) { | |
foreach ($this->_value as $arrayIndex => $arrayItem) { | |
if (!$arrayItem->validate()) { | |
foreach ($arrayItem->getErrors() as $eattr => $evalue) { | |
foreach ($evalue as $eerror) { | |
$this->behavior->owner->addError($this->targetRelation.'['.$arrayIndex.']['.$eattr.']', $eerror); | |
} | |
} | |
$isValid = false; | |
} | |
} | |
} else { | |
if (!$this->_value->validate()) { | |
foreach ($this->_value->getErrors() as $eattr => $evalue) { | |
foreach ($evalue as $eerror) { | |
$this->behavior->owner->addError($this->targetRelation.'['.$eattr.']', $eerror); | |
} | |
} | |
} | |
$isValid = false; | |
} | |
return $isValid; | |
} | |
public function save() | |
{ | |
if ($this->clearOnSet) { | |
if ($this->clearBeforeSave instanceof \Closure) { | |
call_user_func($this->clearBeforeSave, $this->behavior, $this->targetRelation); | |
} else { | |
throw new InvalidConfigException('Must set clearBeforeSave when clearOnSet'); | |
} | |
} | |
if (!isset($this->_value)) | |
return true; | |
$isValid = true; | |
if ($this->isArray) { | |
foreach ($this->_value as $arrayIndex => $arrayItem) { | |
if ($this->processSaveModel instanceof \Closure) { | |
call_user_func($this->processSaveModel, $this->behavior, $this->targetRelation, $arrayIndex, $arrayItem); | |
} | |
if (!$arrayItem->save()) { | |
$isValid = false; | |
} | |
} | |
} else { | |
if ($this->processSaveModel instanceof \Closure) { | |
call_user_func($this->processSaveModel, $this->behavior, $this->targetRelation, null, $this->behavior->owner->{$this->originalRelation}); | |
} | |
if (!$this->_value->save()) | |
$isValid = false; | |
} | |
return $isValid; | |
} | |
public function getValue() | |
{ | |
if (isset($this->_value)) { | |
return $this->_value; | |
} | |
return $this->behavior->owner->{$this->originalRelation}; | |
} | |
public function setValue($value) | |
{ | |
if (is_null($value) || !is_array($value)) | |
return; | |
$currentValue = isset($this->_value)?$this->_value:$this->behavior->owner->{$this->originalRelation}; | |
if ($this->isArray) { | |
if (is_null($currentValue) || $this->clearOnSet) { | |
$currentValue = []; | |
} | |
foreach ($value as $vindex => $vvalue) { | |
if (!isset($currentValue[$vindex])) { | |
$newItem = $this->createNewItem($vindex); | |
if (is_null($newItem)) { | |
continue; | |
} | |
$currentValue[$vindex] = $newItem; | |
} | |
$currentValue[$vindex]->setAttributes($vvalue); | |
} | |
} else { | |
if (!isset($currentValue)) { | |
$newItem = $this->createNewItem(); | |
if (is_null($newItem)) { | |
return; | |
} | |
$currentValue = $newItem; | |
} | |
$currentValue->setAttributes($vvalue); | |
} | |
$this->_value = $currentValue; | |
} | |
public function reset() | |
{ | |
$this->_value = null; | |
} | |
protected function createNewItem($index = null) | |
{ | |
if ($this->newItem === false) { | |
if (YII_DEBUG) { | |
\Yii::trace("Nested model {$this->targetRelation} cannot create new item in '" . get_class($this) . "'.", __METHOD__); | |
} | |
return null; | |
} | |
if (is_null($this->newItem)) { | |
throw new InvalidConfigException('New item class not configured'); | |
} | |
if ($this->newItem instanceof \Closure) { | |
return call_user_func($this->newItem, $this->behavior, $this->targetRelation, $index); | |
} | |
return \Yii::createObject($this->newItem); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\components; | |
use yii\base\Behavior; | |
use yii\helpers\ArrayHelper; | |
use yii\db\BaseActiveRecord; | |
use yii\base\InvalidValueException; | |
class NestedModelBehavior extends Behavior | |
{ | |
/** | |
* @var string | |
*/ | |
public $namingTemplate = '{relation}_nested'; | |
/** | |
* @var array List of the model relations in one of the following formats: | |
* ```php | |
* [ | |
* 'first', // This will use default configuration and virtual relation template | |
* 'second' => 'target_second', // This will use default configuration with custom relation template | |
* 'third' => [ | |
* 'relation' => 'thrid_rel', // Optional | |
* 'targetAttribute' => 'target_third', // Optional | |
* // Rest of configuration | |
* ] | |
* ] | |
* ``` | |
*/ | |
public $nestedModels = []; | |
/** | |
* @var array | |
*/ | |
public $nestedModelConfig = ['class' => 'app\components\NestedModelAttribute']; | |
/** | |
* @var $NestedModelAttribute[] | |
*/ | |
public $nestedModelValues = []; | |
public function init() | |
{ | |
$this->prepareNestedModels(); | |
} | |
protected function prepareNestedModels() | |
{ | |
foreach ($this->nestedModels as $key => $value) | |
{ | |
$config = $this->nestedModelConfig; | |
if (is_integer($key)) { | |
$originalRelation = $value; | |
$targetRelation = $this->processTemplate($originalRelation); | |
} else { | |
$originalRelation = $key; | |
if (is_string($value)) { | |
$targetRelation = $value; | |
} else { | |
$targetRelation = ArrayHelper::remove($value, 'targetRelation', $this->processTemplate($originalRelation)); | |
$originalRelation = ArrayHelper::remove($value, 'relation', $originalRelation); | |
$config = array_merge($config, $value); | |
} | |
} | |
$config['behavior'] = $this; | |
$config['originalRelation'] = $originalRelation; | |
$config['targetRelation'] = $targetRelation; | |
$this->nestedModelValues[$targetRelation] = $config; | |
} | |
} | |
protected function processTemplate($originalRelation) | |
{ | |
return strtr($this->namingTemplate, [ | |
'{relation}' => $originalRelation, | |
]); | |
} | |
public function events() | |
{ | |
$events = []; | |
$events[BaseActiveRecord::EVENT_BEFORE_VALIDATE] = 'onBeforeValidate'; | |
$events[BaseActiveRecord::EVENT_AFTER_FIND] = 'onAfterFind'; | |
$events[BaseActiveRecord::EVENT_AFTER_INSERT] = 'onAfterSave'; | |
$events[BaseActiveRecord::EVENT_AFTER_UPDATE] = 'onAfterSave'; | |
$events[BaseActiveRecord::EVENT_UNSAFE_ATTRIBUTE] = 'onUnsafeAttribute'; | |
return $events; | |
} | |
/** | |
* Performs validation for all the relations | |
* @param Event $event | |
*/ | |
public function onBeforeValidate($event) | |
{ | |
foreach (array_keys($this->nestedModelValues) as $targetRelation) { | |
$value = $this->getNestedModel($targetRelation); | |
if ($value instanceof NestedModelAttribute && $this->owner->isAttributeSafe($targetRelation)) { | |
if (!$value->validate()) | |
$event->isValid = false; | |
} | |
} | |
} | |
/** | |
* Reset when record changes | |
* @param Event $event | |
*/ | |
public function onAfterFind($event) | |
{ | |
foreach (array_keys($this->nestedModelValues) as $targetRelation) { | |
$value = $this->getNestedModel($targetRelation); | |
if ($value instanceof NestedModelAttribute) { | |
$value->reset(); | |
} | |
} | |
} | |
/** | |
* Save relation if safe | |
* @param Event $event | |
*/ | |
public function onAfterSave($event) | |
{ | |
foreach (array_keys($this->nestedModelValues) as $targetRelation) { | |
$value = $this->getNestedModel($targetRelation); | |
if ($value instanceof NestedModelAttribute && $this->owner->isAttributeSafe($targetRelation)) { | |
$value->save(); | |
} | |
} | |
} | |
/** | |
* @param ModelUnsafeAttributeEvent $event | |
*/ | |
public function onUnsafeAttribute($event) | |
{ | |
if ($this->canSetProperty($event->attributeName)) { | |
if ($this->owner->isAttributeSafe($this->getNestedModel($event->attributeName)->targetRelation)) { | |
$this->__set($event->attributeName, $event->attributeValue); | |
$event->isSafe = true; | |
$event->handled = true; | |
} | |
} | |
} | |
public function canGetProperty($name, $checkVars = true) | |
{ | |
if ($this->hasNestedModel($name)) { | |
return true; | |
} | |
return parent::canGetProperty($name, $checkVars); | |
} | |
public function hasNestedModel($name) | |
{ | |
return isset($this->nestedModelValues[$name]); | |
} | |
public function canSetProperty($name, $checkVars = true) | |
{ | |
if ($this->hasNestedModel($name)) { | |
return true; | |
} | |
return parent::canSetProperty($name, $checkVars); | |
} | |
public function __get($name) | |
{ | |
if ($this->hasNestedModel($name)) { | |
return $this->getNestedModel($name)->getValue(); | |
} | |
return parent::__get($name); | |
} | |
public function __set($name, $value) | |
{ | |
if ($this->hasNestedModel($name)) { | |
$this->getNestedModel($name)->setValue($value); | |
return; | |
} | |
parent::__set($name, $value); | |
} | |
public function getNestedModel($name) | |
{ | |
if (is_array($this->nestedModelValues[$name])) { | |
$this->nestedModelValues[$name] = \Yii::createObject($this->nestedModelValues[$name]); | |
} | |
return $this->nestedModelValues[$name]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\components; | |
use yii\validators\Validator; | |
class NestedModelValidator extends Validator | |
{ | |
public function init() | |
{ | |
parent::init(); | |
$this->skipOnEmpty = false; | |
} | |
public function validateAttribute($model, $attribute) | |
{ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment