├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── ContainerInterface.php ├── ContainerTrait.php ├── Mapping.php ├── Validator.php ├── elasticsearch └── ActiveRecord.php └── mongodb ├── ActiveRecord.php └── ActiveRecordFile.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii 2 Embedded (Nested) Models extension Change Log 2 | =================================================== 3 | 4 | 1.0.3, August 23, 2018 5 | ---------------------- 6 | 7 | - Enh #19: Usage of deprecated `yii\base\InvalidParamException` changed to `yii\base\InvalidArgumentException` ones (klimov-paul) 8 | - Enh #20: Added support for `Traversable` instances as the embedded source (klimov-paul) 9 | 10 | 11 | 1.0.2, November 3, 2017 12 | ----------------------- 13 | 14 | - Bug #16: Fixed `ContainerTrait::__isset()` returns incorrect result for embedded model properties (rodion-k) 15 | - Bug: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul) 16 | 17 | 18 | 1.0.1, October 17, 2016 19 | ----------------------- 20 | 21 | - Enh #8: Added `Validator::$initializedOnly` option allowing skip validation for not initialized embedded models (klimov-paul) 22 | 23 | 24 | 1.0.0, December 26, 2015 25 | ------------------------ 26 | 27 | - Initial release. 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Yii framework is free software. It is released under the terms of 2 | the following BSD License. 3 | 4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii2tech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Embedded (Nested) Models Extension for Yii 2

6 |
7 |

