Completed
Branch dbal-improvement (e43d29)
by Anton
06:02
created

Document::dotGet()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 35
rs 6.7273
cc 7
eloc 17
nc 6
nop 1
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ODM;
9
10
use Spiral\Models\AccessorInterface;
11
use Spiral\Models\ActiveEntityInterface;
12
use Spiral\Models\EntityInterface;
13
use Spiral\Models\Events\EntityEvent;
14
use Spiral\ODM\Entities\DocumentSelector;
15
use Spiral\ODM\Entities\DocumentSource;
16
use Spiral\ODM\Exceptions\DefinitionException;
17
use Spiral\ODM\Exceptions\DocumentException;
18
use Spiral\ODM\Exceptions\ODMException;
19
20
/**
21
 * DocumentEntity with added ActiveRecord methods and ability to connect to associated source.
22
 *
23
 * Document also provides an ability to specify aggregations using it's schema:
24
 *
25
 * protected $schema = [
26
 *     ...,
27
 *     'outer' => [self::ONE => Outer::class, [   //Reference to outer document using internal
28
 *          '_id' => 'self::outerID'              //outerID value
29
 *     ]],
30
 *     'many' => [self::MANY => Outer::class, [   //Reference to many outer document using
31
 *          'innerID' => 'self::_id'              //document primary key
32
 *     ]]
33
 * ];
34
 *
35
 * Note: self::{name} construction will be replaced with document value in resulted query, even
36
 * in case of arrays ;) You can also use dot notation to get value from nested document.
37
 *
38
 * @var array
39
 */
