Completed
Pull Request — master (#1241)
by Marco
12:49
created

ProxyFactory::resetUninitializedProxy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0116
Metric Value
dl 0
loc 13
ccs 6
cts 7
cp 0.8571
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2.0116
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Proxy;
21
22
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
23
use Doctrine\Common\Proxy\AbstractProxyFactory;
24
use Doctrine\Common\Proxy\Exception\InvalidArgumentException;
25
use Doctrine\Common\Proxy\Exception\OutOfBoundsException;
26
use Doctrine\Common\Proxy\ProxyDefinition;
27
use Doctrine\Common\Proxy\ProxyGenerator;
28
use Doctrine\Common\Util\ClassUtils;
29
use Doctrine\ORM\EntityManagerInterface;
30
use Doctrine\ORM\Persisters\Entity\EntityPersister;
31
use Doctrine\ORM\EntityNotFoundException;
32
use Doctrine\ORM\Utility\IdentifierFlattener;
33
use ProxyManager\Generator\ClassGenerator;
34
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
35
use ProxyManager\Proxy\GhostObjectInterface;
36
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
37
use ProxyManager\ProxyGenerator\Util\Properties;
38
use ReflectionClass;
39
use ReflectionProperty;
40
41
/**
42
 * This factory is used to create proxy objects for entities at runtime.
43
 *
44
 * @author Roman Borschel <[email protected]>
45
 * @author Giorgio Sironi <[email protected]>
46
 * @author Marco Pivetta  <[email protected]>
47
 * @since 2.0
48
 */
49
class ProxyFactory extends AbstractProxyFactory
50
{
51
    /**
52
     * @var EntityManagerInterface The EntityManager this factory is bound to.
53
     */
54
    private $em;
55
56
    /**
57
     * @var \Doctrine\ORM\UnitOfWork The UnitOfWork this factory uses to retrieve persisters
58
     */
59
    private $uow;
60
61
    /**
62
     * @var string
63
     */
64
    private $proxyNs;
65
66
    /**
67
     * The IdentifierFlattener used for manipulating identifiers
68
     *
69
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
70
     */
71
    private $identifierFlattener;
72
73
    /**
74
     * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
75
     * connected to the given <tt>EntityManager</tt>.
76
     *
77
     * @param EntityManagerInterface $em           The EntityManager the new factory works for.
78
     * @param string                 $proxyDir     The directory to use for the proxy classes. It must exist.
79
     * @param string                 $proxyNs      The namespace to use for the proxy classes.
80
     * @param boolean|int            $autoGenerate The strategy for automatically generating proxy classes. Possible
81
     *                                             values are constants of Doctrine\Common\Proxy\AbstractProxyFactory.
82
     */
83 2314
    public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = AbstractProxyFactory::AUTOGENERATE_NEVER)
84
    {
85 2314
        $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
86
87 2314
        $proxyGenerator->setPlaceholder('baseProxyInterface', Proxy::class);
88 2314
        parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
89
90 2314
        $this->em                  = $em;
91 2314
        $this->uow                 = $em->getUnitOfWork();
92 2314
        $this->proxyNs             = $proxyNs;
93 2314
        $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
94 2314
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * @return GhostObjectInterface
100
     */
101 219
    public function getProxy($className, array $identifier)
102
    {
103 219
        $definition = $this->createProxyDefinition($className);
104 219
        $fqcn       = $definition->proxyClassName;
105
106 219
        if (! class_exists($fqcn, false)) {
107 88
            $generatorStrategy    = new EvaluatingGeneratorStrategy();
108 88
            $proxyGenerator       = new ClassGenerator();
109 88
            $skippedProperties    = array_filter(
110 88
                Properties::fromReflectionClass(new ReflectionClass($className))->getInstanceProperties(),
111
                function (ReflectionProperty $property) use ($definition) {
112 88
                    return ! in_array(
113 88
                            $property->getName(),
114
                            array_map(
115
                                function (ReflectionProperty $property) {
116 88
                                    return $property->getName();
117 88
                                },
118 88
                                $definition->reflectionFields
119
                            )
120
                        )
121 88
                        || in_array($property->getName(), $definition->identifierFields);
122 88
                }
123
            );
124
125 88
            $proxyGenerator->setName($fqcn);
126
127 88
            (new LazyLoadingGhostGenerator())->generate(
128 88
                $this->em->getClassMetadata($className)->getReflectionClass(),
129
                $proxyGenerator,
130
                [
131 88
                    'skippedProperties' => array_map([$this, 'getInternalReflectionPropertyName'], $skippedProperties)
132
                ]
133
            );
134
135 88
            $generatorStrategy->generate($proxyGenerator);
136
        }
137
138 219
        $proxy = $fqcn::staticProxyConstructor($definition->initializer/*, $definition->cloner*/);
139
140 219
        foreach ($definition->identifierFields as $idField) {
141 219
            if (! isset($identifier[$idField])) {
142
                throw OutOfBoundsException::missingPrimaryKeyValue($className, $idField);
143
            }
144
145 219
            $definition->reflectionFields[$idField]->setValue($proxy, $identifier[$idField]);
146
        }
147
148 219
        return $proxy;
149
    }
150
151
    /**
152
     * Reset initialization/cloning logic for an un-initialized proxy
153
     *
154
     * @param GhostObjectInterface $proxy
155
     *
156
     * @return GhostObjectInterface
157
     *
158
     * @throws \Doctrine\Common\Proxy\Exception\InvalidArgumentException
159
     */
160 6
    public function resetUninitializedProxy(GhostObjectInterface $proxy)
161
    {
162 6
        if ($proxy->isProxyInitialized()) {
163
            throw InvalidArgumentException::unitializedProxyExpected($proxy);
0 ignored issues
show
Documentation introduced by
$proxy is of type object<ProxyManager\Proxy\GhostObjectInterface>, but the function expects a object<Doctrine\Common\Persistence\Proxy>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
164
        }
165
166 6
        $className  = ClassUtils::getClass($proxy);
167 6
        $definition = $this->createProxyDefinition($className);
168
169 6
        $proxy->setProxyInitializer($definition->initializer);
0 ignored issues
show
Documentation introduced by
$definition->initializer is of type callable, but the function expects a null|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
170
171 6
        return $proxy;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $proxy; (ProxyManager\Proxy\GhostObjectInterface) is incompatible with the return type of the parent method Doctrine\Common\Proxy\Ab...resetUninitializedProxy of type Doctrine\Common\Proxy\Proxy.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177 1
    protected function skipClass(ClassMetadata $metadata)
178
    {
179
        /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadataInfo */
180 1
        return $metadata->isMappedSuperclass || $metadata->getReflectionClass()->isAbstract();
181
    }
182
183
    /**
184
     * {@inheritDoc}
185
     */
186 219
    protected function createProxyDefinition($className)
187
    {
188 219
        $classMetadata   = $this->em->getClassMetadata($className);
189 219
        $entityPersister = $this->uow->getEntityPersister($className);
190 219
        $cloner          = $this->createCloner($classMetadata, $entityPersister);
191
192 219
        return new ProxyDefinition(
193 219
            ClassUtils::generateProxyClassName($className, $this->proxyNs),
194 219
            $classMetadata->getIdentifierFieldNames(),
195 219
            $classMetadata->getReflectionProperties(),
196 219
            $this->createInitializer($classMetadata, $entityPersister, $cloner),
197 219
            $this->createCloner($classMetadata, $entityPersister)
198
        );
199
    }
200
201
    /**
202
     * Creates a closure capable of initializing a proxy
203
     *
204
     * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata
205
     * @param \Doctrine\ORM\Persisters\Entity\EntityPersister    $entityPersister
206
     * @param \Closure                                           $cloner
207
     *
208
     * @return \Closure
209
     *
210
     * @throws \Doctrine\ORM\EntityNotFoundException
211
     */
212 219
    private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, \Closure $cloner)
213
    {
214
        return function (
215
            GhostObjectInterface $proxy,
216
            $method,
217
            $parameters,
218
            & $initializer
219
        ) use (
220 103
            $entityPersister,
221 103
            $classMetadata,
222 103
            $cloner
223
        ) {
224 103
            if (! $initializer) {
225
                return false;
226
            }
227
228 103
            if ('__clone' === strtolower($method)) {
229 2
                $cloner($proxy, $initializer);
230
231 1
                return true;
232
            }
233
234 101
            $initializerBkp = $initializer;
235 101
            $initializer    = null;
236
237 101
            if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) {
238 3
                $initializer = $initializerBkp;
239
240 3
                throw new EntityNotFoundException($classMetadata->getName());
241
            }
242
243 98
            return true;
244 219
        };
245
    }
246
247
    /**
248
     * Creates a closure capable of finalizing state a cloned proxy
249
     *
250
     * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata
251
     * @param \Doctrine\ORM\Persisters\Entity\EntityPersister    $entityPersister
252
     *
253
     * @return \Closure
254
     *
255
     * @throws \Doctrine\ORM\EntityNotFoundException
256
     */
257
    private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister)
