Failed Conditions
Pull Request — develop (#6719)
by Marco
65:21
created

StaticProxyFactory::generateProxyClasses()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 14
nc 2
nop 1
1
<?php
2
3
4
declare(strict_types=1);
5
6
namespace Doctrine\ORM\Proxy\Factory;
7
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\EntityNotFoundException;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Doctrine\ORM\Mapping\TransientMetadata;
12
use Doctrine\ORM\Persisters\Entity\EntityPersister;
13
use ProxyManager\Factory\LazyLoadingGhostFactory;
14
use ProxyManager\Proxy\GhostObjectInterface;
15
16
/**
17
 * Static factory for proxy objects.
18
 *
19
 * @package Doctrine\ORM\Proxy\Factory
20
 * @since 3.0
21
 *
22
 * @author Benjamin Eberlei <[email protected]>
23
 * @author Guilherme Blanco <[email protected]>
24
 * @author Marco Pivetta <[email protected]>
25
 *
26
 * @internal this class is to be used by ORM internals only
27
 */
28
final class StaticProxyFactory implements ProxyFactory
29
{
30
    private const SKIPPED_PROPERTIES = 'skippedProperties';
31
32
    /**
33
     * @var EntityManagerInterface
34
     */
35
    private $entityManager;
36
37
    /**
38
     * @var LazyLoadingGhostFactory
39
     */
40
    private $proxyFactory;
41
42
    /**
43
     * @var ClassMetadata[] indexed by metadata class name
44
     */
45
    private $cachedMetadata = [];
46
47
    /**
48
     * @var \Closure[] indexed by metadata class name
49
     */
50
    private $cachedInitializers = [];
51
52
    /**
53
     * @var EntityPersister[] indexed by metadata class name
54
     */
55
    private $cachedPersisters = [];
56
57
    /**
58
     * @var string[][] indexed by metadata class name
59
     */
60
    private $cachedSkippedProperties = [];
61
62
    public function __construct(
63
        EntityManagerInterface $entityManager,
64
        LazyLoadingGhostFactory $proxyFactory
65
    ) {
66
        $this->entityManager     = $entityManager;
67
        $this->proxyFactory      = $proxyFactory;
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     *
73
     * @param ClassMetadata[] $classMetadataList
74
     */
75
    public function generateProxyClasses(array $classMetadataList) : int
76
    {
77
        $concreteClasses = \array_filter($classMetadataList, function (ClassMetadata $metadata) : bool {
78
            return ! ($metadata->isMappedSuperclass || $metadata->getReflectionClass()->isAbstract());
79
        });
80
81
        foreach ($concreteClasses as $metadata) {
82
            $className = $metadata->getClassName();
83
84
            $this
85
                ->proxyFactory
86
                ->createProxy(
87
                    $className,
88
                    function () {
89
                        // empty closure, serves its purpose, for now
90
                    },
91
                    $this->cachedSkippedProperties[$className]
92
                        ?? $this->cachedSkippedProperties[$className] = [
93
                            self::SKIPPED_PROPERTIES => $this->skippedFieldsFqns($metadata)
94
                        ]
95
                );
96
        }
97
98
        return \count($concreteClasses);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     *
104
     * @throws \Doctrine\ORM\EntityNotFoundException
105
     */
106
    public function getProxy(string $className, array $identifier) : GhostObjectInterface
107
    {
108
        $metadata  = $this->cachedMetadata[$className]
109
            ?? $this->cachedMetadata[$className] = $this->entityManager->getClassMetadata($className);
110
        $persister = $this->cachedPersisters[$className]
111
            ?? $this->cachedPersisters[$className] = $this
112
                ->entityManager
113
                ->getUnitOfWork()
114
                ->getEntityPersister($metadata->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName does only exist in Doctrine\ORM\Mapping\ClassMetadata, but not in Doctrine\Common\Persistence\Mapping\ClassMetadata.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
115
116
        $proxyInstance = $this
117
            ->proxyFactory
118
            ->createProxy(
119
                $metadata->getClassName(),
120
                $this->cachedInitializers[$className]
121
                    ?? $this->cachedInitializers[$className] = $this->makeInitializer($metadata, $persister),
0 ignored issues
show
Bug introduced by
It seems like $metadata defined by $this->cachedMetadata[$c...ssMetadata($className)) on line 108 can also be of type object<Doctrine\Common\P...\Mapping\ClassMetadata>; however, Doctrine\ORM\Proxy\Facto...tory::makeInitializer() does only seem to accept object<Doctrine\ORM\Mapping\ClassMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
122
                $this->cachedSkippedProperties[$className]
123
                    ?? $this->cachedSkippedProperties[$className] = [
124
                        self::SKIPPED_PROPERTIES => $this->skippedFieldsFqns($metadata)
0 ignored issues
show
Bug introduced by
It seems like $metadata defined by $this->cachedMetadata[$c...ssMetadata($className)) on line 108 can also be of type object<Doctrine\Common\P...\Mapping\ClassMetadata>; however, Doctrine\ORM\Proxy\Facto...ry::skippedFieldsFqns() does only seem to accept object<Doctrine\ORM\Mapping\ClassMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
125
                    ]
126
            );
127
128
        $persister->setIdentifier($proxyInstance, $identifier);
129
130
        return $proxyInstance;
131
    }
132
133
    private function makeInitializer(ClassMetadata $metadata, EntityPersister $persister) : \Closure
134
    {
135
        return function (
136
            GhostObjectInterface $ghostObject,
137
            string $method, // we don't care
138
            array $parameters, // we don't care
139
            & $initializer,
140
            array $properties // we currently do not use this
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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...
141
        ) use ($metadata, $persister) : bool {
142
            $originalInitializer = $initializer;
143
            $initializer = null;
144
145
            $identifier = $persister->getIdentifier($ghostObject);
146
147
            // @TODO how do we use `$properties` in the persister? That would be a massive optimisation
148
            if (! $persister->loadById($identifier, $ghostObject)) {
149
                $initializer = $originalInitializer;
150
151
                throw EntityNotFoundException::fromClassNameAndIdentifier(
152
                    $metadata->getClassName(),
153
                    $identifier
154
                );
155
            }
156
157
            return true;
158
        };
159
    }
160
161
    private function skippedFieldsFqns(ClassMetadata $metadata) : array
162
    {
163
        return \array_merge(
164
            $this->identifierFieldFqns($metadata),
165
            $this->transientFieldsFqns($metadata)
166
        );
167
    }
168
169
    private function transientFieldsFqns(ClassMetadata $metadata) : array
170
    {
171
        $transientFieldsFqns = [];
172
173
        foreach ($metadata->getDeclaredPropertiesIterator() as $name => $property) {
174
            if (! $property instanceof TransientMetadata) {
175
                continue;
176
            }
177
178
            $transientFieldsFqns[] = $this->propertyFqcn(
179
                $property
180
                    ->getDeclaringClass()
181
                    ->getReflectionClass()
182
                    ->getProperty($name) // @TODO possible NPR. This should never be null, why is it allowed to be?
183
            );
184
        }
185
186
        return $transientFieldsFqns;
187
    }
188
189
    private function identifierFieldFqns(ClassMetadata $metadata) : array
190
    {
191
        $idFieldFqcns = [];
192
193
        foreach ($metadata->getIdentifierFieldNames() as $idField) {
194
            $idFieldFqcns[] = $this->propertyFqcn(
195
                $metadata
196
                    ->getProperty($idField)
197
                    ->getDeclaringClass()
198
                    ->getReflectionClass()
199
                    ->getProperty($idField) // @TODO possible NPR. This should never be null, why is it allowed to be?
200
            );
201
        }
202
203
        return $idFieldFqcns;
204
    }
205
206
    private function propertyFqcn(\ReflectionProperty $property) : string
207
    {
208
        if ($property->isPrivate()) {
209
            return "\0" . $property->getDeclaringClass()->getName() . "\0" . $property->getName();
0 ignored issues
show
introduced by
Consider using $property->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
210
        }
211
212
        if ($property->isProtected()) {
213
            return "\0*\0" . $property->getName();
214
        }
215
216
        return $property->getName();
217
    }
218
}
219