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()) { |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
} |
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.