Passed
Pull Request — master (#7065)
by Michael
12:50
created

PersistentObject::set()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.0957

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 5
nop 2
dl 0
loc 28
ccs 14
cts 16
cp 0.875
crap 7.0957
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\ORM\Mapping\AssociationMetadata;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Doctrine\ORM\Mapping\FieldMetadata;
12
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
13
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
14
use function get_class;
15
use function lcfirst;
16
use function substr;
17
18
/**
19
 * PersistentObject base class that implements getter/setter methods for all mapped fields and associations
20
 * by overriding __call.
21
 *
22
 * This class is a forward compatible implementation of the PersistentObject trait.
23
 *
24
 * Limitations:
25
 *
26
 * 1. All persistent objects have to be associated with a single EntityManager, multiple
27
 *    EntityManagers are not supported. You can set the EntityManager with `PersistentObject#setEntityManager()`.
28
 * 2. Setters and getters only work if a ClassMetadata instance was injected into the PersistentObject.
29
 *    This is either done on `postLoad` of an object or by accessing the global object manager.
30
 * 3. There are no hooks for setters/getters. Just implement the method yourself instead of relying on __call().
31
 * 4. Slower than handcoded implementations: An average of 7 method calls per access to a field and 11 for an association.
32
 * 5. Only the inverse side associations get autoset on the owning side as well. Setting objects on the owning side
33
 *    will not set the inverse side associations.
34
 *
35
 * @example
36
 *
37
 *  PersistentObject::setEntityManager($em);
38
 *
39
 *  class Foo extends PersistentObject
40
 *  {
41
 *      private $id;
42
 *  }
43
 *
44
 *  $foo = new Foo();
45
 *  $foo->getId(); // method exists through __call
46
 */
47
abstract class PersistentObject implements EntityManagerAware
48
{
49
    /**
50
     * @var EntityManagerInterface|null
51
     */
52
    private static $entityManager = null;
53
54
    /**
55
     * @var ClassMetadata|null
56
     */
57
    private $cm;
58
59
    /**
60
     * Sets the entity manager responsible for all persistent object base classes.
61
     */
62 7
    public static function setEntityManager(?EntityManagerInterface $entityManager = null)
63
    {
64 7
        self::$entityManager = $entityManager;
65 7
    }
66
67
    /**
68
     * @return EntityManagerInterface|null
69
     */
70
    public static function getEntityManager()
71
    {
72
        return self::$entityManager;
73
    }
74
75
    /**
76
     * Injects the Doctrine Object Manager.
77
     *
78
     * @throws \RuntimeException
79
     */
80 6
    public function injectEntityManager(EntityManagerInterface $entityManager, ClassMetadata $classMetadata) : void
81
    {
82 6
        if ($entityManager !== self::$entityManager) {
83
            throw new \RuntimeException(
84
                'Trying to use PersistentObject with different EntityManager instances. ' .
85
                'Was PersistentObject::setEntityManager() called?'
86
            );
87
        }
88
89 6
        $this->cm = $classMetadata;
90 6
    }
91
92
    /**
93
     * Sets a persistent fields value.
94
     *
95
     * @param string  $field
96
     * @param mixed[] $args
97
     *
98
     * @return object
99
     *
100
     * @throws \BadMethodCallException   When no persistent field exists by that name.
101
     * @throws \InvalidArgumentException When the wrong target object type is passed to an association.
102
     */
103 4
    private function set($field, $args)
104
    {
105 4
        $this->initializeDoctrine();
106
107 4
        $property = $this->cm->getProperty($field);
0 ignored issues
show
Bug introduced by
The method getProperty() 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

107
        /** @scrutinizer ignore-call */ 
108
        $property = $this->cm->getProperty($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...
Bug introduced by
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

107
        /** @scrutinizer ignore-call */ 
108
        $property = $this->cm->getProperty($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...
108
109 4
        if (! $property) {
110
            throw new \BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->getClassName() . "'");
0 ignored issues
show
Bug introduced by
The method getClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

110
            throw new \BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->/** @scrutinizer ignore-call */ getClassName() . "'");

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...
111
        }
112
113
        switch (true) {
114 4
            case ($property instanceof FieldMetadata && ! $property->isPrimaryKey()):
115 4
                $this->{$field} = $args[0];
116 4
                break;
117
118 1
            case ($property instanceof ToOneAssociationMetadata):
119 1
                $targetClassName = $property->getTargetEntity();
120
121 1
                if ($args[0] !== null && ! ($args[0] instanceof $targetClassName)) {
122
                    throw new \InvalidArgumentException("Expected persistent object of type '" . $targetClassName . "'");
123
                }
124
125 1
                $this->{$field} = $args[0];
126 1
                $this->completeOwningSide($property, $args[0]);
127 1
                break;
128
        }
129
130 4
        return $this;
131
    }
132
133
    /**
134
     * Gets a persistent field value.
135
     *
136
     * @param string $field
137
     *
138
     * @return mixed
139
     *
140
     * @throws \BadMethodCallException When no persistent field exists by that name.
141
     */
142 6
    private function get($field)
143
    {
144 6
        $this->initializeDoctrine();
145
146 6
        $property = $this->cm->getProperty($field);
147
148 6
        if (! $property) {
149
            throw new \BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->getClassName() . "'");
150
        }
151
152 6
        return $this->{$field};
153
    }
154
155
    /**
156
     * If this is an inverse side association, completes the owning side.
157
     *
158
     * @param object $targetObject
159
     */
160 1
    private function completeOwningSide(AssociationMetadata $property, $targetObject)
161
    {
162
        // add this object on the owning side as well, for obvious infinite recursion
163
        // reasons this is only done when called on the inverse side.
164 1
        if ($property->isOwningSide()) {
165 1
            return;
166
        }
167
168
        $mappedByField    = $property->getMappedBy();
169
        $targetMetadata   = self::$entityManager->getClassMetadata($property->getTargetEntity());
0 ignored issues
show
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

169
        /** @scrutinizer ignore-call */ 
170
        $targetMetadata   = self::$entityManager->getClassMetadata($property->getTargetEntity());

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...
170
        $targetProperty   = $targetMetadata->getProperty($mappedByField);
171
        $setterMethodName = ($targetProperty instanceof ToManyAssociationMetadata ? 'add' : 'set') . $mappedByField;
172
173
        $targetObject->{$setterMethodName}($this);
174
    }
175
176
    /**
177
     * Adds an object to a collection.
178
     *
179
     * @param string  $field
180
     * @param mixed[] $args
181
     *
182
     * @return object
183
     *
184
     * @throws \BadMethodCallException
185
     * @throws \InvalidArgumentException
186
     */
187
    private function add($field, $args)
188
    {
189
        $this->initializeDoctrine();
190
191
        $property = $this->cm->getProperty($field);
192
193
        if (! $property) {
194
            throw new \BadMethodCallException("no field with name '" . $field . "' exists on '" . $this->cm->getClassName() . "'");
195
        }
196
197
        if (! ($property instanceof ToManyAssociationMetadata)) {
198
            throw new \BadMethodCallException('There is no method add' . $field . '() on ' . $this->cm->getClassName());
199
        }
200
201
        $targetClassName = $property->getTargetEntity();
202
203
        if (! ($args[0] instanceof $targetClassName)) {
204
            throw new \InvalidArgumentException("Expected persistent object of type '" . $targetClassName . "'");
205
        }
206
207
        if (! ($this->{$field} instanceof Collection)) {
208
            $this->{$field} = new ArrayCollection($this->{$field} ?: []);
209
        }
210
211
        $this->{$field}->add($args[0]);
212
213
        $this->completeOwningSide($property, $args[0]);
214
215
        return $this;
216
    }
217
218
    /**
219
     * Initializes Doctrine Metadata for this class.
220
     *
221
     * @throws \RuntimeException
222
     */
223 7
    private function initializeDoctrine()
224
    {
225 7
        if ($this->cm !== null) {
226 4
            return;
227
        }
228
229 7
        if (! self::$entityManager) {
230
            throw new \RuntimeException('No runtime entity manager set. Call PersistentObject#setEntityManager().');
231
        }
232
233 7
        $this->cm = self::$entityManager->getClassMetadata(get_class($this));
0 ignored issues
show
Documentation Bug introduced by
It seems like self::entityManager->get...adata(get_class($this)) of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the declared type null|Doctrine\ORM\Mapping\ClassMetadata of property $cm.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
234 7
    }
235
236
    /**
237
     * Magic methods.
238
     *
239
     * @param string  $method
240
     * @param mixed[] $args
241
     *
242
     * @return mixed
243
     *
244
     * @throws \BadMethodCallException
245
     */
246 7
    public function __call($method, $args)
247
    {
248 7
        $command = substr($method, 0, 3);
249 7
        $field   = lcfirst(substr($method, 3));
250
251 7
        switch ($command) {
252 7
            case 'set':
253 4
                return $this->set($field, $args);
254
255 6
            case 'get':
256 6
                return $this->get($field);
257
258
            case 'add':
259
                return $this->add($field, $args);
260
261
            default:
262
                throw new \BadMethodCallException('There is no method ' . $method . ' on ' . $this->cm->getClassName());
263
        }
264
    }
265
}
266