Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.00% |
92 / 115 |
|
63.64% |
7 / 11 |
CRAP | |
0.00% |
0 / 1 |
RunItemMethodAction | |
80.00% |
92 / 115 |
|
63.64% |
7 / 11 |
59.49 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAuthorized | |
56.25% |
9 / 16 |
|
0.00% |
0 / 1 |
18.37 | |||
__invoke | |
72.00% |
36 / 50 |
|
0.00% |
0 / 1 |
10.78 | |||
shouldReturnResource | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
getDisplayNameForMethod | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
6.07 | |||
getInputType | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getOutputType | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getPossibleActionResponseStatuses | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getDescription | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getTags | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getRouteAttributes | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | namespace Apie\Common\Actions; |
3 | |
4 | use Apie\Common\IntegrationTestLogger; |
5 | use Apie\Common\Other\LockUtil; |
6 | use Apie\Core\Actions\ActionResponse; |
7 | use Apie\Core\Actions\ActionResponseStatus; |
8 | use Apie\Core\Actions\ActionResponseStatusList; |
9 | use Apie\Core\Actions\ApieFacadeInterface; |
10 | use Apie\Core\Actions\MethodActionInterface; |
11 | use Apie\Core\BoundedContext\BoundedContextId; |
12 | use Apie\Core\Context\ApieContext; |
13 | use Apie\Core\ContextConstants; |
14 | use Apie\Core\Entities\EntityInterface; |
15 | use Apie\Core\Exceptions\EntityNotFoundException; |
16 | use Apie\Core\Exceptions\InvalidTypeException; |
17 | use Apie\Core\IdentifierUtils; |
18 | use Apie\Core\Lists\StringList; |
19 | use Apie\Core\TypeUtils; |
20 | use Apie\Core\Utils\EntityUtils; |
21 | use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException; |
22 | use Apie\Serializer\Exceptions\ValidationException; |
23 | use Exception; |
24 | use LogicException; |
25 | use ReflectionClass; |
26 | use ReflectionException; |
27 | use ReflectionMethod; |
28 | use ReflectionNamedType; |
29 | |
30 | /** |
31 | * Runs a method from a resource (and persist resource afterwards). |
32 | */ |
33 | final class RunItemMethodAction implements MethodActionInterface |
34 | { |
35 | public function __construct(private readonly ApieFacadeInterface $apieFacade) |
36 | { |
37 | } |
38 | |
39 | public static function isAuthorized(ApieContext $context, bool $runtimeChecks, bool $throwError = false): bool |
40 | { |
41 | $refl = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME, $throwError)); |
42 | $methodName = $context->getContext(ContextConstants::METHOD_NAME, $throwError); |
43 | $method = new ReflectionMethod( |
44 | $context->getContext(ContextConstants::METHOD_CLASS, $throwError), |
45 | $methodName |
46 | ); |
47 | if (EntityUtils::isPolymorphicEntity($refl) && $runtimeChecks && $context->hasContext(ContextConstants::RESOURCE) &&!$method->isStatic()) { |
48 | $refl = new ReflectionClass($context->getContext(ContextConstants::RESOURCE, $throwError)); |
49 | if (!$refl->hasMethod($methodName)) { |
50 | if ($throwError) { |
51 | throw new LogicException('Method ' . $methodName . ' does not exist on this entity'); |
52 | } |
53 | return false; |
54 | } |
55 | $method = $refl->getMethod($methodName); |
56 | } |
57 | if (!$context->appliesToContext($refl, $runtimeChecks, $throwError ? new LogicException('Class access is not allowed!') : null)) { |
58 | return false; |
59 | } |
60 | return $context->appliesToContext($method, $runtimeChecks, $throwError ? new LogicException('Class method is not allowed') : null); |
61 | } |
62 | |
63 | /** |
64 | * @param array<string|int, mixed> $rawContents |
65 | */ |
66 | public function __invoke(ApieContext $context, array $rawContents): ActionResponse |
67 | { |
68 | $context->withContext(ContextConstants::APIE_ACTION, __CLASS__)->checkAuthorization(); |
69 | $resourceClass = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME)); |
70 | if (!$resourceClass->implementsInterface(EntityInterface::class)) { |
71 | throw new InvalidTypeException($resourceClass->name, 'EntityInterface'); |
72 | } |
73 | $method = new ReflectionMethod( |
74 | $context->getContext(ContextConstants::METHOD_CLASS), |
75 | $context->getContext(ContextConstants::METHOD_NAME) |
76 | ); |
77 | $lock = LockUtil::createLock( |
78 | $context, |
79 | [ContextConstants::BOUNDED_CONTEXT_ID, ContextConstants::RESOURCE_NAME, ContextConstants::RESOURCE_ID], |
80 | write: true |
81 | ); |
82 | try { |
83 | if ($method->isStatic()) { |
84 | $resource = null; |
85 | } else { |
86 | $id = $context->getContext(ContextConstants::RESOURCE_ID); |
87 | try { |
88 | $resource = $this->apieFacade->find( |
89 | IdentifierUtils::idStringToIdentifier($id, $context), |
90 | new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID)) |
91 | ); |
92 | } catch (InvalidStringForValueObjectException|EntityNotFoundException $error) { |
93 | IntegrationTestLogger::logException($error); |
94 | return ActionResponse::createClientError($this->apieFacade, $context, $error); |
95 | } |
96 | $context = $context->withContext(ContextConstants::RESOURCE, $resource); |
97 | // polymorphic relation, so could be the incorrect declared method |
98 | if (!$method->getDeclaringClass()->isInstance($resource)) { |
99 | try { |
100 | $method = (new ReflectionClass($resource))->getMethod($method->name); |
101 | } catch (ReflectionException $methodError) { |
102 | $error = new Exception( |
103 | sprintf('Resource "%s" does not support "%s"!', $id, $method->name), |
104 | 0, |
105 | $methodError |
106 | ); |
107 | throw ValidationException::createFromArray(['' => $error]); |
108 | } |
109 | } |
110 | } |
111 | |
112 | $result = $this->apieFacade->denormalizeOnMethodCall( |
113 | $rawContents, |
114 | $resource, |
115 | $method, |
116 | $context |
117 | ); |
118 | if ($resource !== null) { |
119 | if (!$lock->isAcquired()) { |
120 | throw new \LogicException('Lock was released before modification was finished!'); |
121 | } |
122 | $resource = $this->apieFacade->persistExisting( |
123 | $resource, |
124 | new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID)) |
125 | ); |
126 | } |
127 | } finally { |
128 | $lock->release(); |
129 | } |
130 | if (self::shouldReturnResource($method)) { |
131 | $result = $resource; |
132 | } |
133 | return ActionResponse::createRunSuccess($this->apieFacade, $context, $result, $resource); |
134 | } |
135 | |
136 | /** |
137 | * Returns true if we should not return the return value of the method, but should return the return value of the resource. |
138 | * This is the case if: |
139 | * - The method returns void |
140 | * - The method call starts with 'add' or 'remove' and has arguments. |
141 | */ |
142 | public static function shouldReturnResource(ReflectionMethod $method): bool |
143 | { |
144 | $returnType = $method->getReturnType(); |
145 | if ($returnType instanceof ReflectionNamedType && 'void' === $returnType->getName()) { |
146 | return true; |
147 | } |
148 | if ($method->getNumberOfParameters() === 0) { |
149 | return false; |
150 | } |
151 | |
152 | return str_starts_with($method->name, 'add') || str_starts_with($method->name, 'remove'); |
153 | } |
154 | |
155 | /** |
156 | * Returns a string how we should display the method. For example we remove 'add' or 'remove' from the string. |
157 | */ |
158 | public static function getDisplayNameForMethod(ReflectionMethod $method): string |
159 | { |
160 | if ($method->getNumberOfParameters() > 0) { |
161 | if (str_starts_with($method->name, 'remove')) { |
162 | return lcfirst(substr($method->name, strlen('remove'))); |
163 | } |
164 | if (str_starts_with($method->name, 'add')) { |
165 | return lcfirst(substr($method->name, strlen('add'))); |
166 | } |
167 | } |
168 | if (str_starts_with($method->name, 'get') && TypeUtils::couldBeAStream($method->getReturnType())) { |
169 | return lcfirst(substr($method->name, strlen('get'))); |
170 | } |
171 | return $method->name; |
172 | } |
173 | |
174 | /** @param ReflectionClass<object> $class */ |
175 | public static function getInputType(ReflectionClass $class, ?ReflectionMethod $method = null): ReflectionMethod |
176 | { |
177 | assert($method instanceof ReflectionMethod); |
178 | return $method; |
179 | } |
180 | |
181 | /** @param ReflectionClass<object> $class */ |
182 | public static function getOutputType(ReflectionClass $class, ?ReflectionMethod $method = null): ReflectionMethod|ReflectionClass |
183 | { |
184 | assert($method instanceof ReflectionMethod); |
185 | if (RunItemMethodAction::shouldReturnResource($method)) { |
186 | return $class; |
187 | } |
188 | return $method; |
189 | } |
190 | |
191 | public static function getPossibleActionResponseStatuses(?ReflectionMethod $method = null): ActionResponseStatusList |
192 | { |
193 | assert($method instanceof ReflectionMethod); |
194 | $list = [ActionResponseStatus::SUCCESS]; |
195 | |
196 | if (!empty($method->getParameters())) { |
197 | $list[] = ActionResponseStatus::CLIENT_ERROR; |
198 | } |
199 | if (!$method->isStatic()) { |
200 | $list[] = ActionResponseStatus::NOT_FOUND; |
201 | } |
202 | return new ActionResponseStatusList($list); |
203 | } |
204 | |
205 | /** |
206 | * @param ReflectionClass<object> $class |
207 | */ |
208 | public static function getDescription(ReflectionClass $class, ?ReflectionMethod $method = null): string |
209 | { |
210 | assert($method instanceof ReflectionMethod); |
211 | $name = self::getDisplayNameForMethod($method); |
212 | if (str_starts_with($method->name, 'add')) { |
213 | return 'Adds ' . $name . ' to ' . $class->getShortName(); |
214 | } |
215 | if (str_starts_with($method->name, 'remove')) { |
216 | return 'Removes ' . $name . ' from ' . $class->getShortName(); |
217 | } |
218 | return 'Runs method ' . $name . ' on a ' . $class->getShortName() . ' with a specific id'; |
219 | } |
220 | |
221 | /** |
222 | * @param ReflectionClass<object> $class |
223 | */ |
224 | public static function getTags(ReflectionClass $class, ?ReflectionMethod $method = null): StringList |
225 | { |
226 | $className = $class->getShortName(); |
227 | $declared = $method ? $method->getDeclaringClass()->getShortName() : $className; |
228 | if ($className !== $declared) { |
229 | return new StringList([$className, $declared, 'action']); |
230 | } |
231 | return new StringList([$className, 'action']); |
232 | } |
233 | |
234 | /** |
235 | * @param ReflectionClass<object> $class |
236 | */ |
237 | public static function getRouteAttributes(ReflectionClass $class, ?ReflectionMethod $method = null): array |
238 | { |
239 | return |
240 | [ |
241 | ContextConstants::GET_OBJECT => true, |
242 | ContextConstants::RESOURCE_METHOD => true, |
243 | ContextConstants::RESOURCE_NAME => $class->name, |
244 | ContextConstants::METHOD_CLASS => $method->getDeclaringClass()->name, |
245 | ContextConstants::METHOD_NAME => $method->name, |
246 | ContextConstants::DISPLAY_FORM => true, |
247 | ]; |
248 | } |
249 | } |