Passed
Push — master ( 78afc8...a8b608 )
by Valentin
01:49
created

tests/Promise/ProxyPrinterTest.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
declare(strict_types=1);
3
4
namespace Cycle\ORM\Promise\Tests;
5
6
use Cycle\Annotated\Entities;
7
use Cycle\ORM\ORMInterface;
8
use Cycle\ORM\Promise\Declaration\Declarations;
9
use Cycle\ORM\Promise\Declaration\Extractor;
10
use Cycle\ORM\Promise\Declaration\Structure;
11
use Cycle\ORM\Promise\PromiseInterface;
12
use Cycle\ORM\Promise\PromiseResolver;
13
use Cycle\ORM\Promise\ProxyPrinter;
14
use Cycle\ORM\Promise\Tests\Fixtures;
15
use Cycle\ORM\Promise\Utils;
16
use Cycle\Schema;
17
use PhpParser\PrettyPrinter\Standard;
18
use PhpParser\PrettyPrinterAbstract;
19
use Spiral\Core\Container;
20
use Spiral\Database\Driver\SQLite\SQLiteDriver;
21
22
class ProxyPrinterTest extends BaseTest
23
{
24
    public const DRIVER = 'sqlite';
25
26
    /** @var \Spiral\Core\Container */
27
    private $container;
28
29
    public function setUp()
30
    {
31
        self::$config = [
32
            'debug'     => false,
33
            'strict'    => true,
34
            'benchmark' => false,
35
            'sqlite'    => [
36
                'driver' => SQLiteDriver::class,
37
                'check'  => function () {
38
                    return !in_array('sqlite', \PDO::getAvailableDrivers());
39
                },
40
                'conn'   => 'sqlite::memory:',
41
                'user'   => 'sqlite',
42
                'pass'   => ''
43
            ],
44
        ];
45
46
        parent::setUp();
47
48
        $this->container = new Container();
49
        $this->container->bind(ORMInterface::class, $this->orm());
50
    }
51
52
    public function testDeclaration(): void
53
    {
54
        $class = Fixtures\Entity::class;
55
        $as = 'EntityProxy' . __LINE__;
56
57
        $r = new \ReflectionClass($class);
58
        $declaration = Declarations::createFromReflection($r, $as);
59
        $output = $this->make($r, $declaration);
60
        $output = ltrim($output, '<?php');
61
62
        $this->assertFalse(class_exists($declaration->class->getFullName()));
63
64
        eval($output);
65
66
        $this->assertStringNotContainsString('abstract', $output);
67
        $this->assertStringContainsString(sprintf(
68
            'class %s extends %s implements %s',
69
            $as,
70
            Utils::shortName($class),
71
            Utils::shortName(PromiseInterface::class)
72
        ), $output);
73
74
        $proxy = $this->makeProxyObject($class, $declaration->class->getFullName());
75
76
        $this->assertInstanceOf($declaration->class->getFullName(), $proxy);
77
        $this->assertInstanceOf($class, $proxy);
78
        $this->assertInstanceOf(PromiseInterface::class, $proxy);
79
    }
80
81
    /**
82
     * @throws \ReflectionException
83
     */
84
    public function testSameNamespace(): void
85
    {
86
        $class = Fixtures\Entity::class;
87
        $as = 'EntityProxy' . __LINE__;
88
89
        $r = new \ReflectionClass($class);
90
        $declaration = Declarations::createFromReflection($r, $as);
91
        $output = $this->make($r, $declaration);
92
        $output = ltrim($output, '<?php');
93
94
        $this->assertFalse(class_exists($declaration->class->getFullName()));
95
96
        eval($output);
97
98
        $origReflection = new \ReflectionClass($class);
99
        $proxyReflection = new \ReflectionClass($declaration->class->getFullName());
100
        $this->assertSame($origReflection->getNamespaceName(), $proxyReflection->getNamespaceName());
101
    }
102
103
    /**
104
     * @throws \ReflectionException
105
     */
106
    public function testDifferentNamespace(): void
107
    {
108
        $class = Fixtures\Entity::class;
109
        $as = "\EntityProxy" . __LINE__;
110
111
        $r = new \ReflectionClass($class);
112
        $declaration = Declarations::createFromReflection($r, $as);
113
        $output = $this->make($r, $declaration);
114
        $output = ltrim($output, '<?php');
115
116
        $this->assertFalse(class_exists($declaration->class->getFullName()));
117
118
        eval($output);
119
120
        $proxyReflection = new \ReflectionClass($declaration->class->getFullName());
121
        $this->assertSame('', (string)$proxyReflection->getNamespaceName());
122
        $this->assertStringNotContainsString('namespace ', $output);
123
    }
124
125
    /**
126
     * @throws \ReflectionException
127
     */
128
    public function testUseStmtsInSameNamespace(): void
129
    {
130
        $class = Fixtures\Entity::class;
131
        $as = 'EntityProxy' . __LINE__;
132
133
        $r = new \ReflectionClass($class);
134
        $declaration = Declarations::createFromReflection($r, $as);
135
        $output = $this->make($r, $declaration);
136
        $output = ltrim($output, '<?php');
137
138
        $this->assertFalse(class_exists($declaration->class->getFullName()));
139
140
        eval($output);
141
142
        $this->assertSame($this->fetchUseStatements($output), $this->fetchExternalDependencies($declaration->class->getFullName(), [
143
            PromiseResolver::class,
144
            PromiseInterface::class
145
        ]));
146
    }
147
148
    /**
149
     * @throws \ReflectionException
150
     */
151
    public function testUseStmtsInDifferentNamespace(): void
152
    {
153
        $class = Fixtures\Entity::class;
154
        $as = "\EntityProxy" . __LINE__;
155
156
        $r = new \ReflectionClass($class);
157
        $declaration = Declarations::createFromReflection($r, $as);
158
        $output = $this->make($r, $declaration);
159
        $output = ltrim($output, '<?php');
160
161
        $this->assertFalse(class_exists($declaration->class->getFullName()));
162
163
        eval($output);
164
165
        $this->assertSame($this->fetchUseStatements($output), $this->fetchExternalDependencies($declaration->class->getFullName(), [
166
            PromiseResolver::class,
167
            PromiseInterface::class,
168
            $class
169
        ]));
170
    }
171
172
    private function fetchUseStatements(string $code): array
173
    {
174
        $uses = [];
175
        foreach (explode("\n", $code) as $line) {
176
            if (mb_stripos($line, 'use') !== 0) {
177
                continue;
178
            }
179
180
            $uses[] = trim(mb_substr($line, 4), " ;\r\n");
181
        }
182
183
        sort($uses);
184
185
        return $uses;
186
    }
187
188
    /**
189
     * @param string $class
190
     * @param array  $types
191
     *
192
     * @return array
193
     * @throws \ReflectionException
194
     */
195
    private function fetchExternalDependencies(string $class, array $types = []): array
196
    {
197
        $reflection = new \ReflectionClass($class);
198
199
        foreach ($reflection->getConstructor()->getParameters() as $parameter) {
200
            if (!$parameter->hasType() || $parameter->getType()->isBuiltin()) {
201
                continue;
202
            }
203
204
            $types[] = $parameter->getType()->getName();
205
        }
206
207
        sort($types);
208
209
        return $types;
210
    }
211
212
    public function testTraits(): void
213
    {
214
        $r = new \ReflectionClass(Fixtures\EntityWithoutTrait::class);
215
        $this->assertStringNotContainsString(' use ', $this->make($r, Declarations::createFromReflection($r, 'EntityProxy' . __LINE__)));
216
217
        $r = new \ReflectionClass(Fixtures\EntityWithTrait::class);
218
        $this->assertStringNotContainsString(' use ', $this->make($r, Declarations::createFromReflection($r, 'EntityProxy' . __LINE__)));
219
    }
220
221
    public function testConstants(): void
222
    {
223
        $r = new \ReflectionClass(Fixtures\EntityWithoutConstants::class);
224
        $this->assertStringNotContainsString(' const ', $this->make($r, Declarations::createFromReflection($r, 'EntityProxy' . __LINE__)));
225
226
        $r = new \ReflectionClass(Fixtures\EntityWithConstants::class);
227
        $this->assertStringNotContainsString(' const ', $this->make($r, Declarations::createFromReflection($r, 'EntityProxy' . __LINE__)));
228
    }
229
230
    public function testProperties(): void
231
    {
232
        $class = Fixtures\Entity::class;
233
        $as = 'EntityProxy' . __LINE__;
234
235
        $r = new \ReflectionClass($class);
236
        $declaration = Declarations::createFromReflection($r, $as);
237
        $output = $this->make($r, $declaration);
238
        $output = ltrim($output, '<?php');
239
240
        $this->assertFalse(class_exists($declaration->class->getFullName()));
241
242
        eval($output);
243
244
        $reflection = new \ReflectionClass($declaration->class->getFullName());
245
246
        /** @var \ReflectionProperty[] $properties */
247
        $properties = [];
248
        foreach ($reflection->getProperties() as $property) {
249
            if ($property->getDeclaringClass()->getName() !== $declaration->class->getFullName()) {
0 ignored issues
show
Consider using $property->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
250
                continue;
251
            }
252
253
            $properties[] = $property;
254
        }
255
256
        $this->assertCount(1, $properties);
257
        $property = $properties[0];
258
        $this->assertTrue($property->isPrivate());
259
        $this->assertFalse($property->isStatic());
260
        $this->assertStringContainsString('@var PromiseResolver|' . Utils::shortName($class), $property->getDocComment());
261
    }
262
263
    /**
264
     * @throws \ReflectionException
265
     */
266
    public function testHasConstructor(): void
267
    {
268
        $class = Fixtures\EntityWithoutConstructor::class;
269
        $as = 'EntityProxy' . __LINE__;
270
271
        $r = new \ReflectionClass($class);
272
        $declaration = Declarations::createFromReflection($r, $as);
273
        $output = $this->make($r, $declaration);
274
        $output = ltrim($output, '<?php');
275
276
        $this->assertFalse(class_exists($declaration->class->getFullName()));
277
278
        eval($output);
279
280
        $reflection = new \ReflectionClass($declaration->class->getFullName());
281
        $constructor = $reflection->getConstructor();
282
        $this->assertNotNull($constructor);
283
284
        $class = Fixtures\EntityWithConstructor::class;
285
        $as = 'EntityProxy' . __LINE__;
286
287
        $r = new \ReflectionClass($class);
288
        $declaration = Declarations::createFromReflection($r, $as);
289
        $output = $this->make($r, $declaration);
290
        $output = ltrim($output, '<?php');
291
292
        $this->assertFalse(class_exists($declaration->class->getFullName()));
293
294
        eval($output);
295
296
        $reflection = new \ReflectionClass($declaration->class->getFullName());
297
        $constructor = $reflection->getConstructor();
298
        $this->assertNotNull($constructor);
299
    }
300
301
    public function testNotContainParentConstructor(): void
302
    {
303
        $class = Fixtures\EntityWithoutConstructor::class;
304
        $as = 'EntityProxy' . __LINE__;
305
306
        $r = new \ReflectionClass($class);
307
        $declaration = Declarations::createFromReflection($r, $as);
308
        $output = $this->make($r, $declaration);
309
        $output = ltrim($output, '<?php');
310
311
        $this->assertFalse(class_exists($declaration->class->getFullName()));
312
313
        eval($output);
314
315
        $this->assertStringNotContainsString('parent::__construct();', $output);
316
    }
317
318
    public function testContainParentConstructor(): void
319
    {
320
        $class = Fixtures\EntityWithConstructor::class;
321
        $as = 'EntityProxy' . __LINE__;
322
323
        $r = new \ReflectionClass($class);
324
        $declaration = Declarations::createFromReflection($r, $as);
325
        $output = $this->make($r, $declaration);
326
        $output = ltrim($output, '<?php');
327
328
        $this->assertFalse(class_exists($declaration->class->getFullName()));
329
330
        eval($output);
331
332
        $this->assertStringContainsString('parent::__construct();', $output);
333
    }
334
335
    public function testPromiseMethods(): void
336
    {
337
        $class = Fixtures\Entity::class;
338
        $as = 'EntityProxy' . __LINE__;
339
340
        $r = new \ReflectionClass($class);
341
        $declaration = Declarations::createFromReflection($r, $as);
342
        $output = $this->make($r, $declaration);
343
        $output = ltrim($output, '<?php');
344
345
        $this->assertFalse(class_exists($declaration->class->getFullName()));
346
347
        eval($output);
348
349
        $i = new \ReflectionClass(PromiseInterface::class);
350
        foreach ($i->getMethods() as $method) {
351
            $this->assertStringContainsString("public function {$method->getName()}()", $output);
0 ignored issues
show
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
352
        }
353
    }
354
355
    public function testInheritedProperties(): void
356
    {
357
        $class = Fixtures\ChildEntity::class;
358
        $as = 'EntityProxy' . __LINE__;
359
360
        $reflection = new \ReflectionClass($class);
361
362
        $r = new \ReflectionClass($class);
363
        $declaration = Declarations::createFromReflection($r, $as);
364
        $output = $this->make($r, $declaration);
365
        $output = ltrim($output, '<?php');
366
367
        $this->assertFalse(class_exists($declaration->class->getFullName()));
368
369
        eval($output);
370
371
        $sourceProperties = [];
372
        foreach ($reflection->getProperties() as $property) {
373
            $sourceProperties[] = $property->getName();
374
        }
375
376
        $properties = [];
377
        foreach ($this->getDeclaration($r)->properties as $property) {
378
            $properties[] = $property;
379
        }
380
381
        foreach ($sourceProperties as $property) {
382
            $this->assertArrayNotHasKey($property, $properties, "Proxied class contains not expected `{$property}` property");
383
            $this->assertStringNotContainsString(" $property;", $output);
384
        }
385
386
        foreach ($properties as $property) {
387
            $this->assertArrayNotHasKey($property, $sourceProperties, "Origin class contains not expected `{$property}` property");
388
        }
389
    }
390
391
    public function testInheritedMethods(): void
392
    {
393
        $class = Fixtures\ChildEntity::class;
394
        $as = 'EntityProxy' . __LINE__;
395
396
        $reflection = new \ReflectionClass($class);
397
398
        $r = new \ReflectionClass($class);
399
        $declaration = Declarations::createFromReflection($r, $as);
400
        $output = $this->make($r, $declaration);
401
        $output = ltrim($output, '<?php');
402
403
        $this->assertFalse(class_exists($declaration->class->getFullName()));
404
405
        eval($output);
406
407
        $sourceMethods = [];
408
409
        //There're only public and protected methods inside
410
        foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
411
            $sourceMethods[$method->getName()] = $method->isPublic() ? 'public' : 'protected';
0 ignored issues
show
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
412
        }
413
414
        /** @var \PhpParser\Node\Stmt\ClassMethod[] $methods */
415
        $methods = [];
416
        foreach ($this->getDeclaration($r)->methods as $method) {
417
            $methods[$method->name->name] = $method;
418
        }
419
420
        foreach ($sourceMethods as $name => $accessor) {
421
            $this->assertArrayHasKey($name, $methods, "Proxy class does not contain expected `{$name}` method");
422
423
            if ($accessor === 'public') {
424
                $this->assertTrue($methods[$name]->isPublic(), "Proxied method `{$name}` expected to be public");
425
                $this->assertStringContainsString("public function {$name}()", $output);
426
            } else {
427
                $this->assertTrue($methods[$name]->isProtected(), "Proxied method `{$name}` expected to be protected");
428
                $this->assertStringContainsString("protected function {$name}()", $output);
429
            }
430
        }
431
432
        foreach ($methods as $name => $method) {
433
            $this->assertArrayHasKey($name, $sourceMethods, "Origin class does not contain expected `{$name}` method");
434
435
            if ($method->isPublic()) {
436
                $this->assertEquals('public', $sourceMethods[$name], "Proxied method `{$name}` expected to be public");
437
            } elseif ($method->isProtected()) {
438
                $this->assertEquals('protected', $sourceMethods[$name], "Proxied method `{$name}` expected to be public");
439
            } else {
440
                throw new \UnexpectedValueException("\"{$method->name->toString()}\" method not found");
441
            }
442
        }
443
    }
