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

tests/Promise/ProxyPrinterTest.php (2 issues)

Labels
Severity

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()) {
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
}