Passed
Push — master ( 909d6e...b27c7b )
by Paweł
08:53 queued 06:51
created

Route::serialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 8
rs 10
1
<?php
2
3
namespace pjpawel\LightApi\Route;
4
5
use Exception;
6
use pjpawel\LightApi\Container\ContainerLoader;
7
use pjpawel\LightApi\Container\ContainerNotFoundException;
8
use pjpawel\LightApi\Container\LazyService\LazyServiceInterface;
9
use pjpawel\LightApi\Exception\KernelException;
10
use pjpawel\LightApi\Exception\ProgrammerException;
11
use pjpawel\LightApi\Http\Exception\HttpException;
12
use pjpawel\LightApi\Http\Request;
13
use pjpawel\LightApi\Http\Response;
14
use pjpawel\LightApi\Http\ResponseStatus;
15
use Psr\Container\ContainerExceptionInterface;
16
use Psr\Container\NotFoundExceptionInterface;
17
use ReflectionClass;
18
use ReflectionNamedType;
19
20
class Route
21
{
22
23
    private string $className;
24
    private string $methodName;
25
    public string $path;
26
    public array $httpMethods;
27
    public ?string $regexPath = null;
28
29
    public function __construct(string $className, string $methodName, string $path, array $httpMethods)
30
    {
31
        $this->className = $className;
32
        $this->methodName = $methodName;
33
        $this->path = $path;
34
        $this->httpMethods = $httpMethods;
35
    }
36
37
    /**
38
     * @return void
39
     * @throws \ReflectionException
40
     * @throws Exception
41
     */
42
    public function makeRegexPath(): void
43
    {
44
        $this->regexPath = $this->path;
45
        preg_match_all('/\{\w+\}/', $this->path, $pathParameters, PREG_SET_ORDER);
46
        $this->regexPath = '/^' . str_replace('/', '\\/', $this->regexPath) . '$/';
47
        if (empty($pathParameters)) {
48
            return;
49
        }
50
        $pathParameters = array_map(
51
            function($param) {
52
                return substr($param[0], 1, strlen($param[0])-2);
53
            },
54
            $pathParameters
55
        );
56
        $reflectionMethod = (new ReflectionClass($this->className))->getMethod($this->methodName);
57
        foreach ($reflectionMethod->getParameters() as $parameter) {
58
            if (($parameterIndex = array_search($parameter->getName(), $pathParameters, true)) !== false) {
59
                /** @var \ReflectionNamedType $parameterType */
60
                $parameterType = $parameter->getType();
61
                if ($parameterType->isBuiltin()) {
62
                    $parameterTypeName = $parameterType->getName();
63
                    if ($parameterTypeName == 'string') {
64
                        $this->regexPath = str_replace('{' . $pathParameters[$parameterIndex] . '}', '(\w+)', $this->regexPath);
65
                    } elseif ($parameterTypeName == 'int') {
66
                        $this->regexPath = str_replace('{' . $pathParameters[$parameterIndex] . '}', '(\d+)', $this->regexPath);
67
                    } else {
68
                        throw new ProgrammerException(sprintf('Not supported builtin type %s for parameter %s', $parameterTypeName, $parameter->getName()));
69
                    }
70
                    //$this->pathParams[] = $pathParameters[$parameterIndex];
71
                    unset($pathParameters[$parameterIndex]);
72
                } else {
73
                    throw new ProgrammerException('Invalid parameter type ' . $parameterType->getName());
74
                }
75
            }
76
        }
77
        if (!empty($pathParameters)) {
78
            throw new ProgrammerException('Missing value for :' . implode(', ', $pathParameters));
79
        }
80
    }
81
82
    /**
83
     * @param ContainerLoader $container
84
     * @param Request $request
85
     * @return Response
86
     * @throws \ReflectionException
87
     * @throws \pjpawel\LightApi\Container\ContainerNotFoundException
88
     * @throws Exception
89
     */
90
    public function execute(ContainerLoader $container, Request $request): Response
91
    {
92
        try {
93
            $reflectionClass = new ReflectionClass($this->className);
94
            $constructor = $reflectionClass->getConstructor();
95
            $constructorArgs = [];
96
            if ($constructor != null) {
97
                $constructorArgs = $this->loadArguments($constructor->getParameters(), $container, $request); //, false
98
            }
99
            $class = $reflectionClass->newInstanceArgs($constructorArgs);
100
101
            $reflectionMethod = $reflectionClass->getMethod($this->methodName);
102
            $args = $this->loadArguments($reflectionMethod->getParameters(), $container, $request); //, true
103
104
            if (is_subclass_of($class, LazyServiceInterface::class)) {
105
                $container->prepareContainerLocator($class::getAllServices());
106
                $class->setContainer($container);
107
            }
108
109
            $result = $reflectionMethod->invokeArgs($class, $args);
110
            if ($result instanceof Response) {
111
                return $result;
112
            } elseif (is_array($result)) {
113
                return new Response($result);
114
            } elseif (is_string($result)) {
115
                return new Response($result);
116
            } else {
117
                throw new ProgrammerException('Invalid object type of response: ' . var_export($result, true));
118
            }
119
        } catch (HttpException $httpExc) {
120
            return new Response($httpExc->getMessage(), ResponseStatus::from($httpExc->getCode()));
121
        } catch (Exception $exc) {
122
            return new Response('Internal server error occurred', ResponseStatus::INTERNAL_SERVER_ERROR);
123
        }
124
    }
125
126
    /**
127
     * @param \ReflectionParameter[] $parameters
128
     * @param ContainerLoader $container
129
     * @param Request $request
130
     * @return array
131
     * @throws KernelException
132
     * @throws ContainerExceptionInterface
133
     * @throws NotFoundExceptionInterface
134
     * @throws \ReflectionException
135
     * @throws ContainerNotFoundException
136
     */
137
    private function loadArguments(array $parameters, ContainerLoader $container, Request $request): array
138
    {
139
        $pathParameters = [];
140
        if ($this->regexPath !== null && preg_match($this->regexPath, $request->path, $pathParameters) === false) {
141
            throw new KernelException('Cannot math regex path to requested path while loading parameters');
142
        }
143
        array_shift($pathParameters);
144
        $pathParameterIndex = 0;
145
        $args = [];
146
        foreach ($parameters as $parameter) {
147
            /** @var ReflectionNamedType $type */
148
            $type = $parameter->getType();
149
            if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
150
                if ($type->getName() == Request::class) {
151
                    $args[] = $request;
152
                } elseif ($type->getName() == Response::class) {
153
                    $args[] = new Response();
154
                } else {
155
                    $args[] = $container->get($type->getName());
156
                }
157
            } else {
158
                if ($type->getName() == 'int') {
159
                    $args[] = (int) $pathParameters[$pathParameterIndex];
160
                } else {
161
                    $args[] = $pathParameters[$pathParameterIndex];
162
                }
163
                $pathParameterIndex++;
164
            }
165
        }
166
        return $args;
167
    }
168
169
    public function __serialize(): array
170
    {
171
        return [
172
            $this->className,
173
            $this->methodName,
174
            $this->path,
175
            $this->httpMethods,
176
            $this->regexPath
177
        ];
178
    }
179
180
    public function __unserialize(array $data): void
181
    {
182
        $this->className = $data[0];
183
        $this->methodName = $data[1];
184
        $this->path = $data[2];
185
        $this->httpMethods = $data[3];
186
        $this->regexPath = $data[4];
187
    }
188
}