1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Doctrine\Tests\ORM\Proxy; |
||||
6 | |||||
7 | use Doctrine\ORM\EntityNotFoundException; |
||||
8 | use Doctrine\ORM\Mapping\ClassMetadata; |
||||
9 | use Doctrine\ORM\Mapping\ClassMetadataBuildingContext; |
||||
10 | use Doctrine\ORM\Mapping\ClassMetadataFactory; |
||||
11 | use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; |
||||
12 | use Doctrine\ORM\Proxy\Factory\StaticProxyFactory; |
||||
13 | use Doctrine\ORM\Reflection\RuntimeReflectionService; |
||||
14 | use Doctrine\Tests\Mocks\ConnectionMock; |
||||
15 | use Doctrine\Tests\Mocks\DriverMock; |
||||
16 | use Doctrine\Tests\Mocks\EntityManagerMock; |
||||
17 | use Doctrine\Tests\Mocks\UnitOfWorkMock; |
||||
18 | use Doctrine\Tests\Models\Company\CompanyEmployee; |
||||
19 | use Doctrine\Tests\Models\ECommerce\ECommerceFeature; |
||||
20 | use Doctrine\Tests\Models\FriendObject\ComparableObject; |
||||
21 | use Doctrine\Tests\Models\ProxySpecifics\FuncGetArgs; |
||||
22 | use Doctrine\Tests\OrmTestCase; |
||||
23 | use PHPUnit_Framework_MockObject_MockObject; |
||||
24 | use ProxyManager\Proxy\GhostObjectInterface; |
||||
25 | use stdClass; |
||||
26 | use function json_encode; |
||||
27 | |||||
28 | /** |
||||
29 | * Test the proxy generator. Its work is generating on-the-fly subclasses of a given model, which implement the Proxy pattern. |
||||
30 | */ |
||||
31 | class ProxyFactoryTest extends OrmTestCase |
||||
32 | { |
||||
33 | /** @var ConnectionMock */ |
||||
34 | private $connectionMock; |
||||
35 | |||||
36 | /** @var UnitOfWorkMock */ |
||||
37 | private $uowMock; |
||||
38 | |||||
39 | /** @var EntityManagerMock */ |
||||
40 | private $emMock; |
||||
41 | |||||
42 | /** @var StaticProxyFactory */ |
||||
43 | private $proxyFactory; |
||||
44 | |||||
45 | /** @var ClassMetadataBuildingContext */ |
||||
46 | private $metadataBuildingContext; |
||||
47 | |||||
48 | /** |
||||
49 | * {@inheritDoc} |
||||
50 | */ |
||||
51 | protected function setUp() : void |
||||
52 | { |
||||
53 | parent::setUp(); |
||||
54 | |||||
55 | $this->metadataBuildingContext = new ClassMetadataBuildingContext( |
||||
56 | $this->createMock(ClassMetadataFactory::class), |
||||
57 | new RuntimeReflectionService() |
||||
58 | ); |
||||
59 | $this->connectionMock = new ConnectionMock([], new DriverMock()); |
||||
60 | $this->emMock = EntityManagerMock::create($this->connectionMock); |
||||
61 | $this->uowMock = new UnitOfWorkMock($this->emMock); |
||||
62 | |||||
63 | $this->emMock->setUnitOfWork($this->uowMock); |
||||
64 | |||||
65 | $this->proxyFactory = new StaticProxyFactory( |
||||
66 | $this->emMock, |
||||
67 | $this->emMock->getConfiguration()->buildGhostObjectFactory() |
||||
68 | ); |
||||
69 | } |
||||
70 | |||||
71 | public function testReferenceProxyDelegatesLoadingToThePersister() : void |
||||
72 | { |
||||
73 | $identifier = ['id' => 42]; |
||||
74 | $classMetaData = $this->emMock->getClassMetadata(ECommerceFeature::class); |
||||
75 | |||||
76 | $persister = $this |
||||
77 | ->getMockBuilder(BasicEntityPersister::class) |
||||
78 | ->setConstructorArgs([$this->emMock, $classMetaData]) |
||||
79 | ->setMethods(['loadById']) |
||||
80 | ->getMock(); |
||||
81 | |||||
82 | $persister |
||||
83 | ->expects($this->atLeastOnce()) |
||||
84 | ->method('loadById') |
||||
85 | ->with( |
||||
86 | $identifier, |
||||
87 | self::logicalAnd( |
||||
88 | self::isInstanceOf(GhostObjectInterface::class), |
||||
89 | self::isInstanceOf(ECommerceFeature::class) |
||||
90 | ) |
||||
91 | ) |
||||
92 | ->will($this->returnValue(new stdClass())); |
||||
93 | |||||
94 | $this->uowMock->setEntityPersister(ECommerceFeature::class, $persister); |
||||
95 | |||||
96 | /** @var GhostObjectInterface|ECommerceFeature $proxy */ |
||||
97 | $proxy = $this->proxyFactory->getProxy($classMetaData, $identifier); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
98 | |||||
99 | $proxy->getDescription(); |
||||
100 | } |
||||
101 | |||||
102 | public function testSkipMappedSuperClassesOnGeneration() : void |
||||
103 | { |
||||
104 | $cm = new ClassMetadata(stdClass::class, $this->metadataBuildingContext); |
||||
105 | $cm->isMappedSuperclass = true; |
||||
106 | |||||
107 | self::assertSame( |
||||
108 | 0, |
||||
109 | $this->proxyFactory->generateProxyClasses([$cm]), |
||||
110 | 'No proxies generated.' |
||||
111 | ); |
||||
112 | } |
||||
113 | |||||
114 | /** |
||||
115 | * @group 6625 |
||||
116 | * @group embedded |
||||
117 | */ |
||||
118 | public function testSkipEmbeddableClassesOnGeneration() : void |
||||
119 | { |
||||
120 | $cm = new ClassMetadata(stdClass::class, $this->metadataBuildingContext); |
||||
121 | $cm->isEmbeddedClass = true; |
||||
122 | |||||
123 | self::assertSame( |
||||
124 | 0, |
||||
125 | $this->proxyFactory->generateProxyClasses([$cm]), |
||||
126 | 'No proxies generated.' |
||||
127 | ); |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * @group DDC-1771 |
||||
132 | */ |
||||
133 | public function testSkipAbstractClassesOnGeneration() : void |
||||
134 | { |
||||
135 | $cm = new ClassMetadata(AbstractClass::class, $this->metadataBuildingContext); |
||||
136 | |||||
137 | self::assertNotNull($cm->getReflectionClass()); |
||||
138 | |||||
139 | $num = $this->proxyFactory->generateProxyClasses([$cm]); |
||||
140 | |||||
141 | self::assertEquals(0, $num, 'No proxies generated.'); |
||||
142 | } |
||||
143 | |||||
144 | /** |
||||
145 | * @group DDC-2432 |
||||
146 | */ |
||||
147 | public function testFailedProxyLoadingDoesNotMarkTheProxyAsInitialized() : void |
||||
148 | { |
||||
149 | $classMetaData = $this->emMock->getClassMetadata(ECommerceFeature::class); |
||||
150 | |||||
151 | $persister = $this |
||||
152 | ->getMockBuilder(BasicEntityPersister::class) |
||||
153 | ->setConstructorArgs([$this->emMock, $classMetaData]) |
||||
154 | ->setMethods(['load']) |
||||
155 | ->getMock(); |
||||
156 | |||||
157 | $persister |
||||
158 | ->expects($this->atLeastOnce()) |
||||
159 | ->method('load') |
||||
160 | ->will($this->returnValue(null)); |
||||
161 | |||||
162 | $this->uowMock->setEntityPersister(ECommerceFeature::class, $persister); |
||||
163 | |||||
164 | /** @var GhostObjectInterface|ECommerceFeature $proxy */ |
||||
165 | $proxy = $this->proxyFactory->getProxy($classMetaData, ['id' => 42]); |
||||
0 ignored issues
–
show
$classMetaData of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
166 | |||||
167 | try { |
||||
168 | $proxy->getDescription(); |
||||
169 | $this->fail('An exception was expected to be raised'); |
||||
170 | } catch (EntityNotFoundException $exception) { |
||||
171 | } |
||||
172 | |||||
173 | self::assertFalse($proxy->isProxyInitialized()); |
||||
174 | } |
||||
175 | |||||
176 | /** |
||||
177 | * @group DDC-2432 |
||||
178 | */ |
||||
179 | public function testFailedProxyCloningDoesNotMarkTheProxyAsInitialized() : void |
||||
180 | { |
||||
181 | $classMetaData = $this->emMock->getClassMetadata(ECommerceFeature::class); |
||||
182 | |||||
183 | $persister = $this |
||||
184 | ->getMockBuilder(BasicEntityPersister::class) |
||||
185 | ->setConstructorArgs([$this->emMock, $classMetaData]) |
||||
186 | ->setMethods(['load']) |
||||
187 | ->getMock(); |
||||
188 | |||||
189 | $persister |
||||
190 | ->expects($this->atLeastOnce()) |
||||
191 | ->method('load') |
||||
192 | ->will($this->returnValue(null)); |
||||
193 | |||||
194 | $this->uowMock->setEntityPersister(ECommerceFeature::class, $persister); |
||||
195 | |||||
196 | /** @var GhostObjectInterface|ECommerceFeature $proxy */ |
||||
197 | $proxy = $this->proxyFactory->getProxy($classMetaData, ['id' => 42]); |
||||
0 ignored issues
–
show
$classMetaData of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
198 | |||||
199 | try { |
||||
200 | $cloned = clone $proxy; |
||||
201 | $this->fail('An exception was expected to be raised'); |
||||
202 | } catch (EntityNotFoundException $exception) { |
||||
203 | } |
||||
204 | |||||
205 | self::assertFalse($proxy->isProxyInitialized()); |
||||
206 | } |
||||
207 | |||||
208 | public function testProxyClonesParentFields() : void |
||||
209 | { |
||||
210 | $identifier = ['id' => 42]; |
||||
211 | $classMetaData = $this->emMock->getClassMetadata(CompanyEmployee::class); |
||||
212 | |||||
213 | $persister = $this |
||||
214 | ->getMockBuilder(BasicEntityPersister::class) |
||||
215 | ->setConstructorArgs([$this->emMock, $classMetaData]) |
||||
216 | ->setMethods(['loadById']) |
||||
217 | ->getMock(); |
||||
218 | |||||
219 | $persister |
||||
220 | ->expects(self::atLeastOnce()) |
||||
221 | ->method('loadById') |
||||
222 | ->with( |
||||
223 | $identifier, |
||||
224 | self::logicalAnd( |
||||
225 | self::isInstanceOf(GhostObjectInterface::class), |
||||
226 | self::isInstanceOf(CompanyEmployee::class) |
||||
227 | ) |
||||
228 | ) |
||||
229 | ->willReturnCallback(static function (array $id, CompanyEmployee $companyEmployee) { |
||||
230 | $companyEmployee->setSalary(1000); // A property on the CompanyEmployee |
||||
231 | $companyEmployee->setName('Bob'); // A property on the parent class, CompanyPerson |
||||
232 | |||||
233 | return $companyEmployee; |
||||
234 | }); |
||||
235 | |||||
236 | $this->uowMock->setEntityPersister(CompanyEmployee::class, $persister); |
||||
237 | |||||
238 | /** @var GhostObjectInterface|CompanyEmployee $proxy */ |
||||
239 | $proxy = $this->proxyFactory->getProxy($classMetaData, $identifier); |
||||
0 ignored issues
–
show
$classMetaData of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
240 | |||||
241 | $cloned = clone $proxy; |
||||
242 | |||||
243 | self::assertSame(42, $cloned->getId(), 'Expected the Id to be cloned'); |
||||
244 | self::assertSame(1000, $cloned->getSalary(), 'Expect properties on the CompanyEmployee class to be cloned'); |
||||
245 | self::assertSame('Bob', $cloned->getName(), 'Expect properties on the CompanyPerson class to be cloned'); |
||||
246 | } |
||||
247 | |||||
248 | public function testFriendObjectsDoNotLazyLoadIfNotAccessingLazyState() : void |
||||
249 | { |
||||
250 | /** @var BasicEntityPersister|PHPUnit_Framework_MockObject_MockObject $persister */ |
||||
251 | $persister = $this->createMock(BasicEntityPersister::class); |
||||
252 | $persister->expects(self::never())->method('loadById'); |
||||
253 | |||||
254 | $this->uowMock->setEntityPersister(ComparableObject::class, $persister); |
||||
255 | |||||
256 | /** @var ComparableObject|GhostObjectInterface $comparable */ |
||||
257 | $comparable = $this->proxyFactory->getProxy( |
||||
258 | $this->emMock->getClassMetadata(ComparableObject::class), |
||||
0 ignored issues
–
show
$this->emMock->getClassM...omparableObject::class) of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
259 | ['id' => 123] |
||||
260 | ); |
||||
261 | |||||
262 | self::assertInstanceOf(ComparableObject::class, $comparable); |
||||
263 | self::assertInstanceOf(GhostObjectInterface::class, $comparable); |
||||
264 | self::assertFalse($comparable->isProxyInitialized()); |
||||
265 | |||||
266 | // due to implementation details, identity check is not reading lazy state: |
||||
267 | self::assertTrue($comparable->equalTo($comparable)); |
||||
268 | |||||
269 | self::assertFalse($comparable->isProxyInitialized()); |
||||
270 | } |
||||
271 | |||||
272 | public function testFriendObjectsLazyLoadWhenAccessingLazyState() : void |
||||
273 | { |
||||
274 | /** @var BasicEntityPersister|PHPUnit_Framework_MockObject_MockObject $persister */ |
||||
275 | $persister = $this |
||||
276 | ->getMockBuilder(BasicEntityPersister::class) |
||||
277 | ->setConstructorArgs([$this->emMock, $this->emMock->getClassMetadata(ComparableObject::class)]) |
||||
278 | ->setMethods(['loadById']) |
||||
279 | ->getMock(); |
||||
280 | |||||
281 | $persister |
||||
282 | ->expects(self::exactly(2)) |
||||
283 | ->method('loadById') |
||||
284 | ->with( |
||||
285 | self::logicalOr(['id' => 123], ['id' => 456]), |
||||
286 | self::logicalAnd( |
||||
287 | self::isInstanceOf(GhostObjectInterface::class), |
||||
288 | self::isInstanceOf(ComparableObject::class) |
||||
289 | ) |
||||
290 | ) |
||||
291 | ->willReturnCallback(static function (array $id, ComparableObject $comparableObject) { |
||||
292 | $comparableObject->setComparedFieldValue(json_encode($id)); |
||||
293 | |||||
294 | return $comparableObject; |
||||
295 | }); |
||||
296 | |||||
297 | $this->uowMock->setEntityPersister(ComparableObject::class, $persister); |
||||
298 | |||||
299 | $metadata = $this->emMock->getClassMetadata(ComparableObject::class); |
||||
300 | |||||
301 | /** @var ComparableObject|GhostObjectInterface $comparable1 */ |
||||
302 | $comparable1 = $this->proxyFactory->getProxy($metadata, ['id' => 123]); |
||||
0 ignored issues
–
show
$metadata of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
303 | /** @var ComparableObject|GhostObjectInterface $comparable2 */ |
||||
304 | $comparable2 = $this->proxyFactory->getProxy($metadata, ['id' => 456]); |
||||
305 | |||||
306 | self::assertInstanceOf(ComparableObject::class, $comparable1); |
||||
307 | self::assertInstanceOf(ComparableObject::class, $comparable2); |
||||
308 | self::assertInstanceOf(GhostObjectInterface::class, $comparable1); |
||||
309 | self::assertInstanceOf(GhostObjectInterface::class, $comparable2); |
||||
310 | self::assertNotSame($comparable1, $comparable2); |
||||
311 | self::assertFalse($comparable1->isProxyInitialized()); |
||||
312 | self::assertFalse($comparable2->isProxyInitialized()); |
||||
313 | |||||
314 | self::assertFalse( |
||||
315 | $comparable1->equalTo($comparable2), |
||||
316 | 'Due to implementation details, identity check is not reading lazy state' |
||||
317 | ); |
||||
318 | |||||
319 | self::assertTrue($comparable1->isProxyInitialized()); |
||||
320 | self::assertTrue($comparable2->isProxyInitialized()); |
||||
321 | } |
||||
322 | |||||
323 | public function testProxyMethodsSupportFuncGetArgsLogic() : void |
||||
324 | { |
||||
325 | /** @var BasicEntityPersister|PHPUnit_Framework_MockObject_MockObject $persister */ |
||||
326 | $persister = $this->createMock(BasicEntityPersister::class); |
||||
327 | $persister->expects(self::never())->method('loadById'); |
||||
328 | |||||
329 | $this->uowMock->setEntityPersister(FuncGetArgs::class, $persister); |
||||
330 | |||||
331 | /** @var FuncGetArgs|GhostObjectInterface $funcGetArgs */ |
||||
332 | $funcGetArgs = $this->proxyFactory->getProxy( |
||||
333 | $this->emMock->getClassMetadata(FuncGetArgs::class), |
||||
0 ignored issues
–
show
$this->emMock->getClassM...ics\FuncGetArgs::class) of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
334 | ['id' => 123] |
||||
335 | ); |
||||
336 | |||||
337 | self::assertInstanceOf(GhostObjectInterface::class, $funcGetArgs); |
||||
338 | self::assertFalse($funcGetArgs->isProxyInitialized()); |
||||
339 | |||||
340 | self::assertSame( |
||||
341 | [1, 2, 3, 4], |
||||
342 | $funcGetArgs->funcGetArgsCallingMethod(1, 2, 3, 4), |
||||
343 | '`func_get_args()` calls are now supported in proxy implementations' |
||||
344 | ); |
||||
345 | |||||
346 | self::assertFalse($funcGetArgs->isProxyInitialized(), 'No state was accessed anyway'); |
||||
347 | } |
||||
348 | } |
||||
349 | |||||
350 | abstract class AbstractClass |
||||
351 | { |
||||
352 | } |
||||
353 |