Passed
Push — master ( a4f7f2...d3cc39 )
by Melech
04:09
created

checkParameterForEntity()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 16
rs 8.8333
cc 7
nc 3
nop 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Orm\Middleware;
15
16
use Valkyrja\Container\Contract\Container;
17
use Valkyrja\Dispatcher\Data\Contract\ClassDispatch;
18
use Valkyrja\Http\Message\Enum\StatusCode;
19
use Valkyrja\Http\Message\Request\Contract\ServerRequest;
20
use Valkyrja\Http\Message\Response\Contract\Response;
21
use Valkyrja\Http\Middleware\Contract\RouteMatchedMiddleware;
22
use Valkyrja\Http\Middleware\Handler\Contract\RouteMatchedHandler;
23
use Valkyrja\Http\Routing\Data\Contract\Parameter;
24
use Valkyrja\Http\Routing\Data\Contract\Route;
25
use Valkyrja\Orm\Contract\Manager;
26
use Valkyrja\Orm\Data\EntityCast;
27
use Valkyrja\Orm\Data\Value;
28
use Valkyrja\Orm\Data\Where;
29
use Valkyrja\Orm\Entity\Contract\Entity;
30
use Valkyrja\View\Factory\Contract\ResponseFactory;
31
32
use function is_a;
33
use function is_int;
34
use function is_string;
35
36
/**
37
 * Class EntityRouteMatchedMiddleware.
38
 *
39
 * @author Melech Mizrachi
40
 */
