Completed
Branch develop (009098)
by Anton
04:36
created

Document::find()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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
use Spiral\ODM\Traits\FindTrait;
20
21
/**
22
 * DocumentEntity with added ActiveRecord methods and ability to connect to associated source.
23
 *
24
 * Document also provides an ability to specify aggregations using it's schema:
25
 *
26
 * protected $schema = [
27
 *     ...,
28
 *     'outer' => [self::ONE => Outer::class, [   //Reference to outer document using internal
29
 *          '_id' => 'self::outerID'              //outerID value
30
 *     ]],
31
 *     'many' => [self::MANY => Outer::class, [   //Reference to many outer document using
32
 *          'innerID' => 'self::_id'              //document primary key
33
 *     ]]
34
 * ];
35
 *
36
 * Note: self::{name} construction will be replaced with document value in resulted query, even
37
 * in case of arrays ;) You can also use dot notation to get value from nested document.
38
 *
39
 * @var array
40
 */
41
class Document extends DocumentEntity implements ActiveEntityInterface
42
{
43
    /**
44
     * Static find method.
45
     */
46
    use FindTrait;
47
48
    /**
49
     * Indication that save methods must be validated by default, can be altered by calling save
50
     * method with user arguments.
51
     */
52
    const VALIDATE_SAVE = true;
53
54
    /**
55
     * Collection name where document should be stored into.
56
     *
57
     * @var string
58
     */
59
    protected $collection = null;
60
61
    /**
62
     * Database name/id where document related collection located in.
63
     *
64
     * @var string|null
65
     */
66
    protected $database = null;
67
68
    /**
69
     * Set of indexes to be created for associated collection. Use self::INDEX_OPTIONS or "@options"
70
     * for additional parameters.
71
     *
72
     * Example:
73
     * protected $indexes = [
74
     *      ['email' => 1, '@options' => ['unique' => true]],
75
     *      ['name' => 1]
76
     * ];
77
     *
78
     * @link http://php.net/manual/en/mongocollection.ensureindex.php
79
     * @var array
80
     */
81
    protected $indexes = [];
82
83
    /**
84
     * @see Component::staticContainer()
85
     * @param array           $fields
86
     * @param EntityInterface $parent
87
     * @param ODM             $odm
88
     * @param array           $odmSchema
89
     */
90
    public function __construct(
91
        $fields = [],
92
        EntityInterface $parent = null,
93
        ODM $odm = null,
94
        $odmSchema = null
95
    ) {
96
        parent::__construct($fields, $parent, $odm, $odmSchema);
97
98
        if ((!$this->isLoaded() && !$this->isEmbedded())) {
99
            //Document is newly created instance
100
            $this->solidState(true)->invalidate();
101
        }
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     *
107
     * @return \MongoId|null
108
     */
109
    public function primaryKey()
110
    {
111
        return isset($this->fields['_id']) ? $this->fields['_id'] : null;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function isLoaded()
118
    {
119
        return (bool)$this->primaryKey();
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     *
125
     * Create or update document data in database.
126
     *
127
     * @param bool|null $validate Overwrite default option declared in VALIDATE_SAVE to force or
128
     *                            disable validation before saving.
129
     * @throws DocumentException
130
     * @event saving()
131
     * @event saved()
132
     * @event updating()
133
     * @event updated()
134
     */
135
    public function save($validate = null)
136
    {
137
        $validate = !is_null($validate) ? $validate : static::VALIDATE_SAVE;
138
139
        if ($validate && !$this->isValid()) {
140
            //Using default model behaviour
141
            return false;
142
        }
143
144
        if ($this->isEmbedded()) {
145
            throw new DocumentException(
146
                "Embedded document '" . get_class($this) . "' can not be saved into collection."
147
            );
148
        }
149
150
        //Associated collection
151
        $collection = $this->mongoCollection();
152
153
        if (!$this->isLoaded()) {
154
            $this->dispatch('saving', new EntityEvent($this));
155
            unset($this->fields['_id']);
156
157
            //Create new document
158
            $collection->insert($this->fields = $this->serializeData());
159
160
            $this->dispatch('saved', new EntityEvent($this));
161 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...
162
            $this->dispatch('updating', new EntityEvent($this));
163
164
            //Update existed document
165
            $collection->update(['_id' => $this->primaryKey()], $this->buildAtomics());
166
167
            $this->dispatch('updated', new EntityEvent($this));
168
        }
169
170
        $this->flushUpdates();
171
172
        return true;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     *
178
     * @throws DocumentException
179
     * @event deleting()
180
     * @event deleted()
181
     */
182
    public function delete()
183
    {
184
        if ($this->isEmbedded()) {
185
            throw new DocumentException(
186
                "Embedded document '" . get_class($this) . "' can not be deleted from collection."
187
            );
188
        }
189
190
        $this->dispatch('deleting', new EntityEvent($this));
191
        if ($this->isLoaded()) {
192
            $this->mongoCollection()->remove(['_id' => $this->primaryKey()]);
193
        }
194
195
        $this->fields = $this->odmSchema()[ODM::D_DEFAULTS];
196
        $this->dispatch('deleted', new EntityEvent($this));
197
    }
198
199
    /**
200
     * {@inheritdoc} See DataEntity class.
201
     *
202
     * ODM: Get instance of Collection or Document associated with described aggregation.
203
     *
204
     * Example:
205
     * $parentGroup = $user->group();
206
     * echo $user->posts()->where(['published' => true])->count();
207
     *
208
     * @return mixed|AccessorInterface|DocumentSelector|Document[]|Document
209
     * @throws DocumentException
210
     */
211
    public function __call($offset, array $arguments)
212
    {
213
        if (!isset($this->odmSchema()[ODM::D_AGGREGATIONS][$offset])) {
214
            //Field getter/setter
215
            return parent::__call($offset, $arguments);
216
        }
217
218
        return $this->aggregate($offset);
219
    }
220
221
    /**
222
     * Get document aggregation.
223
     *
224
     * @param string $aggregation
225
     * @return DocumentSelector|Document
226
     */
227
    public function aggregate($aggregation)
228
    {
229
        if (!isset($this->odmSchema()[ODM::D_AGGREGATIONS][$aggregation])) {
230
            throw new DocumentException("Undefined aggregation '{$aggregation}'.");
231
        }
232
233
        $aggregation = $this->odmSchema()[ODM::D_AGGREGATIONS][$aggregation];
234
235
        //Query preparations
236
        $query = $this->interpolateQuery($aggregation[ODM::AGR_QUERY]);
237
238
        //Every aggregation works thought ODM collection
239
        $selector = $this->odm->selector($aggregation[ODM::ARG_CLASS], $query);
240
241
        //In future i might need separate class to represent aggregation
242
        if ($aggregation[ODM::AGR_TYPE] == self::ONE) {
243
            return $selector->findOne();
244
        }
245
246
        return $selector;
247
    }
248
249
    /**
250
     * @return Object
251
     */
252
    public function __debugInfo()
253
    {
254
        if (empty($this->collection)) {
255
            return (object)[
256
                'fields'  => $this->getFields(),
257
                'atomics' => $this->hasUpdates() ? $this->buildAtomics() : [],
258
                'errors'  => $this->getErrors()
259
            ];
260
        }
261
262
        return (object)[
263
            'collection' => $this->odmSchema()[ODM::D_DB] . '/' . $this->collection,
264
            'fields'     => $this->getFields(),
265
            'atomics'    => $this->hasUpdates() ? $this->buildAtomics() : [],
266
            'errors'     => $this->getErrors()
267
        ];
268
    }
269
270
    /**
271
     * Instance of ODM Selector associated with specific document.
272
     *
273
     * @see Component::staticContainer()
274
     * @param ODM $odm ODM component, global container will be called if not instance provided.
275
     * @return DocumentSource
276
     * @throws ODMException
277
     */
278 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...
279
    {
280
        if (empty($odm)) {
281
            //Using global container as fallback
282
            $odm = self::staticContainer()->get(ODM::class);
283
        }
284
285
        return $odm->source(static::class);
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     *
291
     * Accessor options include field type resolved by DocumentSchema.
292
     *
293
     * @throws ODMException
294
     * @throws DefinitionException
295
     */
296
    protected function createAccessor($accessor, $value)
297
    {
298
        $accessor = parent::createAccessor($accessor, $value);
299
300
        if (
301
            $accessor instanceof CompositableInterface
302
            && !$this->isLoaded()
303
            && !$this->isEmbedded()
304
        ) {
305
            //Newly created object
306
            $accessor->invalidate();
307
        }
308
309
        return $accessor;
310
    }
311
312
    /**
313
     * Interpolate aggregation query with document values.
314
     *
315
     * @param array $query
316
     * @return array
317
     */
318
    protected function interpolateQuery(array $query)
319
    {
320
        $fields = $this->fields;
321
        array_walk_recursive($query, function (&$value) use ($fields) {
322
            if (strpos($value, 'self::') === 0) {
323
                $value = $this->dotGet(substr($value, 6));
324
            }
325
        });
326
327
        return $query;
328
    }
329
330
    /**
331
     * Get field value using dot notation.
332
     *
333
     * @param string $name
334
     * @return mixed|null
335
     */
336
    private function dotGet($name)
337
    {
338
        /**
339
         * @var EntityInterface|AccessorInterface|array $source
340
         */
341
        $source = $this;
342
343
        $path = explode('.', $name);
344
        foreach ($path as $step) {
345
            if ($source instanceof EntityInterface) {
346
                if (!$source->hasField($step)) {
347
                    return null;
348
                }
349
350
                //Sub entity
351
                $source = $source->getField($step);
352
                continue;
353
            }
354
355
            if ($source instanceof AccessorInterface) {
356
                $source = $source->serializeData();
357
                continue;
358
            }
359
360
            if (is_array($source) && array_key_exists($step, $source)) {
361
                $source = &$source[$step];
362
                continue;
363
            }
364
365
            //Unable to resolve value, an exception required here
366
            return null;
367
        }
368
369
        return $source;
370
    }
371
372
    /**
373
     * Associated mongo collection.
374
     *
375
     * @return \MongoCollection
376
     */
377
    private function mongoCollection()
378
    {
379
        return $this->odm->mongoCollection(static::class);
380
    }
381
}