Completed
Pull Request — 1.3.x (#71)
by Grégoire
13:22 queued 11:22
created

PersistentObject   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 30
eloc 52
dl 0
loc 184
rs 10
c 0
b 0
f 0
ccs 63
cts 63
cp 1

9 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 7 3
A setObjectManager() 0 3 1
B set() 0 13 7
A injectObjectManager() 0 8 2
A __call() 0 14 4
A completeOwningSide() 0 13 3
A getObjectManager() 0 3 1
A add() 0 15 6
A initializeDoctrine() 0 11 3
1
<?php
2
3
namespace Doctrine\Persistence;
4
5
use BadMethodCallException;
6
use Doctrine\Common\Collections\ArrayCollection;
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\Persistence\Mapping\ClassMetadata;
9
use InvalidArgumentException;
10
use RuntimeException;
11
use function lcfirst;
12
use function substr;
13
use Doctrine\Persistence\ObjectManager;
0 ignored issues
show
Coding Style introduced by
Use statements should be sorted alphabetically. The first wrong one is Doctrine\Persistence\ObjectManager.
Loading history...
introduced by
Use Doctrine\Persistence\ObjectManager is from the same namespace – that is prohibited.
Loading history...
14
use Doctrine\Persistence\ObjectManagerAware;
0 ignored issues
show
introduced by
Use Doctrine\Persistence\ObjectManagerAware is from the same namespace – that is prohibited.
Loading history...
15
16
/**
17
 * PersistentObject base class that implements getter/setter methods for all mapped fields and associations
18
 * by overriding __call.
19
 *
20
 * This class is a forward compatible implementation of the PersistentObject trait.
21
 *
22
 * Limitations:
23
 *
24
 * 1. All persistent objects have to be associated with a single ObjectManager, multiple
25
 *    ObjectManagers are not supported. You can set the ObjectManager with `PersistentObject#setObjectManager()`.
26
 * 2. Setters and getters only work if a ClassMetadata instance was injected into the PersistentObject.
27
 *    This is either done on `postLoad` of an object or by accessing the global object manager.
28
 * 3. There are no hooks for setters/getters. Just implement the method yourself instead of relying on __call().
29
 * 4. Slower than handcoded implementations: An average of 7 method calls per access to a field and 11 for an association.
30
 * 5. Only the inverse side associations get autoset on the owning side as well. Setting objects on the owning side
31
 *    will not set the inverse side associations.
32
 *
33
 * @deprecated Deprecated `PersistentObject` class in 1.2. Please implement this functionality
34
 *             directly in your application if you want ActiveRecord style functionality.
35
 *
36
 * @example
37
 *
38
 *  PersistentObject::setObjectManager($em);
39
 *
40
 *  class Foo extends PersistentObject
41
 *  {
42
 *      private $id;
43
 *  }
44
 *
45
 *  $foo = new Foo();
46
 *  $foo->getId(); // method exists through __call
47
 */
48
abstract class PersistentObject implements ObjectManagerAware
49
{
50
    /** @var ObjectManager|null */
51
    private static $objectManager = null;
52
53
    /** @var ClassMetadata|null */
54
    private $cm = null;
55
56
    /**
57
     * Sets the object manager responsible for all persistent object base classes.
58
     *
59
     * @return void
60
     */
61 18
    public static function setObjectManager(?ObjectManager $objectManager = null)
62
    {
63 18
        self::$objectManager = $objectManager;
64 18
    }
65
66
    /**
67
     * @return ObjectManager|null
68
     */
69 1
    public static function getObjectManager()
70
    {
71 1
        return self::$objectManager;
72
    }
73
74
    /**
75
     * Injects the Doctrine Object Manager.
76
     *
77
     * @return void
78
     *
79
     * @throws RuntimeException
80
     */
81 18
    public function injectObjectManager(ObjectManager $objectManager, ClassMetadata $classMetadata)
82
    {
83 18
        if ($objectManager !== self::$objectManager) {
84 1
            throw new RuntimeException('Trying to use PersistentObject with different ObjectManager instances. ' .
85 1
                'Was PersistentObject::setObjectManager() called?');
86
        }
87
88 18
        $this->cm = $classMetadata;
89 18
    }
90
91
    /**
92
     * Sets a persistent fields value.
93
     *
94
     * @param string  $field
95
     * @param mixed[] $args
96
     *
97
     * @return void
98
     *
99
     * @throws BadMethodCallException   When no persistent field exists by that name.
100
     * @throws InvalidArgumentException When the wrong target object type is passed to an association.
101
     */
102 7
    private function set($field, $args)
103
    {
104 7
        if ($this->cm->hasField($field) && ! $this->cm->isIdentifier($field)) {
0 ignored issues
show
Bug introduced by
The method hasField() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

104
        if ($this->cm->/** @scrutinizer ignore-call */ hasField($field) && ! $this->cm->isIdentifier($field)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
105 1
            $this->$field = $args[0];
106 6
        } elseif ($this->cm->hasAssociation($field) && $this->cm->isSingleValuedAssociation($field)) {
107 4
            $targetClass = $this->cm->getAssociationTargetClass($field);
108 4
            if (! ($args[0] instanceof $targetClass) && $args[0] !== null) {
109 1
                throw new InvalidArgumentException("Expected persistent object of type '" . $targetClass . "'");
110
            }
111 3
            $this->$field = $args[0];
112 3
            $this->completeOwningSide($field, $targetClass, $args[0]);
0 ignored issues
show
Bug introduced by
$targetClass of type string is incompatible with the type Doctrine\Persistence\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\Persistence\Per...t::completeOwningSide(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
            $this->completeOwningSide($field, /** @scrutinizer ignore-type */ $targetClass, $args[0]);
Loading history...
113
        } else {
114 2
            throw new BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->getName() . "'");
115
        }
116 4
    }
117
118
    /**
119
     * Gets a persistent field value.
120
     *
121
     * @param string $field
122
     *
123
     * @return mixed
124
     *
125
     * @throws BadMethodCallException When no persistent field exists by that name.
126
     */
127 8
    private function get($field)
128
    {
129 8
        if ($this->cm->hasField($field) || $this->cm->hasAssociation($field)) {
130 7
            return $this->$field;
131
        }
132
133 1
        throw new BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->getName() . "'");
134
    }
135
136
    /**
137
     * If this is an inverse side association, completes the owning side.
138
     *
139
     * @param string        $field
140
     * @param ClassMetadata $targetClass
141
     * @param object        $targetObject
142
     *
143
     * @return void
144
     */
145 3
    private function completeOwningSide($field, $targetClass, $targetObject)
146
    {
147
        // add this object on the owning side as well, for obvious infinite recursion
148
        // reasons this is only done when called on the inverse side.
149 3
        if (! $this->cm->isAssociationInverseSide($field)) {
150 3
            return;
151
        }
152
153 1
        $mappedByField  = $this->cm->getAssociationMappedByTargetField($field);
154 1
        $targetMetadata = self::$objectManager->getClassMetadata($targetClass);
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Persistence\Mapping\ClassMetadata is incompatible with the type string expected by parameter $className of Doctrine\Persistence\Obj...ger::getClassMetadata(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
        $targetMetadata = self::$objectManager->getClassMetadata(/** @scrutinizer ignore-type */ $targetClass);
Loading history...
Bug introduced by
The method getClassMetadata() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

154
        /** @scrutinizer ignore-call */ 
155
        $targetMetadata = self::$objectManager->getClassMetadata($targetClass);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
155
156 1
        $setter = ($targetMetadata->isCollectionValuedAssociation($mappedByField) ? 'add' : 'set') . $mappedByField;
157 1
        $targetObject->$setter($this);
158 1
    }
159
160
    /**
161
     * Adds an object to a collection.
162
     *
163
     * @param string  $field
164
     * @param mixed[] $args
165
     *
166
     * @return void
167
     *
168
     * @throws BadMethodCallException
169
     * @throws InvalidArgumentException
170
     */
171 3
    private function add($field, $args)
172
    {
173 3
        if (! $this->cm->hasAssociation($field) || ! $this->cm->isCollectionValuedAssociation($field)) {
174 1
            throw new BadMethodCallException('There is no method add' . $field . '() on ' . $this->cm->getName());
175
        }
176
177 2
        $targetClass = $this->cm->getAssociationTargetClass($field);
178 2
        if (! ($args[0] instanceof $targetClass)) {
179 1
            throw new InvalidArgumentException("Expected persistent object of type '" . $targetClass . "'");
180
        }
181 1
        if (! ($this->$field instanceof Collection)) {
182 1
            $this->$field = new ArrayCollection($this->$field ?: []);
183
        }
184 1
        $this->$field->add($args[0]);
185 1
        $this->completeOwningSide($field, $targetClass, $args[0]);
0 ignored issues
show
Bug introduced by
$targetClass of type string is incompatible with the type Doctrine\Persistence\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\Persistence\Per...t::completeOwningSide(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

185
        $this->completeOwningSide($field, /** @scrutinizer ignore-type */ $targetClass, $args[0]);
Loading history...
186 1
    }
187
188
    /**
189
     * Initializes Doctrine Metadata for this class.
190
     *
191
     * @return void
192
     *
193
     * @throws RuntimeException
194
     */
195 16
    private function initializeDoctrine()
196
    {
197 16
        if ($this->cm !== null) {
198 14
            return;
199
        }
200
201 3
        if (! self::$objectManager) {
202 1
            throw new RuntimeException('No runtime object manager set. Call PersistentObject#setObjectManager().');
203
        }
204
205 2
        $this->cm = self::$objectManager->getClassMetadata(static::class);
206 2
    }
207
208
    /**
209
     * Magic methods.
210
     *
211
     * @param string  $method
212
     * @param mixed[] $args
213
     *
214
     * @return mixed
215
     *
216
     * @throws BadMethodCallException
217
     */
218 16
    public function __call($method, $args)
219
    {
220 16
        $this->initializeDoctrine();
221
222 15
        $command = substr($method, 0, 3);
223 15
        $field   = lcfirst(substr($method, 3));
224 15
        if ($command === 'set') {
225 7
            $this->set($field, $args);
226 12
        } elseif ($command === 'get') {
227 8
            return $this->get($field);
228 5
        } elseif ($command === 'add') {
229 3
            $this->add($field, $args);
230
        } else {
231 2
            throw new BadMethodCallException('There is no method ' . $method . ' on ' . $this->cm->getName());
232
        }
233 4
    }
234
}
235