41
class EntityRouteMatchedMiddleware implements RouteMatchedMiddleware
42
{
43
    /**
44
     * The errors template directory.
45
     *
46
     * @var string
47
     */
48
    protected string $errorsTemplateDir = 'errors';
49
50
    public function __construct(
51
        protected Container $container,
52
        protected Manager $orm,
53
        protected ResponseFactory $responseFactory,
54
    ) {
55
    }
56
57
    /**
58
     * @inheritDoc
59
     */
60
    public function routeMatched(ServerRequest $request, Route $route, RouteMatchedHandler $handler): Route|Response
61
    {
62
        $routeOrResponse = $this->checkRouteForEntities($route);
63
64
        if ($routeOrResponse instanceof Response) {
65
            return $routeOrResponse;
66
        }
67
68
        return $handler->routeMatched($request, $routeOrResponse);
69
    }
70
71
    /**
72
     * Check route for entities.
73
     *
74
     * @param Route $route The route
75
     *
76
     * @return Response|Route
77
     */
78
    protected function checkRouteForEntities(Route $route): Response|Route
79
    {
80
        $parameters = $route->getParameters();
81
        $dispatch   = $route->getDispatch();
82
83
        if ($parameters !== [] && $dispatch instanceof ClassDispatch) {
84
            $arguments    = $dispatch->getArguments() ?? [];
85
            $dependencies = $dispatch->getDependencies() ?? [];
86
87
            // Iterate through the params
88
            foreach ($parameters as $index => $parameter) {
89
                $response = $this->checkParameterForEntity((int) $index, $parameter, $dependencies, $arguments);
90
91
                if ($response !== null) {
92
                    return $response;
93
                }
94
            }
95
96
            $route = $route->withDispatch($dispatch->withArguments($arguments));
97
            $route = $route->withDispatch($dispatch->withDependencies($dependencies));
98
        }
99
100
        return $route;
101
    }
102
103
    /**
104
     * Check a route's parameters for an entity.
105
     *
106
     * @param int                     $index        The index
107
     * @param Parameter               $parameter    The parameter
108
     * @param class-string[]          $dependencies The route dependencies
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...
109
     * @param array<array-key, mixed> $arguments    The arguments
110
     *
111
     * @return Response|null
112
     */
113
    protected function checkParameterForEntity(int $index, Parameter $parameter, array &$dependencies, array &$arguments): Response|null
114
    {
115
        $type = $parameter->getCast()->type ?? null;
116
117
        if ($type !== null && is_a($type, Entity::class, true)) {
118
            $match = $arguments[$index];
119
120
            if ((is_string($match) && $match !== '') || is_int($match) || $match instanceof Entity) {
121
                /** @var Entity|non-empty-string|int $match */
122
                return $this->findAndSetEntityFromParameter($parameter, $type, $dependencies, $match);
123
            }
124
125
            return $this->getBadRequestResponse($type, $match);
126
        }
127
128
        return null;
129
    }
130
131
    /**
132
     * Try to find and set a route's entity dependency.
133
     *
134
     * @param Parameter                   $parameter    The parameter
135
     * @param class-string<Entity>        $entityName   The entity class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Entity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Entity>.
Loading history...
136
     * @param class-string[]              $dependencies The dependencies
137
     * @param Entity|non-empty-string|int $value        The value
138
     *
139
     * @return Response|null
140
     */
141
    protected function findAndSetEntityFromParameter(
142
        Parameter $parameter,
143
        string $entityName,
144
        array &$dependencies,
145
        Entity|string|int &$value
146
    ): Response|null {
147
        if ($value instanceof Entity) {
148
            return null;
149
        }
150
151
        // Attempt to get the entity from the ORM repository
152
        $entity = $this->findEntityFromParameter($parameter, $entityName, $value);
153
154
        if ($entity === null) {
155
            return $this->getNotFoundResponse($entityName, $value);
156
        }
157
158
        // Replace the route match with this entity
159
        /** @param-out Entity $value */
160
        $value = $entity;
161
162
        $updatedDependencies = [];
163
164
        foreach ($dependencies as $dependency) {
165
            if ($dependency !== $entityName) {
166
                $updatedDependencies[] = $dependency;
167
            }
168
        }
169
170
        $dependencies = $updatedDependencies;
171
172
        return null;
173
    }
174
175
    /**
176
     * Try to find a route's entity dependency.
177
     *
178
     * @param Parameter            $parameter  The parameter
179
     * @param class-string<Entity> $entityName The entity class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Entity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Entity>.
Loading history...
180
     * @param non-empty-string|int $value      The value
181
     *
182
     * @return Entity|null
183
     */
184
    protected function findEntityFromParameter(
185
        Parameter $parameter,
186
        string $entityName,
187
        string|int $value
188
    ): Entity|null {
189
        $cast          = $parameter->getCast();
190
        $repository    = $this->orm->createRepository($entityName);
191
        $field         = null;
192
        $relationships = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $relationships is dead and can be removed.
Loading history...
193
194
        if ($cast instanceof EntityCast) {
195
            $relationships = $cast->relationships ?? [];
196
            $field         = $cast->column;
197
        }
198
199
        // If there is a field specified to use
200
        if ($field !== null && $field !== '') {
201
            return $repository->findBy(new Where(new Value(name: $field, value: $value)));
202
        }
203
204
        return $repository->find($value);
205
    }
206
207
    /**
208
     * Response for when the entity was not found with the given value.
209
     *
210
     * @param class-string<Entity> $entity The entity not found
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Entity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Entity>.
Loading history...
211
     * @param mixed                $value  [optional] The value used to check for the entity
212
     *
213
     * @return Response
214
     */
215
    protected function getNotFoundResponse(string $entity, mixed $value): Response
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
    protected function getNotFoundResponse(string $entity, /** @scrutinizer ignore-unused */ mixed $value): Response

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $entity is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
    protected function getNotFoundResponse(/** @scrutinizer ignore-unused */ string $entity, mixed $value): Response

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
216
    {
217
        return $this->responseFactory
218
            ->createResponseFromView(
219
                template: "$this->errorsTemplateDir/" . ((string) StatusCode::NOT_FOUND->value),
220
                statusCode: StatusCode::NOT_FOUND,
221
            );
222
    }
223
224
    /**
225
     * Response for when bad data has been provided to match for the entity.
226
     *
227
     * @param class-string<Entity> $entity The entity with bad data
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Entity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Entity>.
Loading history...
228
     * @param mixed                $value  [optional] The bad data value
229
     *
230
     * @return Response
231
     */
232
    protected function getBadRequestResponse(string $entity, mixed $value): Response
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

232
    protected function getBadRequestResponse(string $entity, /** @scrutinizer ignore-unused */ mixed $value): Response

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $entity is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

232
    protected function getBadRequestResponse(/** @scrutinizer ignore-unused */ string $entity, mixed $value): Response

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
233
    {
234
        return $this->responseFactory
235
            ->createResponseFromView(
236
                template: "$this->errorsTemplateDir/" . ((string) StatusCode::BAD_REQUEST->value),
237
                statusCode: StatusCode::BAD_REQUEST,
238
            );
239
    }
240
}
241