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