GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( a271fd...48c1ee )
by De
02:28
created

Document::removeRelation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 6
rs 9.4286
cc 1
eloc 3
nc 1
nop 2
1
<?php
2
3
/**
4
 * This file is part of the PHPMongo package.
5
 *
6
 * (c) Dmytro Sokil <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sokil\Mongo;
13
14
use Sokil\Mongo\Document\RelationManager;
15
use Sokil\Mongo\Document\RevisionManager;
16
use Sokil\Mongo\Document\InvalidDocumentException;
17
use Sokil\Mongo\Collection\Definition;
18
use Sokil\Mongo\Document\OptimisticLockFailureException;
19
use Symfony\Component\EventDispatcher\EventDispatcher;
20
use GeoJson\Geometry\Geometry;
21
22
/**
23
 * Instance of this class is a representation of one document from collection.
24
 *
25
 * @link https://github.com/sokil/php-mongo#document-schema Document schema
26
 * @link https://github.com/sokil/php-mongo#create-new-document Create new document
27
 * @link https://github.com/sokil/php-mongo#get-and-set-data-in-document get and set data
28
 * @link https://github.com/sokil/php-mongo#storing-document Saving document
29
 * @link https://github.com/sokil/php-mongo#document-validation Validation
30
 * @link https://github.com/sokil/php-mongo#deleting-collections-and-documents Deleting documents
31
 * @link https://github.com/sokil/php-mongo#events Event handlers
32
 * @link https://github.com/sokil/php-mongo#behaviors Behaviors
33
 * @link https://github.com/sokil/php-mongo#relations Relations
34
 *
35
 * @method \Sokil\Mongo\Document onAfterConstruct(callable $handler, int $priority = 0)
36
 * @method \Sokil\Mongo\Document onBeforeValidate(callable $handler, int $priority = 0)
37
 * @method \Sokil\Mongo\Document onAfterValidate(callable $handler, int $priority = 0)
38
 * @method \Sokil\Mongo\Document onValidateError(callable $handler, int $priority = 0)
39
 * @method \Sokil\Mongo\Document onBeforeInsert(callable $handler, int $priority = 0)
40
 * @method \Sokil\Mongo\Document onAfterInsert(callable $handler, int $priority = 0)
41
 * @method \Sokil\Mongo\Document onBeforeUpdate(callable $handler, int $priority = 0)
42
 * @method \Sokil\Mongo\Document onAfterUpdate(callable $handler, int $priority = 0)
43
 * @method \Sokil\Mongo\Document onBeforeSave(callable $handler, int $priority = 0)
44
 * @method \Sokil\Mongo\Document onAfterSave(callable $handler, int $priority = 0)
45
 * @method \Sokil\Mongo\Document onBeforeDelete(callable $handler, int $priority = 0)
46
 * @method \Sokil\Mongo\Document onAfterDelete(callable $handler, int $priority = 0)
47
 *
48
 * @author Dmytro Sokil <[email protected]>
49
 */
