Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.16% covered (success)
90.16%
55 / 61
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
StreamItemMethodAction
90.16% covered (success)
90.16%
55 / 61
70.00% covered (warning)
70.00%
7 / 10
18.31
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
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 __invoke
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
3.05
 toDownload
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 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
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getRouteAttributes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Common\Actions;
3
4use Apie\Common\IntegrationTestLogger;
5use Apie\Common\Other\DownloadFile;
6use Apie\Common\Other\LockUtil;
7use Apie\Core\Actions\ActionResponse;
8use Apie\Core\Actions\ActionResponseStatus;
9use Apie\Core\Actions\ActionResponseStatusList;
10use Apie\Core\Actions\ApieFacadeInterface;
11use Apie\Core\Actions\MethodActionInterface;
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\FileStorage\StoredFile;
19use Apie\Core\IdentifierUtils;
20use Apie\Core\Lists\StringList;
21use Apie\Core\PropertyAccess;
22use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
23use Apie\Serializer\Exceptions\ValidationException;
24use LogicException;
25use Nyholm\Psr7\Factory\Psr17Factory;
26use Nyholm\Psr7\Stream;
27use Psr\Http\Message\ResponseInterface;
28use Psr\Http\Message\UploadedFileInterface;
29use ReflectionClass;
30use ReflectionMethod;
31
32/**
33 * Runs a method from  a resource and will stream the result.
34 */
35final class StreamItemMethodAction implements MethodActionInterface
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        return $context->appliesToContext($refl, $runtimeChecks, $throwError ? new LogicException('Class access is not allowed!') : null);
45    }
46
47    /**
48     * @param array<string|int, mixed> $rawContents
49     */
50    public function __invoke(ApieContext $context, array $rawContents): ActionResponse
51    {
52        $context->withContext(ContextConstants::APIE_ACTION, __CLASS__)->checkAuthorization();
53        $resourceClass = new ReflectionClass($context->getContext(ContextConstants::RESOURCE_NAME));
54        if (!$resourceClass->implementsInterface(EntityInterface::class)) {
55            throw new InvalidTypeException($resourceClass->name, 'EntityInterface');
56        }
57        $properties = explode('/', $context->getContext('properties'));
58        $lock = LockUtil::createLock(
59            $context,
60            [ContextConstants::BOUNDED_CONTEXT_ID, ContextConstants::RESOURCE_NAME, ContextConstants::RESOURCE_ID]
61        );
62        try {
63            $id = $context->getContext(ContextConstants::RESOURCE_ID);
64            try {
65                $resource = $this->apieFacade->find(
66                    IdentifierUtils::idStringToIdentifier($id, $context),
67                    new BoundedContextId($context->getContext(ContextConstants::BOUNDED_CONTEXT_ID))
68                );
69            } catch (InvalidStringForValueObjectException|EntityNotFoundException $error) {
70                IntegrationTestLogger::logException($error);
71                return ActionResponse::createClientError($this->apieFacade, $context, $error);
72            }
73            $context = $context->withContext(ContextConstants::RESOURCE, $resource);
74            $result = PropertyAccess::getPropertyValue($resource, $properties, $context, false);
75            $result = $this->toDownload($result);
76        } finally {
77            $lock->release();
78        }
79
80        return ActionResponse::createRunSuccess($this->apieFacade, $context, $result, $resource);
81    }
82
83    private function toDownload(mixed $result): ResponseInterface
84    {
85        $factory = new Psr17Factory();
86        $response = $factory->createResponse(200);
87        if (is_resource($result)) {
88            $stream = Stream::create($result);
89            $response = $response->withBody($stream);
90            $response = $response->withHeader('Content-Type', 'application/octet-stream');
91            return $response;
92        }
93        if ($result instanceof UploadedFileInterface) {
94            $stream = $result->getStream();
95            $response = $response->withBody($stream);
96            $filename = $result->getClientFilename();
97            $mimeType = $result instanceof StoredFile ? $result->getServerMimeType() : $result->getClientMediaType();
98            $response = $response->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
99            $response = $response->withHeader('Content-Type', $mimeType);
100            return $response;
101        }
102        throw ValidationException::createFromArray(['' => new LogicException('There is nothing to stream')]);
103    }
104
105    /** @param ReflectionClass<object> $class */
106    public static function getInputType(ReflectionClass $class, ?ReflectionMethod $method = null): ReflectionMethod
107    {
108        return $class->getConstructor() ?? new ReflectionMethod(DownloadFile::class, '__construct');
109    }
110
111    /** @param ReflectionClass<object> $class */
112    public static function getOutputType(ReflectionClass $class, ?ReflectionMethod $method = null): ReflectionMethod
113    {
114        return new ReflectionMethod(DownloadFile::class, 'download');
115    }
116
117    public static function getPossibleActionResponseStatuses(?ReflectionMethod $method = null): ActionResponseStatusList
118    {
119        return new ActionResponseStatusList([
120            ActionResponseStatus::SUCCESS,
121            ActionResponseStatus::CLIENT_ERROR,
122            ActionResponseStatus::NOT_FOUND,
123        ]);
124    }
125
126    /**
127     * @param ReflectionClass<object> $class
128     */
129    public static function getDescription(ReflectionClass $class, ?ReflectionMethod $method = null): string
130    {
131        return 'Streams a file on a ' . $class->getShortName() . ' with a specific id';
132    }
133    
134    /**
135     * @param ReflectionClass<object> $class
136     */
137    public static function getTags(ReflectionClass $class, ?ReflectionMethod $method = null): StringList
138    {
139        $className = $class->getShortName();
140        $declared = $method ? $method->getDeclaringClass()->getShortName() : $className;
141        if ($className !== $declared) {
142            return new StringList([$className, $declared, 'action', 'download']);
143        }
144        return new StringList([$className, 'action', 'download']);
145    }
146
147    /**
148     * @param ReflectionClass<object> $class
149     */
150    public static function getRouteAttributes(ReflectionClass $class, ?ReflectionMethod $method = null): array
151    {
152        return
153        [
154            ContextConstants::GET_OBJECT => true,
155            ContextConstants::RESOURCE_METHOD => true,
156            ContextConstants::RESOURCE_NAME => $class->name,
157            ContextConstants::DISPLAY_FORM => true,
158        ];
159    }
160}