Resource::actions()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 50
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3.009

Importance

Changes 0
Metric Value
cc 3
eloc 37
nc 4
nop 0
dl 0
loc 50
ccs 18
cts 20
cp 0.9
crap 3.009
rs 9.328
c 0
b 0
f 0
1
<?php
2
3
namespace roaresearch\yii2\roa\controllers;
4
5
use roaresearch\yii2\roa\{actions, FileRecord, hal\ARContract};
6
use Yii;
7
use yii\{
8
    base\InvalidRouteException,
9
    data\ActiveDataProvider,
10
    db\ActiveQuery,
11
    filters\VerbFilter,
12
    helpers\ArrayHelper,
13
    web\MethodNotAllowedHttpException,
14
    web\NotFoundHttpException
15
};
16
17
/**
18
 * Resource Controller with OAuth2 Support.
19
 *
20
 * @author  Angel (Faryshta) Guevara <[email protected]>
21
 */
22
class Resource extends \yii\rest\ActiveController
23
{
24
    /**
25
     * @var string[] list of rest actions defined by default.
26
     */
27
    public const DEFAULT_REST_ACTIONS = [
28
        'index',
29
        'view',
30
        'create',
31
        'update',
32
        'delete',
33
        'file-stream', // download files
34
        'options',
35
    ];
36
37
    /**
38
     * @var string name of the attribute to be used on `findModel()`.
39
     */
40
    public string $idAttribute = 'id';
41
42
    /**
43
     * @var ?string attribute name used to filter only the records associated to
44
     * the logged user.
45
     * If `null` then no filter will be added.
46
     */
47
    public ?string $userAttribute;
48
49
    /**
50
     * @var ?string class name for the model to be used on the search.
51
     * Must implement `roaresearch\yii2\roa\ResourceSearch`
52
     */
53
    public ?string $searchClass = null;
54
55
    /**
56
     * @var string name of the form which will hold the GET parameters to filter
57
     * results on a search request.
58
     */
59
    public string $searchFormName = '';
60
61
    /**
62
     * @var string[] $attribute => $param pairs to filter the queries.
63
     */
64
    public array $filterParams = [];
65
66
    /**
67
     * @var string[] array used in `actions\Create::fileAttributes`
68
     * @see actions\LoadFileTrait::$fileAttributes
69
     */
70
    public array $createFileAttributes = [];
71
72
    /**
73
     * @var string[] array used in `actions\Update::fileAttributes`
74
     * @see actions\LoadFileTrait::$fileAttributes
75
     */
76
    public array $updateFileAttributes = [];
77
78
    /**
79
     * @var string the message shown when no register is found.
80
     */
81
    public string $notFoundMessage = 'The record "{id}" does not exists.';
82
83
    /**
84
     * @inheritdoc
85
     */
86 14
    public function behaviors()
87
    {
88
        return [
89
            // content negotiator, autenticator, etc moved by default to
90
            // api container
91
            'verbFilter' => [
92
                'class' => VerbFilter::class,
93 14
                'actions' => $this->buildAllowedVerbs(),
94
            ],
95
        ];
96
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101 14
    public function actions()
102
    {
103 14
        $index = $this->searchClass
104
            ? [
105 11
                'class' => actions\Index::class,
106 11
                'searchClass' => $this->searchClass,
107 11
                'formName' => $this->searchFormName,
108
            ]
109
            : [
110 3
                'class' => \yii\rest\IndexAction::class,
111 3
                'modelClass' => $this->modelClass,
112
                'prepareDataProvider' => [$this, 'indexProvider'],
113
            ];
114 14
        $interfaces = class_implements($this->modelClass);
115 14
        $fileStream = isset($interfaces[FileRecord::class])
116
            ? [
117
                'class' => actions\FileStream::class,
118
                'modelClass' => $this->modelClass,
119
                'findModel' => [$this, 'findModel'],
120
            ]
121 14
            : null;
122
123
        return [
124
            'index' => $index,
125
            'view' => [
126
                'class' => actions\View::class,
127 14
                'modelClass' => $this->modelClass,
128
                'findModel' => [$this, 'findModel'],
129
            ],
130
            'update' => [
131
                'class' => actions\Update::class,
132 14
                'modelClass' => $this->modelClass,
133
                'findModel' => [$this, 'findModel'],
134 14
                'scenario' => $this->updateScenario,
135 14
                'fileAttributes' => $this->updateFileAttributes,
136
            ],
137
            'create' => [
138
                'class' => actions\Create::class,
0 ignored issues
show
Bug introduced by
The type roaresearch\yii2\roa\actions\Create was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
139 14
                'modelClass' => $this->modelClass,
140 14
                'scenario' => $this->createScenario,
141 14
                'fileAttributes' => $this->createFileAttributes,
142
            ],
143
            'delete' => [
144
                'class' => actions\Delete::class,
145 14
                'modelClass' => $this->modelClass,
146
                'findModel' => [$this, 'findModel'],
147
            ],
148
            'file-stream' => $fileStream,
149
            'options' => [
150
                'class' => \yii\rest\OptionsAction::class,
151
            ],
152
        ];
153
    }
154
155
    /**
156
     * Creates a data provider for the request.
157
     *
158
     * @return ActiveDataProvider
159
     */
160
    public function indexProvider(): ActiveDataProvider
161
    {
162
        return new ActiveDataProvider(['query' => $this->indexQuery()]);
163
    }
164
165
    /**
166
     * Finds the record based on the provided id or throws an exception.
167
     * @param int $id the unique identifier for the record.
168
     * @return ARContract
169
     * @throws NotFoundHttpException if the record can't be found.
170
     */
171 10
    public function findModel($id): ARContract
172
    {
173 10
        return $this->findQuery($id)->one() ?: throw new NotFoundHttpException(
174 10
            strtr($this->notFoundMessage, ['{id}' => $id])
175
        );
176
    }
177
178
    /**
179
     * Creates the query to be used by the `findOne()` method.
180
     *
181
     * @param int $id the unique identifier
182
     * @return ActiveQuery
183
     */
184 10
    public function findQuery($id): ActiveQuery
185
    {
186 10
        return $this->baseQuery()->andWhere([$this->idAttribute => $id]);
187
    }
188
189
    /**
190
     * Creates the query to be used by the `index` action when `$searchClass` is
191
     * not set.
192
     *
193
     * @return ActiveQuery
194
     */
195
    public function indexQuery(): ActiveQuery
196
    {
197
        return $this->baseQuery();
198
    }
199
200
    /**
201
     * @return ActiveQuery
202
     */
203 10
    protected function baseQuery(): ActiveQuery
204
    {
205 10
        return $this->modelClass::find()
206 10
            ->andFilterWhere($this->filterCondition());
207
    }
208
209
    /**
210
     * @return array the conditions to filter the base query to find records.
211
     */
212 10
    protected function filterCondition(): array
213
    {
214 10
        $condition = [];
215 10
        foreach ($this->filterParams as $attribute => $param) {
216 4
            if (is_int($attribute)) {
217 4
                $attribute = $param;
218
            }
219 4
            $condition[$attribute] = Yii::$app->request->getQueryParam($param);
220
        }
221
222 10
        if (isset($this->userAttribute)) {
223
            $condition[$this->userAttribute] = Yii::$app->user->id;
224
        }
225
226 10
        return $condition;
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232 14
    protected function verbs()
233
    {
234
        return [
235 14
            'index' => ['GET', 'HEAD'],
236
            'view' => ['GET', 'HEAD'],
237
            'create' => ['POST'],
238
            'update' => ['PUT', 'PATCH', 'POST'],
239
            'delete' => ['DELETE'],
240
            'file-stream' => ['GET'],
241
            'options' => ['OPTIONS'],
242
        ];
243
    }
244
245
    /**
246
     * @return string[] actions which serve a single record.
247
     */
248 14
    protected function listRecordActions(): array
249
    {
250 14
        return ['view', 'update', 'delete'];
251
    }
252
253
    /**
254
     * @return string[] actions which serve a collection of records.
255
     */
256 14
    protected function listCollectionActions(): array
257
    {
258 14
        return ['index', 'create'];
259
    }
260
261
    /**
262
     * Builds the HTTP Methods allowed for each action.
263
     *
264
     * Since ROA Resources differentiate routes on record routes and collection
265
     * rules it was needed to organize the action into record action and
266
     * collection actions and make sure that all record/collection actions
267
     * returned the same allowed verbs since they are using the same route.
268
     *
269
     * @return string[] which HTTP Methods are allowed for each action id.
270
     * @see VerbFilter::$verbs
271
     */
272 14
    protected function buildAllowedVerbs(): array
273
    {
274 14
        $verbs = $this->verbs();
275 14
        $recordActions = $this->listRecordActions();
276 14
        $collectionActions = $this->listCollectionActions();
277 14
        $recordVerbs = ['OPTIONS'];
278 14
        $collectionVerbs = ['OPTIONS'];
279
280 14
        foreach ($recordActions as $action) {
281 14
            $recordVerbs = array_merge(
282
                $recordVerbs,
283 14
                ArrayHelper::getValue($verbs, $action, [])
284
            );
285
        }
286
287 14
        $recordVerbs = array_values(array_unique(
288 14
            array_map('strtoupper', $recordVerbs)
289
        ));
290
291 14
        foreach ($collectionActions as $action) {
292 14
            $collectionVerbs = array_merge(
293
                $collectionVerbs,
294 14
                ArrayHelper::getValue($verbs, $action, [])
295
            );
296
        }
297
298 14
        $collectionVerbs = array_values(array_unique(
299 14
            array_map('strtoupper', $collectionVerbs)
300
        ));
301
302 14
        $allowedVerbs = ['options' => 'OPTIONS'];
303 14
        foreach ($verbs as $action => $defaultVerbs) {
304 14
            if (in_array($action, $recordActions)) {
305 14
                $allowedVerbs[$action] = $recordVerbs;
306 14
            } elseif (in_array($action, $collectionActions)) {
307 14
                $allowedVerbs[$action] = $collectionVerbs;
308
            } else {
309 14
                $allowedVerbs[$action] = $defaultVerbs;
310
            }
311
        }
312
313 14
        foreach (self::DEFAULT_REST_ACTIONS as $action) {
314 14
            if (!isset($allowedVerbs[$action])) {
315 1
                if (in_array($action, $recordActions)) {
316
                    $allowedVerbs[$action] = $recordVerbs;
317 1
                } elseif (in_array($action, $collectionActions)) {
318 1
                    $allowedVerbs[$action] = $collectionVerbs;
319
                }
320
            }
321
        }
322
323 14
        return $allowedVerbs;
324
    }
325
}
326