Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.53% covered (success)
91.53%
54 / 59
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreateObjectAction
91.53% covered (success)
91.53%
54 / 59
77.78% covered (warning)
77.78%
7 / 9
23.32
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
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
10.08
 __invoke
86.67% covered (warning)
86.67%
26 / 30
0.00% covered (danger)
0.00%
0 / 1
5.06
 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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Common\Actions;
3
4use Apie\Common\Events\ApieResourceCreated;
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\Enums\RequestMethod;
20use Apie\Core\Exceptions\EntityAlreadyPersisted;
21use Apie\Core\Exceptions\EntityNotFoundException;
22use Apie\Core\Exceptions\IndexNotFoundException;
23use Apie\Core\Exceptions\InvalidTypeException;
24use Apie\Core\Lists\StringList;
25use Apie\Core\Utils\EntityUtils;
26use Apie\Core\ValueObjects\Utils;
27use Exception;
28use LogicException;
29use Psr\Http\Message\ServerRequestInterface;
30use ReflectionClass;
31
32/**
33 * Action to create a new object.
34 */
35final class CreateObjectAction implements ActionInterface
36{
37    public function __construct(private readonly ApieFacadeInterface $apieFacade)
38    {
39    }
40
41    public static function isAuthorized(ApieContext $context, bool $runtimeChecks, bool $throwError = false): bool
42    {
43        $refl = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME, $throwError));
44        if (!$context->appliesToContext($refl, $runtimeChecks, $throwError ? new LogicException('Class access is not allowed') : null)) {
45            return false;
46        }
47        if (EntityUtils::isPolymorphicEntity($refl) && $runtimeChecks && $context->hasContext(ContextConstants::RAW_CONTENTS)) {
48            $rawContents = Utils::toArray($context->getContext(ContextConstants::RAW_CONTENTS, $throwError));
49            try {
50                $refl = EntityUtils::findClass($rawContents, $refl);
51            } catch (IndexNotFoundException) {
52            }
53        }
54        $constructor = $refl->getConstructor();
55        if ($constructor && !$context->appliesToContext($constructor, $runtimeChecks, $throwError ? new LogicException('Class instantiation is not allowed') : null)) {
56            return false;
57        }
58        return true;
59    }
60    
61    /**
62     * @param array<string|int, mixed> $rawContents
63     */
64    public function __invoke(ApieContext $context, array $rawContents): ActionResponse
65    {
66        $context->withContext(ContextConstants::RAW_CONTENTS, $rawContents)
67            ->withContext(ContextConstants::APIE_ACTION, __CLASS__)
68            ->checkAuthorization();
69        try {
70            $resource = $this->apieFacade->denormalizeNewObject(
71                $rawContents,
72                $context->getContext(ContextConstants::RESOURCE_NAME),
73                $context
74            );
75        } catch (Exception $error) {
76            IntegrationTestLogger::logException($error);
77            return ActionResponse::createClientError($this->apieFacade, $context, $error);
78        }
79        if (!$resource instanceof EntityInterface) {
80            throw new InvalidTypeException($resource, EntityInterface::class);
81        }
82        $context = $context->withContext(ContextConstants::RESOURCE, $resource);
83        $boundedContextId = new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID));
84
85        // only autoincrement integers should do this, but this means we can not make a lock
86        if ($resource->getId()->toNative() === null) {
87            $resource = $this->apieFacade->persistNew($resource, $boundedContextId);
88        } else {
89            $lock = LockUtil::createLock(
90                $context,
91                [ContextConstants::BOUNDED_CONTEXT_ID, ContextConstants::RESOURCE_NAME, ContextConstants::RESOURCE_ID],
92                write: true
93            );
94            try {
95                $resource = $this->apieFacade->persistNew($resource, $boundedContextId);
96            } catch (Exception $error) {
97                IntegrationTestLogger::logException($error);
98                return ActionResponse::createClientError($this->apieFacade, $context, $error);
99            } finally {
100                $lock->release();
101            }
102        }
103        $context = $context->withContext(ContextConstants::RESOURCE, $resource);
104        $context->dispatchEvent(new ApieResourceCreated($resource, $context));
105        return ActionResponse::createCreationSuccess($this->apieFacade, $context, $resource, $resource);
106    }
107
108    /**
109     * @return ReflectionClass<EntityInterface>
110     */
111    public static function getInputType(ReflectionClass $class): ReflectionClass
112    {
113        return $class;
114    }
115
116    /**
117     * @return ReflectionClass<EntityInterface>
118     */
119    public static function getOutputType(ReflectionClass $class): ReflectionClass
120    {
121        return $class;
122    }
123
124    public static function getPossibleActionResponseStatuses(): ActionResponseStatusList
125    {
126        return new ActionResponseStatusList([
127            ActionResponseStatus::CREATED,
128            ActionResponseStatus::CLIENT_ERROR,
129            ActionResponseStatus::PERISTENCE_ERROR
130        ]);
131    }
132
133    /**
134     * @param ReflectionClass<covariant object> $class
135     */
136    public static function getDescription(ReflectionClass $class): string
137    {
138        $description = 'Creates an instance of ' . $class->getShortName();
139        foreach ($class->getAttributes(Description::class) as $attribute) {
140            $description .= '. ' . $attribute->newInstance()->description;
141        }
142
143        return $description;
144    }
145    
146    /**
147     * @param ReflectionClass<covariant object> $class
148     */
149    public static function getTags(ReflectionClass $class): StringList
150    {
151        return new StringList([$class->getShortName(), 'resource']);
152    }
153
154    /**
155     * @param ReflectionClass<covariant object> $class
156     */
157    public static function getRouteAttributes(ReflectionClass $class): array
158    {
159        return [
160            ContextConstants::CREATE_OBJECT => true,
161            ContextConstants::RESOURCE_NAME => $class->name,
162            ContextConstants::DISPLAY_FORM => true,
163        ];
164    }
165}