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