258
    {
259 219
        return function (GhostObjectInterface $proxy, & $initializer) use ($entityPersister, $classMetadata) {
260 2
            if ($proxy->isProxyInitialized()) {
261
                return;
262
            }
263
264 2
            $initializer = null;
265
266 2
            $class      = $entityPersister->getClassMetadata();
267 2
            $identifier = $classMetadata->getIdentifierValues($proxy);
268 2
            $original   = $entityPersister->loadById($identifier);
269
270 2
            if (null === $original) {
271 1
                throw EntityNotFoundException::fromClassNameAndIdentifier(
272 1
                    $classMetadata->getName(),
273 1
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
0 ignored issues
show
Compatibility introduced by
$classMetadata of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
274
                );
275
            }
276
277 1
            foreach ($class->getReflectionClass()->getProperties() as $property) {
278
                if ( ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
279
                    continue;
280
                }
281
282
                $property->setAccessible(true);
283
                $property->setValue($proxy, $property->getValue($original));
284
            }
285 219
        };
286
    }
287
288
    /**
289
     * @param ReflectionProperty $reflectionProperty
290
     *
291
     * @return string
292
     */
293 88
    private function getInternalReflectionPropertyName(ReflectionProperty $reflectionProperty)
294
    {
295 88
        if ($reflectionProperty->isProtected()) {
296 14
            return "\0*\0" . $reflectionProperty->getName();
297
        }
298
299 75
        if ($reflectionProperty->isPrivate()) {
300 23
            return "\0" . $reflectionProperty->getDeclaringClass()->getName()
0 ignored issues
show
introduced by
Consider using $reflectionProperty->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
301 23
            . "\0" . $reflectionProperty->getName();
302
        }
303
304 55
        return $reflectionProperty->getName();
305
    }
306
}
307