444
445
    private function getDeclaration(\ReflectionClass $class): Structure
446
    {
447
        return $this->extractor()->extract($class);
448
    }
449
450
    private function extractor(): Extractor
451
    {
452
        $container = new Container();
453
454
        return $container->get(Extractor::class);
455
    }
456
457
    /**
458
     * @param string $className
459
     * @param string $proxyFullName
460
     *
461
     * @return object
462
     */
463
    private function makeProxyObject(string $className, string $proxyFullName)
464
    {
465
        return $this->container->make($proxyFullName, ['role' => $className, 'scope' => []]);
466
    }
467
468
    private function make(\ReflectionClass $reflection, Declarations $declaration): string
469
    {
470
        return $this->proxyCreator()->make($reflection, $declaration);
471
    }
472
473
    private function proxyCreator(): ProxyPrinter
474
    {
475
        $container = new Container();
476
        $container->bind(PrettyPrinterAbstract::class, Standard::class);
477
478
        return $container->get(ProxyPrinter::class);
479
    }
480
481
    private function orm(): ORMInterface
482
    {
483
        $schema = (new Schema\Compiler())->compile(new Schema\Registry($this->dbal), [
484
            new Entities($this->locator),
485
            new Schema\Generator\ResetTables(),
486
            new Schema\Generator\GenerateRelations(),
487
            new Schema\Generator\ValidateEntities(),
488
            new Schema\Generator\RenderTables(),
489
            new Schema\Generator\RenderRelations(),
490
            new Schema\Generator\SyncTables(),
491
            new Schema\Generator\GenerateTypecast(),
492
        ]);
493
494
        return $this->withSchema(new \Cycle\ORM\Schema($schema));
495
    }
496
}