Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.51% covered (warning)
85.51%
59 / 69
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ModifyObjectAction
85.51% covered (warning)
85.51%
59 / 69
66.67% covered (warning)
66.67%
6 / 9
26.90
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
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
11.30
 __invoke
88.10% covered (warning)
88.10%
37 / 42
0.00% covered (danger)
0.00%
0 / 1
10.17
 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
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Common\Actions;
3
4use Apie\Common\Events\ApieResourceModified;
5use Apie\Common\Exceptions\EntityEtagConflictException;
6use Apie\Common\IntegrationTestLogger;
7use Apie\Common\Other\LockUtil;
8use Apie\Core\Actions\ActionInterface;
9use Apie\Core\Actions\ActionResponse;
10use Apie\Core\Actions\ActionResponseStatus;
11use Apie\Core\Actions\ActionResponseStatusList;
12use Apie\Core\Actions\ApieFacadeInterface;
13use Apie\Core\Attributes\Description;
14use Apie\Core\BoundedContext\BoundedContextId;
15use Apie\Core\Context\ApieContext;
16use Apie\Core\ContextConstants;
17use Apie\Core\Datalayers\ApieDatalayer;
18use Apie\Core\Entities\EntityInterface;
19use Apie\Core\Exceptions\EntityNotFoundException;
20use Apie\Core\Exceptions\InvalidTypeException;
21use Apie\Core\IdentifierUtils;
22use Apie\Core\Lists\ItemHashmap;
23use Apie\Core\Lists\StringList;
24use Apie\Core\Metadata\MetadataFactory;
25use Apie\Core\Utils\EntityUtils;
26use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
27use LogicException;
28use Psr\Http\Message\ServerRequestInterface;
29use ReflectionClass;
30
31/**
32 * Action to modify an existing object.
33 */
34final class ModifyObjectAction implements ActionInterface
35{
36    public function __construct(private readonly ApieFacadeInterface $apieFacade)
37    {
38    }
39
40    public static function isAuthorized(ApieContext $context, bool $runtimeChecks, bool $throwError = false): bool
41    {
42        $refl = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME, $throwError));
43        if (EntityUtils::isPolymorphicEntity($refl) && $runtimeChecks && $context->hasContext(ContextConstants::RESOURCE)) {
44            $refl = new ReflectionClass($context->getContext(ContextConstants::RESOURCE, $throwError));
45        }
46        $metadata = MetadataFactory::getModificationMetadata($refl, $context);
47        if ($metadata->getHashmap()->count() === 0) {
48            if ($throwError) {
49                throw new LogicException('Metadata for ' . $refl->getShortName() . ' has no fields to edit.');
50            }
51            return false;
52        }
53        return $context->appliesToContext($refl, $runtimeChecks, $throwError ? new LogicException('Operation on ' . $refl->getShortName() . ' is not allowed') : null);
54    }
55    
56    /**
57     * @param array<string|int, mixed> $rawContents
58     */
59    public function __invoke(ApieContext $context, array $rawContents): ActionResponse
60    {
61        $context->withContext(ContextConstants::APIE_ACTION, __CLASS__)->checkAuthorization();
62        $resourceClass = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME));
63        $id = $context->getContext(ContextConstants::RESOURCE_ID);
64        if (!$resourceClass->implementsInterface(EntityInterface::class)) {
65            throw new InvalidTypeException($resourceClass->name, 'EntityInterface');
66        }
67        $lock = LockUtil::createLock(
68            $context,
69            [ContextConstants::BOUNDED_CONTEXT_ID, ContextConstants::RESOURCE_NAME, ContextConstants::RESOURCE_ID],
70            write: true
71        );
72        try {
73            try {
74                $identifier = IdentifierUtils::idStringToIdentifier($id, $context);
75                $boundedContextId = new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID));
76                $resource = $this->apieFacade->find($identifier, $boundedContextId);
77            } catch (InvalidStringForValueObjectException|EntityNotFoundException $error) {
78                IntegrationTestLogger::logException($error);
79                return ActionResponse::createClientError($this->apieFacade, $context, $error);
80            }
81            $request = $context->getContext(ServerRequestInterface::class, false);
82            $ifMatch = $request instanceof ServerRequestInterface
83                ? $request->getHeaderLine('If-Match')
84                : '';
85            if ($context->getContext(ContextConstants::REST_API, false) && $ifMatch !== '') {
86                $metadata = $context->getContext(ApieDatalayer::class)
87                    ->getMetadata($identifier, $boundedContextId);
88                $currentEtag = '"' . $metadata->etag . '"';
89                $providedEtags = array_map('trim', explode(',', $ifMatch));
90                if (!in_array('*', $providedEtags, true) && !in_array($currentEtag, $providedEtags, true)) {
91                    throw new EntityEtagConflictException();
92                }
93            }
94            $context = $context->withContext(ContextConstants::RESOURCE, $resource);
95            $resource = $this->apieFacade->denormalizeOnExistingObject(
96                new ItemHashmap($rawContents),
97                $resource,
98                $context,
99            );
100            if (!$lock->isAcquired()) {
101                throw new \LogicException('Lock was released before modification was finished!');
102            }
103            $resource = $this->apieFacade->persistExisting($resource, new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID)));
104        } catch (\Exception $error) {
105            IntegrationTestLogger::logException($error);
106            return ActionResponse::createClientError($this->apieFacade, $context, $error);
107        } finally {
108            $lock->release();
109        }
110        $context->dispatchEvent(new ApieResourceModified($resource, $context));
111        return ActionResponse::createRunSuccess($this->apieFacade, $context, $resource, $resource);
112    }
113
114    /**
115     * @return ReflectionClass<EntityInterface>
116     */
117    public static function getInputType(ReflectionClass $class): ReflectionClass
118    {
119        return $class;
120    }
121
122    /**
123     * @return ReflectionClass<EntityInterface>
124     */
125    public static function getOutputType(ReflectionClass $class): ReflectionClass
126    {
127        return $class;
128    }
129
130    public static function getPossibleActionResponseStatuses(): ActionResponseStatusList
131    {
132        return new ActionResponseStatusList([
133            ActionResponseStatus::SUCCESS,
134            ActionResponseStatus::CLIENT_ERROR,
135            ActionResponseStatus::PERISTENCE_ERROR
136        ]);
137    }
138
139    /**
140     * @param ReflectionClass<covariant object> $class
141     */
142    public static function getDescription(ReflectionClass $class): string
143    {
144        $description = 'Modifies an instance of ' . $class->getShortName();
145        foreach ($class->getAttributes(Description::class) as $attribute) {
146            $description .= '. ' . $attribute->newInstance()->description;
147        }
148
149        return $description;
150    }
151    
152    /**
153     * @param ReflectionClass<covariant object> $class
154     */
155    public static function getTags(ReflectionClass $class): StringList
156    {
157        return new StringList([$class->getShortName(), 'resource']);
158    }
159
160    /**
161     * @param ReflectionClass<covariant object> $class
162     */
163    public static function getRouteAttributes(ReflectionClass $class): array
164    {
165        return [
166            ContextConstants::EDIT_OBJECT => true,
167            ContextConstants::RESOURCE_NAME => $class->name,
168            ContextConstants::DISPLAY_FORM => true,
169        ];
170    }
171}