Failed Conditions
Pull Request — develop (#6719)
by Marco
63:32
created

StaticProxyFactory::makeInitializer()   B

Complexity

Conditions 2
Paths 1

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 17
nc 1
nop 2
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
 */
25
final class StaticProxyFactory implements ProxyFactory
26
{
27
    private const SKIPPED_PROPERTIES = 'skippedProperties';
28
29
    /**
30
     * @var EntityManagerInterface
31
     */
32
    private $entityManager;
33
34
    /**
35
     * @var LazyLoadingGhostFactory
36
     */
37
    private $proxyFactory;
38
39
    /**
40
     * @var ClassMetadata[] indexed by metadata class name
41
     */
42
    private $cachedMetadata = [];
43
44
    /**
45
     * @var \Closure[] indexed by metadata class name
46
     */
47
    private $cachedInitializers = [];
48
49
    /**
50
     * @var EntityPersister[] indexed by metadata class name
51
     */
52
    private $cachedPersisters = [];
53
54
    /**
55
     * @var string[][] indexed by metadata class name
56
     */
57
    private $cachedSkippedProperties = [];
58
59
    public function __construct(
60
        EntityManagerInterface $entityManager,
61
        LazyLoadingGhostFactory $proxyFactory
62
    ) {
63
        $this->entityManager     = $entityManager;
64
        $this->proxyFactory      = $proxyFactory;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @param ClassMetadata[] $classMetadataList
71
     */
72
    public function generateProxyClasses(array $classMetadataList) : int
73
    {
74
        $concreteClasses = \array_filter($classMetadataList, function (ClassMetadata $metadata) : bool {
75
            return ! ($metadata->isMappedSuperclass || $metadata->getReflectionClass()->isAbstract());
76
        });
77
78
        foreach ($concreteClasses as $metadata) {
79
            $className = $metadata->getClassName();
80
81
            $this
82
                ->proxyFactory
83
                ->createProxy(
84
                    $className,
85
                    function () {
86
                        // empty closure, serves its purpose, for now
87
                    },
88
                    $this->cachedSkippedProperties[$className]
89
                        ?? $this->cachedSkippedProperties[$className] = [
90
                            self::SKIPPED_PROPERTIES => $this->skippedFieldsFqns($metadata)
91
                        ]
92
                );
93
        }
94
95
        return \count($concreteClasses);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     * @throws \Doctrine\ORM\EntityNotFoundException
101
     */
102
    public function getProxy(string $className, array $identifier) : GhostObjectInterface
103
    {
104
        $metadata  = $this->cachedMetadata[$className]
105
            ?? $this->cachedMetadata[$className] = $this->entityManager->getClassMetadata($className);
106
        $persister = $this->cachedPersisters[$className]
107
            ?? $this->cachedPersisters[$className] = $this
108
                ->entityManager
109
                ->getUnitOfWork()
110
                ->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...
111
112
        $proxyInstance = $this
113
            ->proxyFactory
114
            ->createProxy(
115
                $metadata->getClassName(),
116
                $this->cachedInitializers[$className]
117
                    ?? $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 104 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...
118
                $this->cachedSkippedProperties[$className]
119
                    ?? $this->cachedSkippedProperties[$className] = [
120
                        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 104 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...
121
                    ]
122
            );
123
124
        $persister->setIdentifier($proxyInstance, $identifier);
125
126
        return $proxyInstance;
127
    }
128
129
    private function makeInitializer(ClassMetadata $metadata, EntityPersister $persister) : \Closure
130
    {
131
        return function (
132
            GhostObjectInterface $ghostObject,
133
            string $method, // we don't care
134
            array $parameters, // we don't care
135
            & $initializer,
136
            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...
137
        ) use ($metadata, $persister) : bool {
138
            $originalInitializer = $initializer;
139
            $initializer = null;
140
141
            $identifier = $persister->getIdentifier($ghostObject);
142
143
            // @TODO how do we use `$properties` in the persister? That would be a massive optimisation
144
            if (! $persister->loadById($identifier, $ghostObject)) {
145
                $initializer = $originalInitializer;
146
147
                throw EntityNotFoundException::fromClassNameAndIdentifier(
148
                    $metadata->getClassName(),
149
                    $identifier
150
                );
151
            }
152
153
            return true;
154
        };
155
    }
156
157
    private function skippedFieldsFqns(ClassMetadata $metadata) : array
158
    {
159
        return \array_merge(
160
            $this->identifierFieldFqns($metadata),
161
            $this->transientFieldsFqns($metadata)
162
        );
163
    }
164
165
    private function transientFieldsFqns(ClassMetadata $metadata) : array
166
    {
167
        $transientFieldsFqns = [];
168
169
        foreach ($metadata->getDeclaredPropertiesIterator() as $name => $property) {
170
            if (! $property instanceof TransientMetadata) {
171
                continue;
172
            }
173
174
            $transientFieldsFqns[] = $this->propertyFqcn(
175
                $property
176
                    ->getDeclaringClass()
177
                    ->getReflectionClass()
178
                    ->getProperty($name) // @TODO possible NPR. This should never be null, why is it allowed to be?
179
            );
180
        }
181
182
        return $transientFieldsFqns;
183
    }
184
185
    private function identifierFieldFqns(ClassMetadata $metadata) : array
186
    {
187
        $idFieldFqcns = [];
188
189
        foreach ($metadata->getIdentifierFieldNames() as $idField) {
190
            $idFieldFqcns[] = $this->propertyFqcn(
191
                $metadata
192
                    ->getProperty($idField)
193
                    ->getDeclaringClass()
194
                    ->getReflectionClass()
195
                    ->getProperty($idField) // @TODO possible NPR. This should never be null, why is it allowed to be?
196
            );
197
        }
198
199
        return $idFieldFqcns;
200
    }
201
202
    private function propertyFqcn(\ReflectionProperty $property) : string
203
    {
204
        if ($property->isPrivate()) {
205
            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...
206
        }
207
208
        if ($property->isProtected()) {
209
            return "\0*\0" . $property->getName();
210
        }
211
212
        return $property->getName();
213
    }
214
}
215