Skip to content

Instantly share code, notes, and snippets.

@RangelReale
Created March 3, 2016 17:40
Show Gist options
  • Save RangelReale/f2a47f34cd8aeabad2dd to your computer and use it in GitHub Desktop.
Save RangelReale/f2a47f34cd8aeabad2dd to your computer and use it in GitHub Desktop.
Nested model behavior for Yii2 (pull request 11015)
<?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);
}
}
<?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];
}
}
<?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