├── 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 | [](https://packagist.org/packages/yii2tech/embedded)
16 | [](https://packagist.org/packages/yii2tech/embedded)
17 | [](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 | }
--------------------------------------------------------------------------------