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