Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.44% covered (success)
93.44%
57 / 61
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetItemAction
93.44% covered (success)
93.44%
57 / 61
70.00% covered (warning)
70.00%
7 / 10
25.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAuthorized
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
7.23
 __invoke
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 ifNoneMatchIncludes
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 getInputType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutputType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPossibleActionResponseStatuses
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteAttributes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Common\Actions;
3
4use Apie\Common\Events\ApieResourceRead;
5use Apie\Common\Exceptions\EntityNotModifiedException;
6use Apie\Common\Other\LockUtil;
7use Apie\Core\Actions\ActionInterface;
8use Apie\Core\Actions\ActionResponse;
9use Apie\Core\Actions\ActionResponseStatus;
10use Apie\Core\Actions\ActionResponseStatusList;
11use Apie\Core\Actions\ApieFacadeInterface;
12use Apie\Core\Attributes\Description;
13use Apie\Core\BoundedContext\BoundedContextId;
14use Apie\Core\Context\ApieContext;
15use Apie\Core\ContextConstants;
16use Apie\Core\Datalayers\ApieDatalayer;
17use Apie\Core\Datalayers\DataLayerMetadata;
18use Apie\Core\Entities\EntityInterface;
19use Apie\Core\Exceptions\InvalidTypeException;
20use Apie\Core\IdentifierUtils;
21use Apie\Core\Lists\PermissionList;
22use Apie\Core\Lists\StringList;
23use Apie\Core\Permissions\PermissionInterface;
24use Apie\Core\Permissions\RequiresPermissionsInterface;
25use Apie\Core\Utils\EntityUtils;
26use LogicException;
27use Psr\Http\Message\ServerRequestInterface;
28use ReflectionClass;
29
30/**
31 * Action to get a single item resource.
32 */
33final class GetItemAction implements ActionInterface
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        $resource = $context->getContext(ContextConstants::RESOURCE, false);
43        if ($resource instanceof RequiresPermissionsInterface) {
44            $requiredPermissions = $resource->getRequiredPermissions();
45            $user = $context->getContext(ContextConstants::AUTHENTICATED_USER, false);
46            if ($user instanceof PermissionInterface) {
47                $hasPermisions = $user->getPermissionIdentifiers();
48                return $hasPermisions->hasOverlap($requiredPermissions);
49            }
50            return $requiredPermissions->hasOverlap(new PermissionList(['']));
51        }
52        if (EntityUtils::isPolymorphicEntity($refl) && $runtimeChecks && $resource) {
53            $refl = new ReflectionClass($resource);
54        }
55        return $context->appliesToContext($refl, $runtimeChecks, $throwError ? new LogicException('Operation is not allowed') : null);
56    }
57
58    /**
59     * @param array<string|int, mixed> $rawContents
60     */
61    public function __invoke(ApieContext $context, array $rawContents): ActionResponse
62    {
63        $context->withContext(ContextConstants::APIE_ACTION, __CLASS__)->checkAuthorization();
64        $resourceClass = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME));
65        $id = $context->getContext(ContextConstants::RESOURCE_ID);
66        if (!$resourceClass->implementsInterface(EntityInterface::class)) {
67            throw new InvalidTypeException($resourceClass->name, 'EntityInterface');
68        }
69        $lock = LockUtil::createLock(
70            $context,
71            [ContextConstants::BOUNDED_CONTEXT_ID, ContextConstants::RESOURCE_NAME, ContextConstants::RESOURCE_ID]
72        );
73        try {
74            $identifier = IdentifierUtils::idStringToIdentifier($id, $context);
75            $boundedContextId = new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID));
76            $request = $context->getContext(ServerRequestInterface::class, false);
77            if (
78                $context->getContext(ContextConstants::REST_API, false)
79                && $request instanceof ServerRequestInterface
80                && $request->hasHeader('If-None-Match')
81            ) {
82                $metadata = $context->getContext(ApieDatalayer::class)
83                    ->getMetadata($identifier, $boundedContextId);
84                $context = $context->withContext(DataLayerMetadata::class, $metadata);
85                if ($this->ifNoneMatchIncludes($request->getHeaderLine('If-None-Match'), $metadata->etag)) {
86                    throw new EntityNotModifiedException('"' . $metadata->etag . '"');
87                }
88            }
89            $result = $this->apieFacade->find($identifier, $boundedContextId);
90            $context = $context->withContext(ContextConstants::RESOURCE, $result);
91            $context->withContext(ContextConstants::APIE_ACTION, __CLASS__)->checkAuthorization();
92        } finally {
93            $lock->release();
94        }
95        $context->dispatchEvent(new ApieResourceRead($result, $context));
96        return ActionResponse::createRunSuccess($this->apieFacade, $context, $result, $result);
97    }
98
99    private function ifNoneMatchIncludes(string $ifNoneMatch, string $etag): bool
100    {
101        preg_match_all('/(?:W\/)?"[^"]*"|\*/', $ifNoneMatch, $matches);
102        $currentEtag = '"' . $etag . '"';
103        foreach ($matches[0] as $candidate) {
104            if ($candidate === '*' || preg_replace('/^W\//', '', $candidate) === $currentEtag) {
105                return true;
106            }
107        }
108
109        return false;
110    }
111
112    /**
113     * @return ReflectionClass<EntityInterface>
114     */
115    public static function getInputType(ReflectionClass $class): ReflectionClass
116    {
117        return $class;
118    }
119
120    /**
121     * @return ReflectionClass<EntityInterface>
122     */
123    public static function getOutputType(ReflectionClass $class): ReflectionClass
124    {
125        return $class;
126    }
127
128    public static function getPossibleActionResponseStatuses(): ActionResponseStatusList
129    {
130        return new ActionResponseStatusList([
131            ActionResponseStatus::SUCCESS,
132            ActionResponseStatus::AUTHORIZATION_ERROR,
133            ActionResponseStatus::NOT_FOUND
134        ]);
135    }
136
137    public static function getDescription(ReflectionClass $class): string
138    {
139        $description = 'Gets a resource of ' . $class->getShortName() . ' with a specific id';
140        foreach ($class->getAttributes(Description::class) as $attribute) {
141            $description .= '. ' . $attribute->newInstance()->description;
142        }
143
144        return $description;
145    }
146    
147    public static function getTags(ReflectionClass $class): StringList
148    {
149        return new StringList([$class->getShortName(), 'resource']);
150    }
151
152    public static function getRouteAttributes(ReflectionClass $class): array
153    {
154        return [
155            ContextConstants::GET_OBJECT => true,
156            ContextConstants::RESOURCE_NAME => $class->name,
157        ];
158    }
159}