40
class Document extends DocumentEntity implements ActiveEntityInterface
41
{
42
    /**
43
     * Indication that save methods must be validated by default, can be altered by calling save
44
     * method with user arguments.
45
     */
46
    const VALIDATE_SAVE = true;
47
48
    /**
49
     * Collection name where document should be stored into.
50
     *
51
     * @var string
52
     */
53
    protected $collection = null;
54
55
    /**
56
     * Database name/id where document related collection located in.
57
     *
58
     * @var string|null
59
     */
60
    protected $database = null;
61
62
    /**
63
     * Set of indexes to be created for associated collection. Use self::INDEX_OPTIONS or "@options"
64
     * for additional parameters.
65
     *
66
     * Example:
67
     * protected $indexes = [
68
     *      ['email' => 1, '@options' => ['unique' => true]],
69
     *      ['name' => 1]
70
     * ];
71
     *
72
     * @link http://php.net/manual/en/mongocollection.ensureindex.php
73
     * @var array
74
     */
75
    protected $indexes = [];
76
77
    /**
78
     * @see Component::staticContainer()
79
     * @param array           $fields
80
     * @param EntityInterface $parent
81
     * @param ODM             $odm
82
     * @param array           $odmSchema
83
     */
84
    public function __construct(
85
        $fields = [],
86
        EntityInterface $parent = null,
87
        ODM $odm = null,
88
        $odmSchema = null
89
    ) {
90
        parent::__construct($fields, $parent, $odm, $odmSchema);
91
92
        if ((!$this->isLoaded() && !$this->isEmbedded())) {
93
            //Document is newly created instance
94
            $this->solidState(true)->invalidate();
95
        }
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     *
101
     * @return \MongoId|null
102
     */
103
    public function primaryKey()
104
    {
105
        return isset($this->fields['_id']) ? $this->fields['_id'] : null;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function isLoaded()
112
    {
113
        return (bool)$this->primaryKey();
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     *
119
     * Create or update document data in database.
120
     *
121
     * @param bool|null $validate Overwrite default option declared in VALIDATE_SAVE to force or
122
     *                            disable validation before saving.
123
     * @throws DocumentException
124
     * @event saving()
125
     * @event saved()
126
     * @event updating()
127
     * @event updated()
128
     */
129
    public function save($validate = null)
130
    {
131
        $validate = !is_null($validate) ? $validate : static::VALIDATE_SAVE;
132
133
        if ($validate && !$this->isValid()) {
134
            //Using default model behaviour
135
            return false;
136
        }
137
138
        if ($this->isEmbedded()) {
139
            throw new DocumentException(
140
                "Embedded document '" . get_class($this) . "' can not be saved into collection."
141
            );
142
        }
143
144
        //Associated collection
145
        $collection = $this->mongoCollection();
146
147
        if (!$this->isLoaded()) {
148
            $this->dispatch('saving', new EntityEvent($this));
149
            unset($this->fields['_id']);
150
151
            //Create new document
152
            $collection->insert($this->fields = $this->serializeData());
153
154
            $this->dispatch('saved', new EntityEvent($this));
155 View Code Duplication
        } elseif ($this->isSolid() || $this->hasUpdates()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
            $this->dispatch('updating', new EntityEvent($this));
157
158
            //Update existed document
159
            $collection->update(['_id' => $this->primaryKey()], $this->buildAtomics());
160
161
            $this->dispatch('updated', new EntityEvent($this));
162
        }
163
164
        $this->flushUpdates();
165
166
        return true;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     *
172
     * @throws DocumentException
173
     * @event deleting()
174
     * @event deleted()
175
     */
176
    public function delete()
177
    {
178
        if ($this->isEmbedded()) {
179
            throw new DocumentException(
180
                "Embedded document '" . get_class($this) . "' can not be deleted from collection."
181
            );
182
        }
183
184
        $this->dispatch('deleting', new EntityEvent($this));
185
        if ($this->isLoaded()) {
186
            $this->mongoCollection()->remove(['_id' => $this->primaryKey()]);
187
        }
188
189
        $this->fields = $this->odmSchema()[ODM::D_DEFAULTS];
190
        $this->dispatch('deleted', new EntityEvent($this));
191
    }
192
193
    /**
194
     * {@inheritdoc} See DataEntity class.
195
     *
196
     * ODM: Get instance of Collection or Document associated with described aggregation.
197
     *
198
     * Example:
199
     * $parentGroup = $user->group();
200
     * echo $user->posts()->where(['published' => true])->count();
201
     *
202
     * @return mixed|AccessorInterface|DocumentSelector|Document[]|Document
203
     * @throws DocumentException
204
     */
205
    public function __call($offset, array $arguments)
206
    {
207
        if (!isset($this->odmSchema()[ODM::D_AGGREGATIONS][$offset])) {
208
            //Field getter/setter
209
            return parent::__call($offset, $arguments);
210
        }
211
212
        return $this->aggregate($offset);
213
    }
214
215
    /**
216
     * Get document aggregation.
217
     *
218
     * @param string $aggregation
219
     * @return DocumentSelector|Document
220
     */
221
    public function aggregate($aggregation)
222
    {
223
        if (!isset($this->odmSchema()[ODM::D_AGGREGATIONS][$aggregation])) {
224
            throw new DocumentException("Undefined aggregation '{$aggregation}'.");
225
        }
226
227
        $aggregation = $this->odmSchema()[ODM::D_AGGREGATIONS][$aggregation];
228
229
        //Query preparations
230
        $query = $this->interpolateQuery($aggregation[ODM::AGR_QUERY]);
231
232
        //Every aggregation works thought ODM collection
233
        $selector = $this->odm->selector($aggregation[ODM::ARG_CLASS], $query);
234
235
        //In future i might need separate class to represent aggregation
236
        if ($aggregation[ODM::AGR_TYPE] == self::ONE) {
237
            return $selector->findOne();
238
        }
239
240
        return $selector;
241
    }
242
243
    /**
244
     * @return Object
245
     */
246
    public function __debugInfo()
247
    {
248
        if (empty($this->collection)) {
249
            return (object)[
250
                'fields'  => $this->getFields(),
251
                'atomics' => $this->hasUpdates() ? $this->buildAtomics() : [],
252
                'errors'  => $this->getErrors()
253
            ];
254
        }
255
256
        return (object)[
257
            'collection' => $this->odmSchema()[ODM::D_DB] . '/' . $this->collection,
258
            'fields'     => $this->getFields(),
259
            'atomics'    => $this->hasUpdates() ? $this->buildAtomics() : [],
260
            'errors'     => $this->getErrors()
261
        ];
262
    }
263
264
    /**
265
     * Instance of ODM Selector associated with specific document.
266
     *
267
     * @see Component::staticContainer()
268
     * @param ODM $odm ODM component, global container will be called if not instance provided.
269
     * @return DocumentSource
270
     * @throws ODMException
271
     */
272 View Code Duplication
    public static function source(ODM $odm = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
    {
274
        if (empty($odm)) {
275
            //Using global container as fallback
276
            $odm = self::staticContainer()->get(ODM::class);
277
        }
278
279
        return $odm->source(static::class);
280
    }
281
282
    /**
283
     * Just an alias.
284
     *
285
     * @return DocumentSource
286
     */
287
    public static function find()
288
    {
289
        return static::source();
290
    }
291
292
    /**
293
     * {@inheritdoc}
294
     *
295
     * Accessor options include field type resolved by DocumentSchema.
296
     *
297
     * @throws ODMException
298
     * @throws DefinitionException
299
     */
300
    protected function createAccessor($accessor, $value)
301
    {
302
        $accessor = parent::createAccessor($accessor, $value);
303
304
        if (
305
            $accessor instanceof CompositableInterface
306
            && !$this->isLoaded()
307
            && !$this->isEmbedded()
308
        ) {
309
            //Newly created object
310
            $accessor->invalidate();
311
        }
312
313
        return $accessor;
314
    }
315
316
    /**
317
     * Interpolate aggregation query with document values.
318
     *
319
     * @param array $query
320
     * @return array
321
     */
322
    protected function interpolateQuery(array $query)
323
    {
324
        $fields = $this->fields;
325
        array_walk_recursive($query, function (&$value) use ($fields) {
326
            if (strpos($value, 'self::') === 0) {
327
                $value = $this->dotGet(substr($value, 6));
328
            }
329
        });
330
331
        return $query;
332
    }
333
334
    /**
335
     * Get field value using dot notation.
336
     *
337
     * @param string $name
338
     * @return mixed|null
339
     */
340
    private function dotGet($name)
341
    {
342
        /**
343
         * @var EntityInterface|AccessorInterface|array $source
344
         */
345
        $source = $this;
346
347
        $path = explode('.', $name);
348
        foreach ($path as $step) {
349
            if ($source instanceof EntityInterface) {
350
                if (!$source->hasField($step)) {
351
                    return null;
352
                }
353
354
                //Sub entity
355
                $source = $source->getField($step);
356
                continue;
357
            }
358
359
            if ($source instanceof AccessorInterface) {
360
                $source = $source->serializeData();
361
                continue;
362
            }
363
364
            if (is_array($source) && array_key_exists($step, $source)) {
365
                $source = &$source[$step];
366
                continue;
367
            }
368
369
            //Unable to resolve value, an exception required here
370
            return null;
371
        }
372
373
        return $source;
374
    }
375
376
    /**
377
     * Associated mongo collection.
378
     *
379
     * @return \MongoCollection
380
     */
381
    private function mongoCollection()
382
    {
383
        return $this->odm->mongoCollection(static::class);
384
    }
385
}