Compiler::compile()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 2
b 0
f 0
nc 4
nop 2
dl 0
loc 22
ccs 11
cts 11
cp 1
crap 4
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Aop;
6
7
use Doctrine\Common\Annotations\AnnotationException;
8
use ParseError;
9
use Ray\Aop\Exception\CompilationFailedException;
10
use Ray\Aop\Exception\NotWritableException;
11
use ReflectionClass;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Ray\Aop\ReflectionClass. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
12
13
use function array_keys;
14
use function assert;
15
use function class_exists;
16
use function file_exists;
17
use function file_put_contents;
18
use function is_writable;
19
use function method_exists;
20
use function sprintf;
21
use function str_replace;
22
23
use const PHP_VERSION_ID;
24
25
final class Compiler implements CompilerInterface
26
{
27 24
    /** @var string */
28
    public $classDir;
29 24
30 1
    /** @throws AnnotationException */
31
    public function __construct(string $classDir)
32 24
    {
33 24
        if (! is_writable($classDir)) {
34 24
            throw new NotWritableException($classDir);
35 24
        }
36 24
37
        $this->classDir = $classDir;
38 24
    }
39
40 1
    /**
41
     * {@inheritDoc}
42 1
     *
43
     * @template T of object
44
     */
45 1
    public function newInstance(string $class, array $args, BindInterface $bind)
46
    {
47 1
        $compiledClass = $this->compile($class, $bind);
48 1
        assert(class_exists($compiledClass));
49
        $instance = (new ReflectionClass($compiledClass))->newInstanceArgs($args);
50
        if (isset($instance->bindings)) {
51
            $instance->bindings = $bind->getBindings();
52
        }
53 10
54
        assert($instance instanceof $class);
55 10
56 10
        return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance also could return the type object which includes types incompatible with the return type mandated by Ray\Aop\CompilerInterface::newInstance() of Ray\Aop\T. Consider adding a type-check to rule them out.
Loading history...
57 10
    }
58
59 10
    /**
60
     * {@inheritDoc}
61
     */
62
    public function compile(string $class, BindInterface $bind): string
63
    {
64
        if ($this->hasNoBinding($class, $bind)) {
65 18
            return $class;
66
        }
67 18
68 1
        $className = new AopPostfixClassName($class, (string) $bind, $this->classDir);
69
        if (class_exists($className->fqn, false)) {
70 17
            return $className->fqn;
71 17
        }
72 7
73
        try {
74 10
            $this->requireFile($className, new ReflectionClass($class), $bind);
75 10
            // @codeCoverageIgnoreStart
76
        } catch (ParseError $e) {
77
            $msg = sprintf('class:%s Compilation failed in Ray.Aop. This is most likely a bug in Ray.Aop, please report it to the issue. https://github.com/ray-di/Ray.Aop/issues', $class);
78 1
79
            throw new CompilationFailedException($msg);
80 1
            // @codeCoverageIgnoreEnd
81
        }
82 9
83
        return $className->fqn;
84 9
    }
85
86
    /** @param class-string $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
87 18
    private function hasNoBinding(string $class, BindInterface $bind): bool
88
    {
89 18
        $hasMethod = $this->hasBoundMethod($class, $bind);
90
91 18
        return ! $bind->getBindings() && ! $hasMethod;
92
    }
93
94 17
    /** @param class-string $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
95
    private function hasBoundMethod(string $class, BindInterface $bind): bool
96 17
    {
97
        $bindingMethods = array_keys($bind->getBindings());
98 17
        $hasMethod = false;
99
        foreach ($bindingMethods as $bindingMethod) {
100
            if (! method_exists($class, $bindingMethod)) {
101 18
                continue;
102
            }
103 18
104 18
            $hasMethod = true;
105 18
        }
106 17
107 17
        return $hasMethod;
108
    }
109
110
    /** @param ReflectionClass<object> $sourceClass */
111 18
    private function requireFile(AopPostfixClassName $className, ReflectionClass $sourceClass, BindInterface $bind): void
112
    {
113
        $file = $this->getFileName($className->fqn);
114 9
        if (! file_exists($file)) {
115
            $code = new AopCode(new MethodSignatureString(PHP_VERSION_ID));
116 9
            $aopCode = $code->generate($sourceClass, $bind, $className->postFix);
117 9
            file_put_contents($file, $aopCode);
118
        }
119 9
120 9
        require_once $file;
121
        class_exists($className->fqn); // ensure class is created
122
    }
123
124
    private function getFileName(string $aopClassName): string
125
    {
126
        $flatName = str_replace('\\', '_', $aopClassName);
127
128
        return sprintf('%s/%s.php', $this->classDir, $flatName);
129
    }
130
}
131