ResourcePresenter::link()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
3
namespace kalanis\Restful\Application\UI;
4
5
6
use kalanis\Restful\Application\Exceptions\BadRequestException;
7
use kalanis\Restful\Application\IResourcePresenter;
8
use kalanis\Restful\Application\IResponseFactory;
9
use kalanis\Restful\Application\ResponseFactory;
10
use kalanis\Restful\Application\Responses\ErrorResponse;
11
use kalanis\Restful\Exceptions\InvalidStateException;
12
use kalanis\Restful\Http\IInput;
13
use kalanis\Restful\Http\Input;
14
use kalanis\Restful\Http\InputFactory;
15
use kalanis\Restful\IResource;
16
use kalanis\Restful\IResourceFactory;
17
use kalanis\Restful\Resource\Link;
18
use kalanis\Restful\Security\AuthenticationContext;
19
use kalanis\Restful\Security\Exceptions\SecurityException;
20
use Nette\Application;
21
use Nette\Application\UI;
22
use ReflectionClass;
23
use ReflectionMethod;
24
use Throwable;
25
26
27
/**
28
 * Base presenter for REST API presenters
29
 * @package kalanis\Restful\Application
30
 */
31
abstract class ResourcePresenter extends UI\Presenter implements IResourcePresenter
32
{
33
34
    /** @internal */
35
    public const VALIDATE_ACTION_PREFIX = 'validate';
36
37
    /** @var IResource|array<string, mixed>|null */
38
    protected mixed $resource = null;
39
40
    #[\Nette\DI\Attributes\Inject]
41
    public IResourceFactory $resourceFactory;
42
43
    #[\Nette\DI\Attributes\Inject]
44
    public IResponseFactory $responseFactory;
45
46
    #[\Nette\DI\Attributes\Inject]
47
    public AuthenticationContext $authentication;
48
49
    private ?IInput $input = null;
50
51
    #[\Nette\DI\Attributes\Inject]
52
    public InputFactory $inputFactory;
53
54
    /**
55
     * Check security and other presenter requirements
56
     * @param ReflectionClass<object>|ReflectionMethod $element
57
     * @return void
58
     */
59
    public function checkRequirements(ReflectionClass|ReflectionMethod $element): void
60
    {
61
        try {
62
            parent::checkRequirements($element);
63
        } catch (Application\ForbiddenRequestException $e) {
64
            $this->sendErrorResource($e);
65
        }
66
67
        // Try to authenticate client
68
        try {
69
            $this->authentication->authenticate($this->getInput());
70
        } catch (SecurityException $e) {
71
            $this->sendErrorResource($e);
72
        }
73
    }
74
75
    /**
76
     * Send error resource to output
77
     */
78
    protected function sendErrorResource(Throwable $e, ?string $contentType = null): void
79
    {
80
        try {
81
            $this->sendResponse(
82
                new ErrorResponse(
83
                    $this->responseFactory->create($this->createErrorResource($e)),
84
                    (99 < $e->getCode() && 600 > $e->getCode() ? $e->getCode() : 400)
85
                )
86
            );
87
88
        } catch (InvalidStateException $e) {
89
            // if the $contentType is not forced and the user has requested an unacceptable content-type, default to JSON
90
            $accept = $this->getHttpRequest()->getHeader('Accept');
91
92
            /** @var ResponseFactory $responseFactory */
93
            $responseFactory = $this->responseFactory;
94
            if (is_null($contentType) && (!$accept || !$responseFactory->isAcceptable($accept))) {
95
                $contentType = IResource::JSON;
96
            }
97
            $this->sendErrorResource(
98
                BadRequestException::unsupportedMediaType($e->getMessage(), $e),
99
                $contentType
100
            );
101
        }
102
    }
103
104
    /**
105
     * Create error response from exception
106
     * @param Throwable $e
107
     * @return IResource
108
     */
109
    protected function createErrorResource(Throwable $e): IResource
110
    {
111
        $params = [
112
            'code' => $e->getCode(),
113
            'status' => 'error',
114
            'message' => $e->getMessage(),
115
        ];
116
117
        if (($e instanceof BadRequestException) && !empty($e->errors)) {
118
            $params['errors'] = $e->errors;
119
        }
120
121
        return $this->resourceFactory->create($params);
122
    }
123
124
    /**
125
     * Get input
126
     * @return IInput
127
     */
128
    public function getInput(): IInput
129
    {
130
        if ($this->input) {
131
            return $this->input;
132
        }
133
        try {
134
            $input = $this->inputFactory->create();
135
            $this->input = $input;
136
            return $input;
137
        } catch (BadRequestException $e) {
138
            $this->sendErrorResource($e);
139
            // wont happen
140
            throw $e;
141
        }
142
    }
143
144
    /**
145
     * Create resource link representation object
146
     * @param string $destination
147
     * @param array<string, string>|string $args
148
     * @param string $rel
149
     * @throws UI\InvalidLinkException
150
     * @return string
151
     */
152
    public function link(string $destination, $args = [], $rel = Link::SELF): string
153
    {
154
        return new Link(parent::link($destination, $args), $rel);
155
    }
156
157
    /**
158
     * Presenter startup
159
     *
160
     * @throws BadRequestException
161
     */
162
    protected function startup(): void
163
    {
164
        parent::startup();
165
        $this->autoCanonicalize = false;
166
167
        try {
168
            // Create resource object
169
            $this->resource = $this->resourceFactory->create();
170
171
            // calls $this->validate<Action>()
172
            $validationProcessed = $this->tryCall(static::formatValidateMethod($this->getAction()), $this->params);
173
174
            // Check if input is validate
175
            $input = $this->getInput();
176
            /** @var Input<string, mixed> $input */
177
            if (!$input->isValid() && true === $validationProcessed) {
178
                $errors = $input->validate();
179
                throw BadRequestException::unprocessableEntity($errors, 'Validation Failed: ' . $errors[0]->getMessage());
180
            }
181
        } catch (BadRequestException $e) {
182
            if (422 === $e->getCode()) {
183
                $this->sendErrorResource($e);
184
                return;
185
            }
186
            throw $e;
187
        } catch (InvalidStateException $e) {
188
            $this->sendErrorResource($e);
189
        }
190
    }
191
192
    /**
193
     * Validate action method
194
     */
195
    public static function formatValidateMethod(string $action): string
196
    {
197
        return self::VALIDATE_ACTION_PREFIX . $action;
198
    }
199
200
    /**
201
     * On before render
202
     */
203
    protected function beforeRender(): void
204
    {
205
        parent::beforeRender();
206
        $this->sendResource();
207
    }
208
209
    /**
210
     * Get REST API response
211
     * @param string|null $contentType
212
     * @throws InvalidStateException
213
     */
214
    public function sendResource(?string $contentType = null): void
215
    {
216
        try {
217
            $this->sendResponse(
218
                $this->responseFactory->create(
219
                    ($this->resource instanceof IResource)
220
                        ? $this->resource
221
                        : $this->resourceFactory->create((array) $this->resource)
222
                    ,
223
                    $contentType,
224
                )
225
            );
226
        } catch (InvalidStateException $e) {
227
            $this->sendErrorResource(
228
                BadRequestException::unsupportedMediaType($e->getMessage(), $e),
229
                $contentType,
230
            );
231
        }
232
    }
233
}
234