50
class Document extends Structure
51
{
52
    const FIELD_TYPE_DOUBLE = 1;
53
    const FIELD_TYPE_STRING = 2;
54
    const FIELD_TYPE_OBJECT = 3;
55
    const FIELD_TYPE_ARRAY = 4;
56
    const FIELD_TYPE_BINARY_DATA = 5;
57
    const FIELD_TYPE_UNDEFINED = 6; // deprecated
58
    const FIELD_TYPE_OBJECT_ID = 7;
59
    const FIELD_TYPE_BOOLEAN = 8;
60
    const FIELD_TYPE_DATE = 9;
61
    const FIELD_TYPE_NULL = 10;
62
    const FIELD_TYPE_REGULAR_EXPRESSION = 11;
63
    const FIELD_TYPE_JAVASCRIPT = 13;
64
    const FIELD_TYPE_SYMBOL = 14;
65
    const FIELD_TYPE_JAVASCRIPT_WITH_SCOPE = 15;
66
    const FIELD_TYPE_INT32 = 16;
67
    const FIELD_TYPE_TIMESTAMP = 17;
68
    const FIELD_TYPE_INT64 = 18;
69
    const FIELD_TYPE_MIN_KEY = 255;
70
    const FIELD_TYPE_MAX_KEY = 127;
71
72
    /**
73
     *
74
     * @var \Sokil\Mongo\Document\RelationManager
75
     */
76
    private $relationManager;
77
78
    const RELATION_HAS_ONE = 'HAS_ONE';
79
    const RELATION_BELONGS = 'BELONGS';
80
    const RELATION_HAS_MANY = 'HAS_MANY';
81
    const RELATION_MANY_MANY = 'MANY_MANY';
82
83
    /**
84
     *
85
     * @var \Sokil\Mongo\Document\RevisionManager
86
     */
87
    private $revisionManager;
88
89
    /**
90
     *
91
     * @var \Sokil\Mongo\Collection
92
     */
93
    private $collection;
94
95
    /**
96
     * Name of scenario, used for validating fields
97
     * @var string
98
     */
99
    private $scenario;
100
101
    /**
102
     * @var array validator errors
103
     */
104
    private $errors = array();
105
106
    /**
107
     * @var array manually added validator errors
108
     */
109
    private $triggeredErrors = array();
110
111
    /**
112
     * @var \Symfony\Component\EventDispatcher\EventDispatcher Event Dispatcher instance
113
     */
114
    private $eventDispatcher;
115
116
    /**
117
     * @var \Sokil\Mongo\Operator Modification operator instance
118
     */
119
    private $operator;
120
121
    /**
122
     *
123
     * @var array list of defined behaviors
124
     */
125
    private $behaviors = array();
126
127
    /**
128
     *
129
     * @var array list of namespaces
130
     */
131
    private $validatorNamespaces = array(
132
        '\Sokil\Mongo\Validator',
133
    );
134
135
    /**
136
     *
137
     * @var array document options
138
     */
139
    private $options;
140
141
    /**
142
     * @param \Sokil\Mongo\Collection $collection instance of Mongo collection
143
     * @param array $data mongo document
144
     * @param array $options options of object initialization
145
     */
146
    public function __construct(Collection $collection, array $data = null, array $options = array())
147
    {
148
        $this->collection = $collection;
149
150
        // configure document with options
151
        $this->options = $options;
152
153
        // init document
154
        $this->initDocument();
155
156
        // execute before construct callable
157
        $this->beforeConstruct();
158
159
        // set data
160
        if ($data) {
161
            if ($this->getOption('stored')) {
162
                // load stored
163
                $this->replace($data);
164
            } else {
165
                // create unstored
166
                $this->merge($data);
167
            }
168
        }
169
170
        // use versioning
171
        if($this->getOption('versioning')) {
172
            $this->getRevisionManager()->listen();
173
        }
174
175
        // execure after construct event handlers
176
        $this->eventDispatcher->dispatch('afterConstruct');
177
    }
178
179
    public function getOptions()
180
    {
181
        return $this->options;
182
    }
183
184
    public function getOption($name, $default = null)
185
    {
186
        return isset($this->options[$name]) ? $this->options[$name] : $default;
187
    }
188
189
    public function hasOption($name)
190
    {
191
        return isset($this->options[$name]);
192
    }
193
194
    /**
195
     * Add own namespace of validators
196
     *
197
     * @param type $namespace
198
     * @return \Sokil\Mongo\Document
199
     */
200
    public function addValidatorNamespace($namespace)
201
    {
202
        $this->validatorNamespaces[] = rtrim($namespace, '\\');
203
        return $this;
204
    }
205
206
    /**
207
     * Event handler, called before running constructor.
208
     * May be overridden in child classes
209
     */
210
    public function beforeConstruct()
211
    {
212
213
    }
214
215
    /**
216
     * Get instance of collection
217
     * @return \Sokil\Mongo\Collection
218
     */
219
    public function getCollection()
220
    {
221
        return $this->collection;
222
    }
223
224
    /**
225
     * Reset all data passed to object in run-time, like events, behaviors,
226
     * data modifications, etc. to the state just after open or save document
227
     *
228
     * @return \Sokil\Mongo\Document
229
     */
230
    public function reset()
231
    {
232
        // reset structure
233
        parent::reset();
234
235
        // reset errors
236
        $this->errors = array();
237
        $this->triggeredErrors = array();
238
239
        // reset behaviors
240
        $this->clearBehaviors();
241
242
        // init delegates
243
        $this->initDocument();
244
245
        return $this;
246
    }
247
248
    /**
249
     * Reload data from db and reset all unsaved data
250
     */
251
    public function refresh()
252
    {
253
        $data = $this->collection
254
            ->getMongoCollection()
255
            ->findOne(array(
256
                '_id' => $this->getId()
257
            ));
258
259
        $this->replace($data);
260
261
        $this->operator->reset();
262
263
        return $this;
264
    }
265
266
    /**
267
     * Initialise relative classes
268
     */
269
    private function initDocument()
270
    {
271
        // start event dispatching
272
        $this->eventDispatcher = new EventDispatcher;
273
        
274
        // create operator
275
        $this->operator = $this->getCollection()->operator();
276
277
        // attach behaviors
278
        $this->attachBehaviors($this->behaviors());
279
        if($this->hasOption('behaviors')) {
280
            $this->attachBehaviors($this->getOption('behaviors'));
281
        }
282
    }
283
284
    public function __toString()
285
    {
286
        return (string) $this->getId();
287
    }
288
289
    public function __call($name, $arguments)
290
    {
291
        // behaviors
292
        foreach ($this->behaviors as $behavior) {
293
            if (!method_exists($behavior, $name)) {
294
                continue;
295
            }
296
297
            return call_user_func_array(array($behavior, $name), $arguments);
298
        }
299
300
        // adding event
301
        if('on' === substr($name, 0, 2)) {
302
            // prepent ebent name to function args
303
            $addListenerArguments = $arguments;
304
            array_unshift($addListenerArguments, lcfirst(substr($name, 2)));
305
            // add listener
306
            call_user_func_array(
307
                array($this->eventDispatcher, 'addListener'),
308
                $addListenerArguments
309
            );
310
            
311
            return $this;
312
        }
313
314
        // getter
315
        if ('get' === strtolower(substr($name, 0, 3))) {
316
            return $this->get(lcfirst(substr($name, 3)));
317
        }
318
319
        // setter
320
        if ('set' === strtolower(substr($name, 0, 3)) && isset($arguments[0])) {
321
            return $this->set(lcfirst(substr($name, 3)), $arguments[0]);
322
        }
323
324
        throw new Exception('Document has no method "' . $name . '"');
325
    }
326
327
    public function __get($name)
328
    {
329
        if ($this->getRelationManager()->isRelationExists($name)) {
330
            // resolve relation
331
            return $this->getRelationManager()->getRelated($name);
332
        } else {
333
            // get document parameter
334
            return parent::__get($name);
335
        }
336
    }
337
338
    /**
339
     * Set geo data as GeoJson object
340
     *
341
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
342
     * to use Point, LineString and Polygon.
343
     *
344
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
345
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
346
     *
347
     * @link http://geojson.org/
348
     * @param string $field
349
     * @param \GeoJson\Geometry\Geometry $geometry
350
     * @return \Sokil\Mongo\Document
351
     */
352
    public function setGeometry($field, Geometry $geometry)
353
    {
354
        return $this->set($field, $geometry);
355
    }
356
357
    /**
358
     * Set point as longitude and latitude
359
     *
360
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
361
     * to use Point, LineString and Polygon.
362
     *
363
     * @link http://docs.mongodb.org/manual/core/2dsphere/#point
364
     * @param string $field
365
     * @param float $longitude
366
     * @param float $latitude
367
     * @return \Sokil\Mongo\Document
368
     */
369
    public function setPoint($field, $longitude, $latitude)
370
    {
371
        return $this->setGeometry(
372
            $field,
373
            new \GeoJson\Geometry\Point(array(
374
                $longitude,
375
                $latitude
376
            ))
377
        );
378
    }
379
380
    /**
381
     * Set point as longitude and latitude in legacy format
382
     *
383
     * May be used 2d index
384
     *
385
     * @link http://docs.mongodb.org/manual/core/2d/#geospatial-indexes-store-grid-coordinates
386
     * @param string $field
387
     * @param float $longitude
388
     * @param float $latitude
389
     * @return \Sokil\Mongo\Document
390
     */
391
    public function setLegacyPoint($field, $longitude, $latitude)
392
    {
393
        return $this->set(
394
            $field,
395
            array($longitude, $latitude)
396
        );
397
    }
398
399
    /**
400
     * Set line string as array of points
401
     *
402
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
403
     * to use Point, LineString and Polygon.
404
     *
405
     * @link http://docs.mongodb.org/manual/core/2dsphere/#linestring
406
     * @param string $field
407
     * @param array $pointArray array of points
408
     * @return \Sokil\Mongo\Document
409
     */
410
    public function setLineString($field, array $pointArray)
411
    {
412
        return $this->setGeometry(
413
            $field,
414
            new \GeoJson\Geometry\LineString($pointArray)
415
        );
416
    }
417
418
    /**
419
     * Set polygon as array of line rings.
420
     *
421
     * Line ring is closed line string (first and last point same).
422
     * Line string is array of points.
423
     *
424
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
425
     * to use Point, LineString and Polygon.
426
     *
427
     * @link http://docs.mongodb.org/manual/core/2dsphere/#polygon
428
     * @param string $field
429
     * @param array $lineRingsArray array of line rings
430
     * @return \Sokil\Mongo\Document
431
     */
432
    public function setPolygon($field, array $lineRingsArray)
433
    {
434
        return $this->setGeometry(
435
            $field,
436
            new \GeoJson\Geometry\Polygon($lineRingsArray)
437
        );
438
    }
439
440
    /**
441
     * Set multi point as array of points
442
     *
443
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
444
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
445
     *
446
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipoint
447
     * @param string $field
448
     * @param array $pointArray array of point arrays
449
     * @return \Sokil\Mongo\Document
450
     */
451
    public function setMultiPoint($field, $pointArray)
452
    {
453
        return $this->setGeometry(
454
            $field,
455
            new \GeoJson\Geometry\MultiPoint($pointArray)
456
        );
457
    }
458
459
    /**
460
     * Set multi line string as array of line strings
461
     *
462
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
463
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
464
     *
465
     * http://docs.mongodb.org/manual/core/2dsphere/#multilinestring
466
     * @param string $field
467
     * @param array $lineStringArray array of line strings
468
     * @return \Sokil\Mongo\Document
469
     */
470
    public function setMultiLineString($field, $lineStringArray)
471
    {
472
        return $this->setGeometry(
473
            $field,
474
            new \GeoJson\Geometry\MultiLineString($lineStringArray)
475
        );
476
    }
477
478
    /**
479
     * Set multy polygon as array of polygons.
480
     *
481
     * Polygon is array of line rings.
482
     * Line ring is closed line string (first and last point same).
483
     * Line string is array of points.
484
     *
485
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
486
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
487
     *
488
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipolygon
489
     * @param string $field
490
     * @param array $polygonsArray array of polygons
491
     * @return \Sokil\Mongo\Document
492
     */
493
    public function setMultyPolygon($field, array $polygonsArray)
494
    {
495
        return $this->setGeometry(
496
            $field,
497
            new \GeoJson\Geometry\MultiPolygon($polygonsArray)
498
        );
499
    }
500
501
    /**
502
     * Set collection of different geometries
503
     *
504
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
505
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
506
     *
507
     * @link http://docs.mongodb.org/manual/core/2dsphere/#geometrycollection
508
     * @param string $field
509
     * @param array $geometryCollection
510
     * @return \Sokil\Mongo\Document
511
     */
512
    public function setGeometryCollection($field, array $geometryCollection)
513
    {
514
        return $this->setGeometry(
515
            $field,
516
            new \GeoJson\Geometry\GeometryCollection($geometryCollection)
517
        );
518
    }
519
520
    /**
521
     * Check if document belongs to specified collection
522
     *
523
     * @deprecated since 1.12.8 Use Collection::hasDocument()
524
     * @param \Sokil\Mongo\Collection $collection collection instance
525
     * @return boolean
526
     */
527
    public function belongsToCollection(Collection $collection)
528
    {
529
        return $collection->hasDocument($this);
530
    }
531
532
    /**
533
     * Override in child class to define relations
534
     * @return array relation description
535
     */
536
    protected function relations()
537
    {
538
        // [relationName => [relationType, targetCollection, reference], ...]
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
539
        return array();
540
    }
541
542
    /**
543
     * Relation definition through mapping is more prior to defined in class
544
     * @return array definition of relations
545
     */
546
    public function getRelationDefinition()
547
    {
548
        $relations = $this->getOption('relations');
549
        if(!is_array($relations)) {
550
            return $this->relations();
551
        }
552
553
        return $relations + $this->relations();
554
    }
555
556
    /**
557
     *
558
     * @return \Sokil\Mongo\Document\RelationManager
559
     */
560
    private function getRelationManager()
561
    {
562
        if($this->relationManager) {
563
            return $this->relationManager;
564
        }
565
566
        $this->relationManager = new RelationManager($this);
567
568
        return $this->relationManager;
569
    }
570
571
    /**
572
     * Get related documents
573
     * @param string $relationName
574
     * @return array|\Sokil\Mongo\Document related document or array of documents
575
     */
576
    public function getRelated($relationName)
577
    {
578
        return $this->getRelationManager()->getRelated($relationName);
579
    }
580
581
    public function addRelation($relationName, Document $document)
582
    {
583
        $this->getRelationManager()->addRelation($relationName, $document);
584
585
        return $this;
586
    }
587
588
    public function removeRelation($relationName, Document $document = null)
589
    {
590
        $this->getRelationManager()->removeRelation($relationName, $document);
591
592
        return $this;
593
    }
594
595
    /**
596
     * Manually trigger defined events
597
     * @param string $eventName event name
598
     * @return \Sokil\Mongo\Event
599
     */
600
    public function triggerEvent($eventName, Event $event = null)
601
    {
602
        if(!$event) {
603
            $event = new Event;
604
        }
605
606
        $event->setTarget($this);
607
608
        return $this->eventDispatcher->dispatch($eventName, $event);
609
    }
610
611
    /**
612
     * Attach event handler
613
     * @param string $event event name
614
     * @param callable|array|string $handler event handler
615
     * @return \Sokil\Mongo\Document
616
     */
617
    public function attachEvent($event, $handler, $priority = 0)
618
    {
619
        $this->eventDispatcher->addListener($event, $handler, $priority);
620
        return $this;
621
    }
622
623
    /**
624
     * Check if event attached
625
     *
626
     * @param string $event event name
627
     * @return bool
628
     */
629
    public function hasEvent($event)
630
    {
631
        return $this->eventDispatcher->hasListeners($event);
632
    }
633
634
    public function getId()
635
    {
636
        return $this->get('_id');
637
    }
638
639
    /**
640
     * Used to define id of stored document. This id must be already present in db
641
     *
642
     * @param \MongoId|string $id id of document
643
     * @return \Sokil\Mongo\Document
644
     */
645
    public function defineId($id)
646
    {
647
648
        if ($id instanceof \MongoId) {
649
            $this->_data['_id'] = $id;
650
            return $this;
651
        }
652
653
        try {
654
            $this->_data['_id'] = new \MongoId($id);
655
        } catch (\MongoException $e) {
656
            $this->_data['_id'] = $id;
657
        }
658
659
        return $this;
660
    }
661
662
    /*
663
     * Used to define id of unstored document. This db is manual
664
     */
665
666
    public function setId($id)
667
    {
668
669
        if ($id instanceof \MongoId) {
670
            return $this->set('_id', $id);
671
        }
672
673
        try {
674
            return $this->set('_id', new \MongoId($id));
675
        } catch (\MongoException $e) {
676
            return $this->set('_id', $id);
677
        }
678
    }
679
680
    public function isStored()
681
    {
682
        return $this->get('_id') && !$this->isModified('_id');
683
    }
684
685
    public function setScenario($scenario)
686
    {
687
        $this->scenario = $scenario;
688
        return $this;
689
    }
690
691
    public function getScenario()
692
    {
693
        return $this->scenario;
694
    }
695
696
    public function setNoScenario()
697
    {
698
        $this->scenario = null;
699
        return $this;
700
    }
701
702
    public function isScenario($scenario)
703
    {
704
        return $scenario === $this->scenario;
705
    }
706
707
    public function rules()
708
    {
709
        return array();
710
    }
711
712
    private function getValidatorClassNameByRuleName($ruleName)
713
    {
714
        if(false !== strpos($ruleName, '_')) {
715
            $className = implode('', array_map('ucfirst', explode('_', strtolower($ruleName))));
716
        } else {
717
            $className = ucfirst(strtolower($ruleName));
718
        }
719
720
        foreach ($this->validatorNamespaces as $namespace) {
721
            $fullyQualifiedClassName = $namespace . '\\' . $className . 'Validator';
722
            if (class_exists($fullyQualifiedClassName)) {
723
                return $fullyQualifiedClassName;
724
            }
725
        }
726
727
        throw new Exception('Validator with name ' . $ruleName . ' not found');
728
    }
729
730
    /**
731
     * check if filled model params is valid
732
     * @return boolean
733
     */
734
    public function isValid()
735
    {
736
        $this->errors = array();
737
738
        foreach ($this->rules() as $rule) {
739
            $fields = array_map('trim', explode(',', $rule[0]));
740
            $ruleName = $rule[1];
741
            $params = array_slice($rule, 2);
742
743
            // check scenario
744 View Code Duplication
            if (!empty($rule['on'])) {
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...
745
                $onScenarios = explode(',', $rule['on']);
746
                if (!in_array($this->getScenario(), $onScenarios)) {
747
                    continue;
748
                }
749
            }
750
751 View Code Duplication
            if (!empty($rule['except'])) {
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...
752
                $exceptScenarios = explode(',', $rule['except']);
753
                if (in_array($this->getScenario(), $exceptScenarios)) {
754
                    continue;
755
                }
756
            }
757
758
            if (method_exists($this, $ruleName)) {
759
                // method
760
                foreach ($fields as $field) {
761
                    $this->{$ruleName}($field, $params);
762
                }
763
            } else {
764
                // validator class
765
                $validatorClassName = $this->getValidatorClassNameByRuleName($ruleName);
766
767
                /* @var $validator \Sokil\Mongo\Validator */
768
                $validator = new $validatorClassName;
769
                if (!$validator instanceof \Sokil\Mongo\Validator) {
770
                    throw new Exception('Validator class must implement \Sokil\Mongo\Validator class');
771
                }
772
773
                $validator->validate($this, $fields, $params);
774
            }
775
        }
776
777
        return !$this->hasErrors();
778
    }
779
780
    /**
781
     *
782
     * @throws \Sokil\Mongo\Document\InvalidDocumentException
783
     * @return \Sokil\Mongo\Document
784
     */
785
    public function validate()
786
    {
787
        if($this->triggerEvent('beforeValidate')->isCancelled()) {
788
            return $this;
789
        }
790
791
        if (!$this->isValid()) {
792
            $exception = new InvalidDocumentException('Document not valid');
793
            $exception->setDocument($this);
794
795
            $this->triggerEvent('validateError');
796
797
            throw $exception;
798
        }
799
800
        $this->triggerEvent('afterValidate');
801
802
        return $this;
803
    }
804
805
    public function hasErrors()
806
    {
807
        return ($this->errors || $this->triggeredErrors);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->triggeredErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
808
    }
809
810
    /**
811
     * get list of validation errors
812
     *
813
     * Format: $errors['fieldName']['rule'] = 'message';
814
     *
815
     * @return array list of validation errors
816
     */
817
    public function getErrors()
818
    {
819
        return array_merge_recursive($this->errors, $this->triggeredErrors);
820
    }
821
822
    /**
823
     * Add validator error from validator classes and methods. This error
824
     * reset on every revalidation
825
     *
826
     * @param string $fieldName dot-notated field name
827
     * @param string $ruleName name of validation rule
828
     * @param string $message error message
829
     * @return \Sokil\Mongo\Document
830
     */
831
    public function addError($fieldName, $ruleName, $message)
832
    {
833
        $this->errors[$fieldName][$ruleName] = $message;
834
835
        // Deprecated. Related to bug when suffix not removed from class.
836
        // Added for back compatibility and will be removed in next versions
837
        $this->errors[$fieldName][$ruleName . 'validator'] = $message;
838
        
839
        return $this;
840
    }
841
842
    /**
843
     * Add errors
844
     *
845
     * @param array $errors
846
     * @return \Sokil\Mongo\Document
847
     */
848
    public function addErrors(array $errors)
849
    {
850
        $this->errors = array_merge_recursive($this->errors, $errors);
851
        return $this;
852
    }
853
854
    /**
855
     * Add custom error which not reset after validation
856
     *
857
     * @param type $fieldName
858
     * @param type $ruleName
859
     * @param type $message
860
     * @return \Sokil\Mongo\Document
861
     */
862
    public function triggerError($fieldName, $ruleName, $message)
863
    {
864
        $this->triggeredErrors[$fieldName][$ruleName] = $message;
865
        return $this;
866
    }
867
868
    /**
869
     * Add custom errors
870
     *
871
     * @param array $errors
872
     * @return \Sokil\Mongo\Document
873
     */
874
    public function triggerErrors(array $errors)
875
    {
876
        $this->triggeredErrors = array_merge_recursive($this->triggeredErrors, $errors);
877
        return $this;
878
    }
879
880
    /**
881
     * Remove custom errors
882
     *
883
     * @return \Sokil\Mongo\Document
884
     */
885
    public function clearTriggeredErrors()
886
    {
887
        $this->triggeredErrors = array();
888
        return $this;
889
    }
890
891
    public function behaviors()
892
    {
893
        return array();
894
    }
895
896
    public function attachBehaviors(array $behaviors)
897
    {
898
        foreach ($behaviors as $name => $behavior) {
899
            $this->attachBehavior($name, $behavior);
900
        }
901
902
        return $this;
903
    }
904
905
    /**
906
     *
907
     * @param string $name unique name of attached behavior
908
     * @param string|array|\Sokil\Mongo\Behavior $behavior Behavior instance or behavior definition
909
     * @return \Sokil\Mongo\Document
910
     * @throws Exception
911
     */
912
    public function attachBehavior($name, $behavior)
913
    {
914
        if(is_string($behavior)) {
915
            // behavior defined as string
916
            $className = $behavior;
917
            $behavior = new $className();
918
        } elseif(is_array($behavior)) {
919
            // behavior defined as array
920
            if (empty($behavior['class'])) {
921
                throw new Exception('Behavior class not specified');
922
            }
923
            $className = $behavior['class'];
924
            unset($behavior['class']);
925
            $behavior = new $className($behavior);
926
        } elseif (!($behavior instanceof Behavior)) {
927
            // behavior bust be Behavior instance, but something else found
928
            throw new Exception('Wrong behavior specified with name ' . $name);
929
        }
930
931
        $behavior->setOwner($this);
932
933
        $this->behaviors[$name] = $behavior;
934
935
        return $this;
936
    }
937
938
    public function clearBehaviors()
939
    {
940
        $this->behaviors = array();
941
        return $this;
942
    }
943
944
    public function getOperator()
945
    {
946
        return $this->operator;
947
    }
948
949
    public function isModificationOperatorDefined()
950
    {
951
        return $this->operator->isDefined();
952
    }
953
954
    /**
955
     * Update value in local cache and in DB
956
     *
957
     * @param string $fieldName point-delimited field name
958
     * @param mixed $value value to store
959
     * @return \Sokil\Mongo\Document
960
     */
961
    public function set($fieldName, $value)
962
    {
963
        parent::set($fieldName, $value);
964
965
        // if document saved - save through update
966
        if ($this->getId()) {
967
            $this->operator->set($fieldName, $value);
968
        }
969
970
        return $this;
971
    }
972
973
    /**
974
     * Remove field
975
     * 
976
     * @param string $fieldName field name
977
     * @return \Sokil\Mongo\Document
978
     */
979
    public function unsetField($fieldName)
980
    {
981
        if (!$this->has($fieldName)) {
982
            return $this;
983
        }
984
985
        parent::unsetField($fieldName);
986
987
        if ($this->getId()) {
988
            $this->operator->unsetField($fieldName);
989
        }
990
991
        return $this;
992
    }
993
994
    public function __unset($fieldName)
995
    {
996
        $this->unsetField($fieldName);
997
    }
998
999
    public function merge(array $data)
1000
    {
1001
        if ($this->isStored()) {
1002
            foreach ($data as $fieldName => $value) {
1003
                $this->set($fieldName, $value);
1004
            }
1005
        } else {
1006
            parent::merge($data);
1007
        }
1008
1009
        return $this;
1010
    }
1011
1012
    /**
1013
     * If field not exist - set value.
1014
     * If field exists and is not array - convert to array and append
1015
     * If field -s array - append
1016
     *
1017
     * @param type $selector
1018
     * @param type $value
1019
     * @return \Sokil\Mongo\Structure
1020
     */
1021
    public function append($selector, $value)
1022
    {
1023
        parent::append($selector, $value);
1024
1025
        // if document saved - save through update
1026
        if ($this->getId()) {
1027
            $this->operator->set($selector, $this->get($selector));
1028
        }
1029
1030
        return $this;
1031
    }
1032
1033
    /**
1034
     * Push argument as single element to field value
1035
     *
1036
     * @param string $fieldName
1037
     * @param mixed $value
1038
     * @return \Sokil\Mongo\Document
1039
      */
1040
    public function push($fieldName, $value)
1041
    {
1042
        $oldValue = $this->get($fieldName);
1043
1044
        if ($value instanceof Structure) {
1045
            $value = $value->toArray();
1046
        }
1047
1048
        // field not exists
1049
        if (!$oldValue) {
1050
            if ($this->getId()) {
1051
                $this->operator->push($fieldName, $value);
1052
            }
1053
            $value = array($value);
1054
        } // field already exist and has single value
1055 View Code Duplication
        elseif (!is_array($oldValue)) {
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...
1056
            $value = array_merge((array) $oldValue, array($value));
1057
            if ($this->getId()) {
1058
                $this->operator->set($fieldName, $value);
1059
            }
1060
        } // field exists and is array
1061
        else {
1062
            if ($this->getId()) {
1063
                // check if array because previous $set operation on single value was executed
1064
                $setValue = $this->operator->get('$set', $fieldName);
1065
                if ($setValue) {
1066
                    $setValue[] = $value;
1067
                    $this->operator->set($fieldName, $setValue);
1068
                } else {
1069
                    $this->operator->push($fieldName, $value);
1070
                }
1071
            }
1072
            $value = array_merge($oldValue, array($value));
1073
        }
1074
1075
        // update local data
1076
        parent::set($fieldName, $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of push()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1077
1078
        return $this;
1079
    }
1080
1081
    /**
1082
     * Push each element of argument's array as single element to field value
1083
     *
1084
     * @param string $fieldName
1085
     * @param array $values
1086
     * @return \Sokil\Mongo\Document
1087
     */
1088
    public function pushEach($fieldName, array $values)
1089
    {
1090
        $oldValue = $this->get($fieldName);
1091
1092
        if ($this->getId()) {
1093
            if (!$oldValue) {
1094
                $this->operator->pushEach($fieldName, $values);
1095 View Code Duplication
            } elseif (!is_array($oldValue)) {
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...
1096
                $values = array_merge((array) $oldValue, $values);
1097
                $this->operator->set($fieldName, $values);
1098
            } else {
1099
                $this->operator->pushEach($fieldName, $values);
1100
                $values = array_merge($oldValue, $values);
1101
            }
1102
        } else {
1103
            if ($oldValue) {
1104
                $values = array_merge((array) $oldValue, $values);
1105
            }
1106
        }
1107
1108
        // update local data
1109
        parent::set($fieldName, $values);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of pushEach()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1110
1111
        return $this;
1112
    }
1113
1114
    public function addToSet($fieldName, $value)
1115
    {
1116
        $oldValues = $this->get($fieldName);
1117
        if (!$oldValues) {
1118
            $newValue = [$value];
1119
            $this->operator->addToSet($fieldName, $value);
1120
        } elseif (!is_array($value)) {
1121
            if ($oldValues === $value) {
1122
                return $this;
1123
            }
1124
            $newValue = array($oldValues, $value);
1125
            $this->operator->set($fieldName, $newValue);
1126
        } else {
1127
            if (in_array($value, $oldValues)) {
1128
                return $this;
1129
            }
1130
            $newValue = array_merge($oldValues, array($value));
1131
            $this->operator->addToSet($fieldName, $value);
1132
        }
1133
1134
        parent::set($fieldName, $newValue);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of addToSet()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1135
1136
        return $this;
1137
    }
1138
1139
    public function addToSetEach($fieldName, array $values)
0 ignored issues
show
Unused Code introduced by
The parameter $fieldName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $values is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1140
    {
1141
1142
    }
1143
1144
    /**
1145
     * Removes from an existing array all instances of a value or
1146
     * values that match a specified query
1147
     *
1148
     * @param integer|string|array|\Sokil\Mongo\Expression|callable $expression
1149
     * @param mixed|\Sokil\Mongo\Expression|callable $value
1150
     * @return \Sokil\Mongo\Document
1151
     */
1152
    public function pull($expression, $value = null)
1153
    {
1154
        $this->operator->pull($expression, $value);
1155
        return $this;
1156
    }
1157
1158
    public function increment($fieldName, $value = 1)
1159
    {
1160
        parent::set($fieldName, (int) $this->get($fieldName) + $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of increment()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1161
1162
        if ($this->getId()) {
1163
            $this->operator->increment($fieldName, $value);
1164
        }
1165
1166
1167
        return $this;
1168
    }
1169
1170
    public function decrement($fieldName, $value = 1)
1171
    {
1172
        return $this->increment($fieldName, -1 * $value);
1173
    }
1174
1175 View Code Duplication
    public function bitwiceAnd($field, $value)
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...
1176
    {
1177
        parent::set($field, (int) $this->get($field) & $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceAnd()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1178
1179
        if ($this->getId()) {
1180
            $this->operator->bitwiceAnd($field, $value);
1181
        }
1182
1183
        return $this;
1184
    }
1185
1186 View Code Duplication
    public function bitwiceOr($field, $value)
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...
1187
    {
1188
        parent::set($field, (int) $this->get($field) | $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceOr()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1189
1190
        if ($this->getId()) {
1191
            $this->operator->bitwiceOr($field, $value);
1192
        }
1193
1194
        return $this;
1195
    }
1196
1197
    public function bitwiceXor($field, $value)
1198
    {
1199
        $oldFieldValue = (int) $this->get($field);
1200
        $newValue = $oldFieldValue ^ $value;
1201
1202
        parent::set($field, $newValue);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceXor()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1203
1204
        if ($this->getId()) {
1205
            if(version_compare($this->getCollection()->getDatabase()->getClient()->getDbVersion(), '2.6', '>=')) {
1206
                $this->operator->bitwiceXor($field, $value);
1207
            } else {
1208
                $this->operator->set($field, $newValue);
1209
            }
1210
        }
1211
1212
        return $this;
1213
    }
1214
1215
    public function save($validate = true)
1216
    {
1217
        // save document
1218
        // if document already in db and not modified - skip this method
1219
        if (!$this->isSaveRequired()) {
1220
            return $this;
1221
        }
1222
1223
        if ($validate) {
1224
            $this->validate();
1225
        }
1226
1227
        // handle beforeSave event
1228
        if($this->triggerEvent('beforeSave')->isCancelled()) {
1229
            return $this;
1230
        }
1231
        if ($this->isStored()) {
1232
            if($this->triggerEvent('beforeUpdate')->isCancelled()) {
1233
                return $this;
1234
            }
1235
1236
            // locking
1237
            $query = array('_id' => $this->getId());
1238
            if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1239
                $query['__version__'] = $this->get('__version__');
1240
                $this->getOperator()->increment('__version__');
1241
            }
1242
1243
            // update
1244
            $status = $this->collection
1245
                ->getMongoCollection()
1246
                ->update(
1247
                    $query,
1248
                    $this->getOperator()->toArray()
1249
                );
1250
1251
            // check update status
1252 View Code Duplication
            if ($status['ok'] != 1) {
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...
1253
                throw new \Sokil\Mongo\Exception(sprintf(
1254
                    'Update error: %s: %s',
1255
                    $status['err'],
1256
                    $status['errmsg']
1257
                ));
1258
            }
1259
1260
            // check if document modified due to specified lock
1261
            if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1262
                if($status['n'] === 0) {
1263
                    throw new OptimisticLockFailureException;
1264
                }
1265
            }
1266
1267
            if ($this->getOperator()->isReloadRequired()) {
1268
                $this->refresh();
1269
            } else {
1270
                $this->getOperator()->reset();
1271
            }
1272
1273
            $this->triggerEvent('afterUpdate');
1274
        } else {
1275
            if($this->triggerEvent('beforeInsert')->isCancelled()) {
1276
                return $this;
1277
            }
1278
1279
            $document = $this->toArray();
1280
1281
            $this
1282
                ->collection
1283
                ->getMongoCollection()
1284
                ->insert($document);
1285
            
1286
            // set id
1287
            $this->defineId($document['_id']);
1288
1289
            // after insert event
1290
            $this->triggerEvent('afterInsert');
1291
        }
1292
1293
        // handle afterSave event
1294
        $this->triggerEvent('afterSave');
1295
1296
        $this->apply();
1297
        
1298
        return $this;
1299
    }
1300
1301
    public function isSaveRequired()
1302
    {
1303
        return !$this->isStored() || $this->isModified() || $this->isModificationOperatorDefined();
1304
    }
1305
1306
    public function delete()
1307
    {
1308
        if ($this->triggerEvent('beforeDelete')->isCancelled()) {
1309
            return $this;
1310
        }
1311
1312
        $status = $this->collection->getMongoCollection()->remove(array(
1313
            '_id'   => $this->getId(),
1314
        ));
1315
1316 View Code Duplication
        if(true !== $status && $status['ok'] != 1) {
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...
1317
            throw new \Sokil\Mongo\Exception(sprintf('Delete document error: %s', $status['err']));
1318
        }
1319
1320
        $this->triggerEvent('afterDelete');
1321
1322
        // drop from document's pool
1323
        $this->getCollection()->removeDocumentFromDocumentPool($this);
1324
1325
        return $this;
1326
    }
1327
1328
    /**
1329
     *
1330
     * @return \Sokil\Mongo\RevisionManager
1331
     */
1332
    public function getRevisionManager()
1333
    {
1334
        if(!$this->revisionManager) {
1335
            $this->revisionManager = new RevisionManager($this);
1336
        }
1337
1338
        return $this->revisionManager;
1339
    }
1340
1341
    /**
1342
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisions()
1343
     */
1344
    public function getRevisions($limit = null, $offset = null)
1345
    {
1346
        return $this->getRevisionManager()->getRevisions($limit, $offset);
1347
    }
1348
1349
    /**
1350
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevision()
1351
     */
1352
    public function getRevision($id)
1353
    {
1354
        return $this->getRevisionManager()->getRevision($id);
1355
    }
1356
1357
    /**
1358
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisionsCount()
1359
     */
1360
    public function getRevisionsCount()
1361
    {
1362
        return $this->getRevisionManager()->getRevisionsCount();
1363
    }
1364
1365
    /**
1366
     * @deprecated since 1.13.0 use self::getRevisionManager()->clearRevisions()
1367
     */
1368
    public function clearRevisions()
1369
    {
1370
        $this->getRevisionManager()->clearRevisions();
1371
        return $this;
1372
    }
1373
1374
}
1375