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