Issues (40)

php-src/Application/ResponseFactory.php (2 issues)

1
<?php
2
3
namespace kalanis\Restful\Application;
4
5
6
use kalanis\Restful\Exceptions\InvalidArgumentException;
7
use kalanis\Restful\Exceptions\InvalidStateException;
8
use kalanis\Restful\IResource;
9
use kalanis\Restful\Mapping\MapperContext;
10
use Nette\Http\IRequest;
11
use Nette\Http\IResponse;
12
use function str_contains;
13
14
15
/**
16
 * REST ResponseFactory
17
 * @package kalanis\Restful\Application
18
 */
19 1
class ResponseFactory implements IResponseFactory
20
{
21
22
    /** @var string|null JSONP request key */
23
    private ?string $jsonp = null;
24
25
    /** @var string pretty print key */
26
    private string $prettyPrintKey = 'prettyPrint';
27
28
    private bool $prettyPrint = true;
29
30
    /** @var array<string, class-string> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
31
    private array $responses = [
32
        IResource::JSON => Responses\TextResponse::class,
33
        IResource::JSONP => Responses\JsonpResponse::class,
34
        IResource::QUERY => Responses\TextResponse::class,
35
        IResource::XML => Responses\TextResponse::class,
36
        IResource::FILE => Responses\FileResponse::class,
37
        IResource::NULL => Responses\NullResponse::class
38
    ];
39
40 1
    public function __construct(
41
        private IResponse              $response,
42
        private readonly IRequest      $request,
43
        private readonly MapperContext $mapperContext,
44
    )
45
    {
46 1
    }
47
48
    /**
49
     * Get JSONP key
50
     * @return string|null [type] [description]
51
     */
52
    public function getJsonp(): ?string
53
    {
54
        return $this->jsonp;
55
    }
56
57
    /**
58
     * Set JSONP key
59
     */
60
    public function setJsonp(?string $jsonp): self
61
    {
62 1
        $this->jsonp = $jsonp;
63 1
        return $this;
64
    }
65
66
    /**
67
     * Set pretty print key
68
     */
69
    public function setPrettyPrintKey(string $prettyPrintKey): self
70
    {
71 1
        $this->prettyPrintKey = $prettyPrintKey;
72 1
        return $this;
73
    }
74
75
    /**
76
     * Register new response type to factory
77
     * @param string $mimeType
78
     * @param class-string $responseClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
79
     * @throws InvalidArgumentException
80
     * @return $this
81
     *
82
     */
83
    public function registerResponse(string $mimeType, string $responseClass): static
84
    {
85 1
        if (!class_exists($responseClass)) {
86 1
            throw new InvalidArgumentException('Response class does not exist.');
87
        }
88
89 1
        $this->responses[$mimeType] = $responseClass;
90 1
        return $this;
91
    }
92
93
    /**
94
     * Unregister API response from factory
95
     */
96
    public function unregisterResponse(string $mimeType): void
97
    {
98
        unset($this->responses[$mimeType]);
99
    }
100
101
    /**
102
     * Set HTTP response
103
     */
104
    public function setHttpResponse(IResponse $response): self
105
    {
106
        $this->response = $response;
107
        return $this;
108
    }
109
110
    /**
111
     * Create new api response
112
     * @param IResource $resource
113
     * @param string|null $contentType
114
     * @throws InvalidStateException
115
     * @return Responses\IResponse
116
     */
117
    public function create(IResource $resource, ?string $contentType = null): Responses\IResponse
118
    {
119 1
        if (is_null($contentType)) {
120 1
            $contentType = is_null($this->jsonp) || empty($this->request->getQuery($this->jsonp))
121 1
                ? $this->getPreferredContentType(strval($this->request->getHeader('Accept')))
122 1
                : IResource::JSONP;
123
        }
124
125 1
        if (!isset($this->responses[$contentType])) {
126
            throw new InvalidStateException('Unregistered API response for ' . $contentType);
127
        }
128
129 1
        if (!class_exists($this->responses[$contentType])) {
130
            throw new InvalidStateException('API response class does not exist.');
131
        }
132
133 1
        if (empty($resource->getData())) {
134 1
            $this->response->setCode(204); // No content
135 1
            $reflection = new \ReflectionClass($this->responses[$contentType]);
136 1
            $response = $reflection->newInstance(
137 1
                $resource->getData(),
138 1
                $this->mapperContext->getMapper($contentType),
139
            );
140
            /** @var Responses\IResponse $response */
141 1
            return $response;
142
        }
143
144 1
        $responseClass = $this->responses[$contentType];
145 1
        $reflection = new \ReflectionClass($responseClass);
146 1
        $response = $reflection->newInstance(
147 1
            $resource->getData(),
148 1
            $this->mapperContext->getMapper($contentType),
149
            $contentType,
150
        );
151
        /** @var Responses\IResponse $response */
152 1
        if ($response instanceof Responses\BaseResponse) {
153 1
            $response->setPrettyPrint($this->isPrettyPrint());
154
        }
155 1
        return $response;
156
    }
157
158
    /**
159
     * Get preferred request content type
160
     * @param string $contentType may be separated with comma
161
     * @throws  InvalidStateException If Accept header is unknown
162
     * @return string
163
     */
164
    protected function getPreferredContentType(string $contentType): string
165
    {
166 1
        $accept = explode(',', $contentType);
167 1
        $acceptableTypes = array_keys($this->responses);
168 1
        if (!$contentType) {
169
            return $acceptableTypes[0];
170
        }
171 1
        foreach ($accept as $mimeType) {
172 1
            if ('*/*' === $mimeType) return $acceptableTypes[0];
173 1
            foreach ($acceptableTypes as $formatMime) {
174 1
                if (empty($formatMime)) {
175 1
                    continue;
176
                }
177 1
                if (str_contains($mimeType, $formatMime)) {
178 1
                    return $formatMime;
179
                }
180
            }
181
        }
182 1
        throw new InvalidStateException('Unknown Accept header: ' . $contentType);
183
    }
184
185
    /**
186
     * Is pretty print enabled
187
     * @return boolean
188
     */
189
    protected function isPrettyPrint(): bool
190
    {
191 1
        $prettyPrintKey = $this->request->getQuery($this->prettyPrintKey);
192 1
        if ('false' === $prettyPrintKey) {
193
            return false;
194
        }
195 1
        if ('true' === $prettyPrintKey) {
196
            return true;
197
        }
198 1
        return $this->prettyPrint;
199
    }
200
201
    /**
202
     * Set pretty print
203
     */
204
    public function setPrettyPrint(bool $prettyPrint): self
205
    {
206
        $this->prettyPrint = $prettyPrint;
207
        return $this;
208
    }
209
210
    /**
211
     * Is given content type acceptable for response
212
     */
213
    public function isAcceptable(string $contentType): bool
214
    {
215
        try {
216 1
            $this->getPreferredContentType($contentType);
217 1
            return true;
218 1
        } catch (InvalidStateException) {
219 1
            return false;
220
        }
221
    }
222
}
223