8 | 9 | This extension provides support for embedded (nested) models usage in Yii2. 10 | In particular it allows working with sub-documents in [MongoDB](https://github.com/yiisoft/yii2-mongodb) and [ElasticSearch](https://github.com/yiisoft/yii2-elasticsearch) 11 | as well as processing complex JSON attributes at relational databases. 12 | 13 | For license information check the [LICENSE](LICENSE.md)-file. 14 | 15 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/embedded/v/stable.png)](https://packagist.org/packages/yii2tech/embedded) 16 | [![Total Downloads](https://poser.pugx.org/yii2tech/embedded/downloads.png)](https://packagist.org/packages/yii2tech/embedded) 17 | [![Build Status](https://travis-ci.org/yii2tech/embedded.svg?branch=master)](https://travis-ci.org/yii2tech/embedded) 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 24 | 25 | Either run 26 | 27 | ``` 28 | php composer.phar require --prefer-dist yii2tech/embedded 29 | ``` 30 | 31 | or add 32 | 33 | ```json 34 | "yii2tech/embedded": "*" 35 | ``` 36 | 37 | to the require section of your composer.json. 38 | 39 | 40 | Usage 41 | ----- 42 | 43 | This extension grants the ability to work with complex model attributes, represented as arrays, as nested models, 44 | represented as objects. 45 | To use this feature the target class should implement [[\yii2tech\embedded\ContainerInterface]] interface. 46 | This can be easily achieved using [[\yii2tech\embedded\ContainerTrait]]. 47 | 48 | For each embedded entity a mapping declaration should be provided. 49 | In order to do so you need to declare method, which name is prefixed with 'embedded', which 50 | should return the [[Mapping]] instance. You may use [[hasEmbedded()]] and [[hasEmbeddedList()]] for this. 51 | 52 | Per each of source field or property a new virtual property will be declared, which name will be composed 53 | by removing 'embedded' prefix from the declaration method name. 54 | 55 | > Note: watch for the naming collisions: if you have a source property named 'profile' the mapping declaration 56 | for it should have different name, like 'profileModel'. 57 | 58 | 59 | Example: 60 | 61 | ```php 62 | use yii\base\Model; 63 | use yii2tech\embedded\ContainerInterface; 64 | use yii2tech\embedded\ContainerTrait; 65 | 66 | class User extends Model implements ContainerInterface 67 | { 68 | use ContainerTrait; 69 | 70 | public $profileData = []; 71 | public $commentsData = []; 72 | 73 | public function embedProfile() 74 | { 75 | return $this->mapEmbedded('profileData', Profile::className()); 76 | } 77 | 78 | public function embedComments() 79 | { 80 | return $this->mapEmbeddedList('commentsData', Comment::className()); 81 | } 82 | } 83 | 84 | $user = new User(); 85 | $user->profile->firstName = 'John'; 86 | $user->profile->lastName = 'Doe'; 87 | 88 | $comment = new Comment(); 89 | $user->comments[] = $comment; 90 | ``` 91 | 92 | Each embedded mapping may have additional options specified. Please refer to [[\yii2tech\embedded\Mapping]] for more details. 93 | 94 | 95 | ## Processing embedded objects 96 | 97 | Embedded feature is similar to regular ActiveRecord relation feature. Their declaration and processing are similar 98 | and have similar specifics and limitations. 99 | All embedded objects are lazy loaded. This means they will not be created until first demand. This saves memory 100 | but may produce unexpected results at some point. 101 | By default, once embedded object is instantiated its source attribute will be unset in order to save memory usage. 102 | You can control this behavior via [[\yii2tech\embedded\Mapping::$unsetSource]]. 103 | 104 | Embedded objects allow simplification of nested data processing, but usually they know nothing about their source 105 | data meaning and global processing. For example: nested object is not aware if its source data comes from database 106 | and it does not know how this data should be saved. Such functionality usually is handled by container object. 107 | Thus at some point you will need to convert data from embedded objects back to its raw format, which allows its 108 | native processing like saving. This can be done using method `refreshFromEmbedded()`: 109 | 110 | ```php 111 | use yii\base\Model; 112 | use yii2tech\embedded\ContainerInterface; 113 | use yii2tech\embedded\ContainerTrait; 114 | 115 | class User extends Model implements ContainerInterface 116 | { 117 | use ContainerTrait; 118 | 119 | public $profileData = [ 120 | 'firstName' => 'Unknown', 121 | 'lastName' => 'Unknown', 122 | ]; 123 | 124 | public function embedProfile() 125 | { 126 | return $this->mapEmbedded('profileData', Profile::className()); 127 | } 128 | } 129 | 130 | $user = new User(); 131 | var_dump($user->profileData); // outputs array: ['firstName' => 'Unknown', 'lastName' => 'Unknown'] 132 | 133 | $user->profile->firstName = 'John'; 134 | $user->profile->lastName = 'Doe'; 135 | 136 | var_dump($user->profileData); // outputs empty array 137 | 138 | $user->refreshFromEmbedded(); 139 | var_dump($user->profileData); // outputs array: ['firstName' => 'John', 'lastName' => 'Doe'] 140 | ``` 141 | 142 | While embedding list of objects (using [[\yii2tech\embedded\ContainerTrait::mapEmbeddedList()]]) the produced 143 | virtual field will be not an array, but an object, which satisfies [[\ArrayAccess]] interface. Thus all manipulations 144 | with such property (even if it may look like using array) will affect container object. 145 | For example: 146 | 147 | ```php 148 | use yii\base\Model; 149 | use yii2tech\embedded\ContainerInterface; 150 | use yii2tech\embedded\ContainerTrait; 151 | 152 | class User extends Model implements ContainerInterface 153 | { 154 | use ContainerTrait; 155 | 156 | public $commentsData = []; 157 | 158 | public function embedComments() 159 | { 160 | return $this->mapEmbeddedList('commentsData', Comment::className()); 161 | } 162 | } 163 | 164 | $user = new User(); 165 | // ... 166 | 167 | $comments = $user->comments; // not a copy of array - copy of object reference! 168 | foreach ($comments as $key => $comment) { 169 | if (...) { 170 | unset($comments[$key]); // unsets `$user->comments[$key]`! 171 | } 172 | } 173 | 174 | $comments = clone $user->comments; // creates a copy of list, but not a copy of contained objects! 175 | $comments[0]->title = 'new value'; // actually sets `$user->comments[0]->title`! 176 | ``` 177 | 178 | 179 | ## Validating embedded models 180 | 181 | Each embedded model should declare its own validation rules and, in general, should be validated separately. 182 | However, you may simplify complex model validation using [[\yii2tech\embedded\Validator]]. 183 | For example: 184 | 185 | ```php 186 | use yii\base\Model; 187 | use yii2tech\embedded\ContainerInterface; 188 | use yii2tech\embedded\ContainerTrait; 189 | 190 | class User extends Model implements ContainerInterface 191 | { 192 | use ContainerTrait; 193 | 194 | public $contactData; 195 | 196 | public function embedContact() 197 | { 198 | return $this->mapEmbedded('contactData', Contact::className()); 199 | } 200 | 201 | public function rules() 202 | { 203 | return [ 204 | ['contact', 'yii2tech\embedded\Validator'], 205 | // ... 206 | ] 207 | } 208 | } 209 | 210 | class Contact extends Model 211 | { 212 | public $email; 213 | 214 | public function rules() 215 | { 216 | return [ 217 | ['email', 'required'], 218 | ['email', 'email'], 219 | ] 220 | } 221 | } 222 | 223 | $user = new User(); 224 | if ($user->load(Yii::$app->request->post()) && $user->contact->load(Yii::$app->request->post())) { 225 | if ($user->validate()) { // automatically validates 'contact' as well 226 | // ... 227 | } 228 | } 229 | ``` 230 | 231 | > Note: pay attention that [[\yii2tech\embedded\Validator]] must be set for the embedded model name - not for its 232 | source attribute. Do not mix them up! 233 | 234 | You can enable [[\yii2tech\embedded\Validator::$initializedOnly]], allowing to skip validation for the embedded model, if 235 | it has not been initialized, e.g. requested at least once. This will save the performance in case source model can be used 236 | in different scenarios, some of which may not require embedded model manipulations. However, in this case embedded source 237 | attribute value will not be validated. You should ensure it validated in other way or it is 'unsafe' for population via 238 | [[\yii\base\Model::load()]] method. 239 | 240 | 241 | ## Saving embedded models 242 | 243 | Keep in mind that embedded models are stored separately from the source model attributes. You will need to use 244 | [[\yii2tech\embedded\ContainerInterface::refreshFromEmbedded()]] method in order to populate source model 245 | attributes with the data from embedded models. 246 | 247 | Also note, that attempt to get 'dirty' value for embedded source attribute will also fail until you use `refreshFromEmbedded()` 248 | even, if embedded model has changed: 249 | 250 | ```php 251 | $user = User::findOne(1); // declares embedded model 'contactModel' from attribute 'contactData' 252 | 253 | if ($user->contactModel->load(Yii::$app->request->post())) { 254 | var_dump($user->isAttributeChanged('contactData')); // outputs `false` 255 | 256 | $user->refreshFromEmbedded(); 257 | var_dump($user->isAttributeChanged('contactData')); // outputs `true` 258 | } 259 | ``` 260 | 261 | In case you are applying 'embedded' functionality to an ActiveRecord class, the best place for the data synchronization 262 | is [[\yii\db\BaseActiveRecord::beforeSave()]] method. For example, application of this extension to the [[\yii\mongodb\ActiveRecord]] 263 | class may look like following: 264 | 265 | ```php 266 | use yii2tech\embedded\ContainerInterface; 267 | use yii2tech\embedded\ContainerTrait; 268 | 269 | class ActiveRecord extends \yii\mongodb\ActiveRecord implements ContainerInterface 270 | { 271 | use ContainerTrait; 272 | 273 | public function beforeSave($insert) 274 | { 275 | if (!parent::beforeSave($insert)) { 276 | return false; 277 | } 278 | $this->refreshFromEmbedded(); // populate this model attributes from embedded models' ones, ensuring they are marked as 'changed' before saving 279 | return true; 280 | } 281 | } 282 | ``` 283 | 284 | 285 | ## Predefined model classes 286 | 287 | This extension is generic and may be applied to any model with complex attributes. However, to simplify integration with 288 | common solutions several base classes are provided by this extension: 289 | - [[\yii2tech\embedded\mongodb\ActiveRecord]] - MongoDB ActiveRecord with embedded feature built-in 290 | - [[\yii2tech\embedded\mongodb\ActiveRecordFile]] - MongoDB GridFS ActiveRecord with embedded feature built-in 291 | - [[\yii2tech\embedded\elasticsearch\ActiveRecord]] - ElasticSearch ActiveRecord with embedded feature built-in 292 | 293 | Provided ActiveRecord classes already implement [[\yii2tech\embedded\ContainerInterface]] and invoke `refreshFromEmbedded()` 294 | on `beforeSave()` stage. 295 | For example, if you are using MongoDB and wish to work with sub-documents, you may simply switch extending from 296 | regular [[\yii\mongodb\ActiveRecord]] to [[\yii2tech\embedded\mongodb\ActiveRecord]]: 297 | 298 | ```php 299 | class User extends \yii2tech\embedded\mongodb\ActiveRecord 300 | { 301 | public static function collectionName() 302 | { 303 | return 'customer'; 304 | } 305 | 306 | public function attributes() 307 | { 308 | return ['_id', 'name', 'email', 'addressData', 'status']; 309 | } 310 | 311 | public function embedAddress() 312 | { 313 | return $this->mapEmbedded('addressData', UserAddress::className()); 314 | } 315 | } 316 | ``` 317 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii2tech/embedded", 3 | "description": "Provides support for embedded (nested) models in Yii2", 4 | "keywords": ["yii2", "embed", "embedded", "nested", "mongo", "mongodb", "elastic", "elasticsearch", "json"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yii2tech/embedded/issues", 9 | "forum": "http://www.yiiframework.com/forum/", 10 | "wiki": "https://github.com/yii2tech/embedded/wiki", 11 | "source": "https://github.com/yii2tech/embedded" 12 | }, 13 | "authors": [ 14 | { 15 | "name": "Paul Klimov", 16 | "email": "klimov.paul@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "yiisoft/yii2": "~2.0.14" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "composer", 25 | "url": "https://asset-packagist.org" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": {"yii2tech\\embedded\\": "src"} 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.0.x-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ContainerInterface.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0 22 | */ 23 | interface ContainerInterface 24 | { 25 | /** 26 | * Sets embedded object or list of objects. 27 | * @param string $name embedded name 28 | * @param object|object[]|null $value embedded value. 29 | */ 30 | public function setEmbedded($name, $value); 31 | 32 | /** 33 | * Returns embedded object or list of objects. 34 | * @param string $name embedded name. 35 | * @return object|object[]|null embedded value. 36 | */ 37 | public function getEmbedded($name); 38 | 39 | /** 40 | * Checks if asked embedded declaration exists. 41 | * @param string $name embedded name 42 | * @return bool whether embedded declaration exists. 43 | */ 44 | public function hasEmbedded($name); 45 | 46 | /** 47 | * Returns list of values from embedded objects named by source fields. 48 | * @return array embedded values. 49 | */ 50 | public function getEmbeddedValues(); 51 | 52 | /** 53 | * Fills up own fields by values fetched from embedded objects. 54 | */ 55 | public function refreshFromEmbedded(); 56 | 57 | /** 58 | * Returns mapping information about specified embedded entity. 59 | * @param string $name embedded name. 60 | * @throws \yii\base\InvalidArgumentException if specified embedded does not exists. 61 | * @throws \yii\base\InvalidConfigException on invalid mapping declaration. 62 | * @return Mapping embedded mapping. 63 | */ 64 | public function getEmbeddedMapping($name); 65 | } -------------------------------------------------------------------------------- /src/ContainerTrait.php: -------------------------------------------------------------------------------- 1 | mapEmbedded('profileData', Profile::className()); 44 | * } 45 | * 46 | * public function embedComments() 47 | * { 48 | * return $this->mapEmbeddedList('commentsData', Comment::className()); 49 | * } 50 | * } 51 | * 52 | * $user = new User(); 53 | * $user->profile->firstName = 'John'; 54 | * $user->profile->lastName = 'Doe'; 55 | * 56 | * $comment = new Comment(); 57 | * $user->comments[] = $comment; 58 | * ``` 59 | * 60 | * In order to synchronize values between embedded entities and container use [[refreshFromEmbedded()]] method. 61 | * 62 | * @see ContainerInterface 63 | * @see Mapping 64 | * 65 | * @author Paul Klimov 66 | * @since 1.0 67 | */ 68 | trait ContainerTrait 69 | { 70 | /** 71 | * @var Mapping[] 72 | */ 73 | private $_embedded = []; 74 | 75 | /** 76 | * PHP getter magic method. 77 | * This method is overridden so that embedded objects can be accessed like properties. 78 | * 79 | * @param string $name property name 80 | * @throws \yii\base\InvalidArgumentException if relation name is wrong 81 | * @return mixed property value 82 | * @see getAttribute() 83 | */ 84 | public function __get($name) 85 | { 86 | if ($this->hasEmbedded($name)) { 87 | return $this->getEmbedded($name); 88 | } 89 | return parent::__get($name); 90 | } 91 | 92 | /** 93 | * PHP setter magic method. 94 | * This method is overridden so that embedded objects can be accessed like properties. 95 | * @param string $name property name 96 | * @param mixed $value property value 97 | */ 98 | public function __set($name, $value) 99 | { 100 | if ($this->hasEmbedded($name)) { 101 | $this->setEmbedded($name, $value); 102 | } else { 103 | parent::__set($name, $value); 104 | } 105 | } 106 | 107 | /** 108 | * Checks if a property value is null. 109 | * This method overrides the parent implementation by checking if the embedded object is null or not. 110 | * @param string $name the property name or the event name 111 | * @return bool whether the property value is null 112 | */ 113 | public function __isset($name) 114 | { 115 | if (isset($this->_embedded[$name])) { 116 | return ($this->_embedded[$name]->getValue($this) !== null); 117 | } 118 | return parent::__isset($name); 119 | } 120 | 121 | /** 122 | * Sets a component property to be null. 123 | * This method overrides the parent implementation by clearing 124 | * the specified embedded object. 125 | * @param string $name the property name or the event name 126 | */ 127 | public function __unset($name) 128 | { 129 | if (isset($this->_embedded[$name])) { 130 | ($this->_embedded[$name]->setValue(null)); 131 | } else { 132 | parent::__unset($name); 133 | } 134 | } 135 | 136 | /** 137 | * Sets embedded object or list of objects. 138 | * @param string $name embedded name 139 | * @param object|object[]|null $value embedded value. 140 | */ 141 | public function setEmbedded($name, $value) 142 | { 143 | $this->getEmbeddedMapping($name)->setValue($value); 144 | } 145 | 146 | /** 147 | * Returns embedded object or list of objects. 148 | * @param string $name embedded name. 149 | * @return object|object[]|null embedded value. 150 | */ 151 | public function getEmbedded($name) 152 | { 153 | return $this->getEmbeddedMapping($name)->getValue($this); 154 | } 155 | 156 | /** 157 | * Returns mapping information about specified embedded entity. 158 | * @param string $name embedded name. 159 | * @throws \yii\base\InvalidArgumentException if specified embedded does not exists. 160 | * @throws \yii\base\InvalidConfigException on invalid mapping declaration. 161 | * @return Mapping embedded mapping. 162 | */ 163 | public function getEmbeddedMapping($name) 164 | { 165 | if (!isset($this->_embedded[$name])) { 166 | $method = $this->composeEmbeddedDeclarationMethodName($name); 167 | if (!method_exists($this, $method)) { 168 | throw new InvalidArgumentException("'" . get_class($this) . "' has no declaration ('{$method}()') for the embedded '{$name}'"); 169 | } 170 | $mapping = call_user_func([$this, $method]); 171 | if (!$mapping instanceof Mapping) { 172 | throw new InvalidConfigException("Mapping declaration '" . get_class($this) . "::{$method}()' should return instance of '" . Mapping::className() . "'"); 173 | } 174 | $this->_embedded[$name] = $mapping; 175 | } 176 | return $this->_embedded[$name]; 177 | } 178 | 179 | /** 180 | * Checks if asked embedded declaration exists. 181 | * @param string $name embedded name 182 | * @return bool whether embedded declaration exists. 183 | */ 184 | public function hasEmbedded($name) 185 | { 186 | return (isset($this->_embedded[$name])) || method_exists($this, $this->composeEmbeddedDeclarationMethodName($name)); 187 | } 188 | 189 | /** 190 | * Declares embedded object. 191 | * @param string $source source field name 192 | * @param string|array $target target class or array configuration. 193 | * @param array $config mapping extra configuration. 194 | * @return Mapping embedding mapping instance. 195 | */ 196 | public function mapEmbedded($source, $target, array $config = []) 197 | { 198 | return Yii::createObject(array_merge( 199 | [ 200 | 'class' => Mapping::className(), 201 | 'source' => $source, 202 | 'target' => $target, 203 | 'multiple' => false, 204 | ], 205 | $config 206 | )); 207 | } 208 | 209 | /** 210 | * Declares embedded list of objects. 211 | * @param string $source source field name 212 | * @param string|array $target target class or array configuration. 213 | * @param array $config mapping extra configuration. 214 | * @return Mapping embedding mapping instance. 215 | */ 216 | public function mapEmbeddedList($source, $target, array $config = []) 217 | { 218 | return Yii::createObject(array_merge( 219 | [ 220 | 'class' => Mapping::className(), 221 | 'source' => $source, 222 | 'target' => $target, 223 | 'multiple' => true, 224 | ], 225 | $config 226 | )); 227 | } 228 | 229 | /** 230 | * @param string $name embedded name. 231 | * @return string declaration method name. 232 | */ 233 | private function composeEmbeddedDeclarationMethodName($name) 234 | { 235 | return 'embed' . $name; 236 | } 237 | 238 | /** 239 | * Returns list of values from embedded objects named by source fields. 240 | * @return array embedded values. 241 | */ 242 | public function getEmbeddedValues() 243 | { 244 | $values = []; 245 | foreach ($this->_embedded as $embedded) { 246 | if (!$embedded->getIsValueInitialized()) { 247 | continue; 248 | } 249 | $values[$embedded->source] = $embedded->extractValues($this); 250 | } 251 | return $values; 252 | } 253 | 254 | /** 255 | * Fills up own fields by values fetched from embedded objects. 256 | */ 257 | public function refreshFromEmbedded() 258 | { 259 | foreach ($this->getEmbeddedValues() as $name => $value) { 260 | $this->$name = $value; 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /src/Mapping.php: -------------------------------------------------------------------------------- 1 | 26 | * @since 1.0 27 | */ 28 | class Mapping extends BaseObject 29 | { 30 | /** 31 | * @var string name of the container source field or property. 32 | */ 33 | public $source; 34 | /** 35 | * @var string|array target class name or array object configuration. 36 | */ 37 | public $target; 38 | /** 39 | * @var bool whether list of objects should match the source value. 40 | */ 41 | public $multiple; 42 | /** 43 | * @var bool whether to create empty object or list of objects, if the source field is null. 44 | * If disabled [[getValue()]] will produce `null` value from null source. 45 | */ 46 | public $createFromNull = true; 47 | /** 48 | * @var bool whether to set `null` for the owner [[source]] field, after the embedded value created. 49 | * While enabled this saves memory usage, but also makes it impossible to use embedded and raw value at the same time. 50 | */ 51 | public $unsetSource = true; 52 | 53 | /** 54 | * @var mixed actual embedded value. 55 | */ 56 | private $_value = false; 57 | 58 | 59 | /** 60 | * Sets the embedded value. 61 | * @param array|object|null $value actual value. 62 | * @throws InvalidArgumentException on invalid argument 63 | */ 64 | public function setValue($value) 65 | { 66 | if (!is_null($value)) { 67 | if ($this->multiple) { 68 | if (is_array($value)) { 69 | $arrayObject = new ArrayObject(); 70 | foreach ($value as $k => $v) { 71 | $arrayObject[$k] = $v; 72 | } 73 | $value = $arrayObject; 74 | } elseif (!($value instanceof \ArrayAccess)) { 75 | throw new InvalidArgumentException("Value should either an array or a null, '" . gettype($value) . "' given."); 76 | } 77 | } else { 78 | if (!is_object($value)) { 79 | throw new InvalidArgumentException("Value should either an object or a null, '" . gettype($value) . "' given."); 80 | } 81 | } 82 | } 83 | 84 | $this->_value = $value; 85 | } 86 | 87 | /** 88 | * Returns actual embedded value. 89 | * @param object $owner owner object. 90 | * @return object|object[]|null embedded value. 91 | */ 92 | public function getValue($owner) 93 | { 94 | if ($this->_value === false) { 95 | $this->_value = $this->createValue($owner); 96 | } 97 | return $this->_value; 98 | } 99 | 100 | /** 101 | * @return bool whether embedded value has been already initialized or not. 102 | * @since 1.0.1 103 | */ 104 | public function getIsValueInitialized() 105 | { 106 | return $this->_value !== false; 107 | } 108 | 109 | /** 110 | * @param object $owner owner object 111 | * @throws InvalidArgumentException on invalid source. 112 | * @return array|null|object value. 113 | */ 114 | private function createValue($owner) 115 | { 116 | if (is_array($this->target)) { 117 | $targetConfig = $this->target; 118 | } else { 119 | $targetConfig = ['class' => $this->target]; 120 | } 121 | 122 | $sourceValue = $owner->{$this->source}; 123 | if ($this->createFromNull && $sourceValue === null) { 124 | $sourceValue = []; 125 | } 126 | if ($sourceValue === null) { 127 | return null; 128 | } 129 | 130 | if ($this->multiple) { 131 | $result = new ArrayObject(); 132 | foreach ($sourceValue as $key => $frame) { 133 | if (!is_array($frame)) { 134 | throw new InvalidArgumentException("Source value for the embedded should be an array."); 135 | } 136 | $result[$key] = Yii::createObject(array_merge($targetConfig, $frame)); 137 | } 138 | } else { 139 | if (!is_array($sourceValue)) { 140 | if (!$sourceValue instanceof Traversable) { 141 | throw new InvalidArgumentException("Source value for the embedded should be an array or 'Traversable' instance."); 142 | } 143 | $sourceValue = iterator_to_array($sourceValue); 144 | } 145 | $result = Yii::createObject(array_merge($targetConfig, $sourceValue)); 146 | } 147 | 148 | if ($this->unsetSource) { 149 | $owner->{$this->source} = null; 150 | } 151 | 152 | return $result; 153 | } 154 | 155 | /** 156 | * Extract embedded object(s) values as array. 157 | * @param object $owner owner object 158 | * @return array|null extracted values. 159 | */ 160 | public function extractValues($owner) 161 | { 162 | $embeddedValue = $this->getValue($owner); 163 | if ($embeddedValue === null) { 164 | $value = null; 165 | } else { 166 | if ($this->multiple) { 167 | $value = []; 168 | foreach ($embeddedValue as $key => $object) { 169 | $value[$key] = $this->extractObjectValues($object); 170 | } 171 | } else { 172 | $value = $this->extractObjectValues($embeddedValue); 173 | } 174 | } 175 | return $value; 176 | } 177 | 178 | /** 179 | * @param object $object 180 | * @return array 181 | */ 182 | private function extractObjectValues($object) 183 | { 184 | $values = ArrayHelper::toArray($object, [], false); 185 | if ($object instanceof ContainerInterface) { 186 | $values = array_merge($values, $object->getEmbeddedValues()); 187 | } 188 | return $values; 189 | } 190 | } -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | mapEmbedded('contactData', Contact::className()); 28 | * } 29 | * 30 | * public function rules() 31 | * { 32 | * return [ 33 | * ['contact', 'yii2tech\embedded\Validator'], 34 | * ] 35 | * } 36 | * } 37 | * 38 | * class Contact extends Model 39 | * { 40 | * public $email; 41 | * 42 | * public function rules() 43 | * { 44 | * return [ 45 | * ['email', 'required'], 46 | * ['email', 'email'], 47 | * ] 48 | * } 49 | * } 50 | * ``` 51 | * 52 | * > Note: pay attention that this validator must be set for the embedded model name - not for its source attribute. 53 | * Do not mix them up! 54 | * 55 | * @see ContainerInterface 56 | * 57 | * @author Paul Klimov 58 | * @since 1.0 59 | */ 60 | class Validator extends \yii\validators\Validator 61 | { 62 | /** 63 | * @var bool whether to add an error message to embedded source attribute instead of embedded name itself. 64 | */ 65 | public $addErrorToSource = true; 66 | /** 67 | * @var bool whether to run validation only in case embedded model(s) has been already initialized (requested as 68 | * object at least once). This option is disabled by default. 69 | * 70 | * @see Mapping::getIsValueInitialized() 71 | * 72 | * @since 1.0.1 73 | */ 74 | public $initializedOnly = false; 75 | 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function init() 81 | { 82 | parent::init(); 83 | if ($this->message === null) { 84 | $this->message = Yii::t('yii', '{attribute} is invalid.'); 85 | } 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function validateAttribute($model, $attribute) 92 | { 93 | if (!($model instanceof ContainerInterface)) { 94 | throw new InvalidConfigException('Owner model must implement "yii2tech\embedded\ContainerInterface" interface.'); 95 | } 96 | 97 | $mapping = $model->getEmbeddedMapping($attribute); 98 | 99 | if ($this->initializedOnly && !$mapping->getIsValueInitialized()) { 100 | return; 101 | } 102 | 103 | $embedded = $model->getEmbedded($attribute); 104 | 105 | if ($mapping->multiple) { 106 | if (!is_array($embedded) && !($embedded instanceof \IteratorAggregate)) { 107 | $error = $this->message; 108 | } else { 109 | foreach ($embedded as $embeddedModel) { 110 | if (!($embeddedModel instanceof Model)) { 111 | throw new InvalidConfigException('Embedded object "' . get_class($embeddedModel) . '" must be an instance or descendant of "' . Model::className() . '".'); 112 | } 113 | if (!$embeddedModel->validate()) { 114 | $error = $this->message; 115 | } 116 | } 117 | } 118 | } else { 119 | if (!($embedded instanceof Model)) { 120 | throw new InvalidConfigException('Embedded object "' . get_class($embedded) . '" must be an instance or descendant of "' . Model::className() . '".'); 121 | } 122 | if (!$embedded->validate()) { 123 | $error = $this->message; 124 | } 125 | } 126 | 127 | if (!empty($error)) { 128 | $this->addError($model, $this->addErrorToSource ? $mapping->source : $attribute, $error); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/elasticsearch/ActiveRecord.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0 22 | */ 23 | class ActiveRecord extends \yii\elasticsearch\ActiveRecord implements ContainerInterface 24 | { 25 | use ContainerTrait; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function beforeSave($insert) 31 | { 32 | if (!parent::beforeSave($insert)) { 33 | return false; 34 | } 35 | $this->refreshFromEmbedded(); 36 | return true; 37 | } 38 | } -------------------------------------------------------------------------------- /src/mongodb/ActiveRecord.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0 22 | */ 23 | class ActiveRecord extends \yii\mongodb\ActiveRecord implements ContainerInterface 24 | { 25 | use ContainerTrait; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function beforeSave($insert) 31 | { 32 | if (!parent::beforeSave($insert)) { 33 | return false; 34 | } 35 | $this->refreshFromEmbedded(); 36 | return true; 37 | } 38 | } -------------------------------------------------------------------------------- /src/mongodb/ActiveRecordFile.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0 22 | */ 23 | class ActiveRecordFile extends \yii\mongodb\file\ActiveRecord implements ContainerInterface 24 | { 25 | use ContainerTrait; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function beforeSave($insert) 31 | { 32 | if (!parent::beforeSave($insert)) { 33 | return false; 34 | } 35 | $this->refreshFromEmbedded(); 36 | return true; 37 | } 38 | } --------------------------------------------------------------------------------