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

102
        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...
103 1
            $this->$field = $args[0];
104 6
        } elseif ($this->cm->hasAssociation($field) && $this->cm->isSingleValuedAssociation($field)) {
105 4
            $targetClass = $this->cm->getAssociationTargetClass($field);
106 4
            if (! ($args[0] instanceof $targetClass) && $args[0] !== null) {
107 1
                throw new InvalidArgumentException("Expected persistent object of type '" . $targetClass . "'");
108
            }
109 3
            $this->$field = $args[0];
110 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

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

152
        $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

152
        /** @scrutinizer ignore-call */ 
153
        $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...
153
154 1
        $setter = ($targetMetadata->isCollectionValuedAssociation($mappedByField) ? 'add' : 'set') . $mappedByField;
155 1
        $targetObject->$setter($this);
156 1
    }
157
158
    /**
159
     * Adds an object to a collection.
160
     *
161
     * @param string  $field
162
     * @param mixed[] $args
163
     *
164
     * @return void
165
     *
166
     * @throws BadMethodCallException
167
     * @throws InvalidArgumentException
168
     */
169 3
    private function add($field, $args)
170
    {
171 3
        if (! $this->cm->hasAssociation($field) || ! $this->cm->isCollectionValuedAssociation($field)) {
172 1
            throw new BadMethodCallException('There is no method add' . $field . '() on ' . $this->cm->getName());
173
        }
174
175 2
        $targetClass = $this->cm->getAssociationTargetClass($field);
176 2
        if (! ($args[0] instanceof $targetClass)) {
177 1
            throw new InvalidArgumentException("Expected persistent object of type '" . $targetClass . "'");
178
        }
179 1
        if (! ($this->$field instanceof Collection)) {
180 1
            $this->$field = new ArrayCollection($this->$field ?: []);
181
        }
182 1
        $this->$field->add($args[0]);
183 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

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