Test Failed
Push — master ( 817d84...d52e3d )
by Raffael
05:44
created

Controller   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 392
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 4
dl 0
loc 392
ccs 0
cts 173
cp 0
rs 6
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
B findNodeByPath() 0 45 11
B findNodesByPath() 0 27 8
B getNodes() 0 23 9
C getNode() 0 31 12
B bulk() 0 31 7
A _getNode() 0 23 4
A _getNodes() 0 21 4

How to fix   Complexity   

Complex Class

Complex classes like Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Api\v1;
13
14
use Balloon\App\Api\v2\Collections as ApiCollection;
15
use Balloon\App\Api\v2\Files as ApiFile;
16
use Balloon\Filesystem;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Filesystem\Node\Collection;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v1\Collection.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
use Balloon\Filesystem\Node\File;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v1\File.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
20
use Balloon\Filesystem\Node\NodeInterface;
21
use Closure;
22
use Generator;
23
use Micro\Http\ExceptionInterface;
24
use Micro\Http\Response;
25
26
abstract class Controller
27
{
28
    /**
29
     * Filesystem.
30
     *
31
     * @var Filesystem
32
     */
33
    protected $fs;
34
35
    /**
36
     * @apiDefine _getNode
37
     *
38
     * @apiParam (GET Parameter) {string} id Either id or p (path) of a node must be given.
39
     * @apiParam (GET Parameter) {string} p Either id or p (path) of a node must be given.
40
     * @apiError (General Error Response) {number} status Status Code
41
     * @apiError (General Error Response) {object[]} data Error body
42
     * @apiError (General Error Response) {string} data.error Exception
43
     * @apiError (General Error Response) {string} data.message Message
44
     * @apiError (General Error Response) {number} data.code Error code
45
     *
46
     * @apiErrorExample {json} Error-Response (Invalid Parameter):
47
     * HTTP/1.1 400 Bad Request
48
     * {
49
     *      "status": 400,
50
     *      "data": {
51
     *          "error": "Balloon\\Exception\\InvalidArgument",
52
     *          "message": "invalid node id specified",
53
     *          "code": 0
54
     *      }
55
     * }
56
     *
57
     * @apiErrorExample {json} Error-Response (Insufficient Access):
58
     * HTTP/1.1 403 Forbidden
59
     * {
60
     *      "status": 403,
61
     *      "data": {
62
     *          "error": "Balloon\\Exception\\Forbidden",
63
     *          "message": "not allowed to read node 51354d073c58891f058b4580",
64
     *          "code": 40
65
     *      }
66
     * }
67
     *
68
     * @apiErrorExample {json} Error-Response (Not found):
69
     * HTTP/1.1 404 Not Found
70
     * {
71
     *      "status": 404,
72
     *      "data": {
73
     *          "error": "Balloon\\Exception\\NotFound",
74
     *          "message": "node 51354d073c58891f058b4580 not found",
75
     *          "code": 49
76
     *      }
77
     * }
78
     */
79
80
    /**
81
     * @apiDefine _multiError
82
     *
83
     * @apiErrorExample {json} Error-Response (Multi node error):
84
     * HTTP/1.1 400 Bad Request
85
     * {
86
     *     "status": 400,
87
     *     "data": [
88
     *         {
89
     *              id: "51354d073c58891f058b4580",
90
     *              name: "file.zip",
91
     *              error: "Balloon\\Exception\\Conflict",
92
     *              message: "node already exists",
93
     *              code: 30
94
     *         }
95
     *     ]
96
     * }
97
     */
98
99
    /**
100
     * @apiDefine _writeAction
101
     *
102
     * @apiErrorExample {json} Error-Response (Conflict):
103
     * HTTP/1.1 400 Bad Request
104
     * {
105
     *      "status": 400,
106
     *      "data": {
107
     *          "error": "Balloon\\Exception\\Conflict",
108
     *          "message": "a node called myname does already exists",
109
     *          "code": 17
110
     *      }
111
     * }
112
     */
113
114
    /**
115
     * @apiDefine _conflictNode
116
     * @apiParam (GET Parameter) {number} [conflict=0] Decides how to handle a conflict if a node with the same name already exists at the destination.
117
     * Possible values are:</br>
118
     *  - 0 No action</br>
119
     *  - 1 Automatically rename the node</br>
120
     *  - 2 Overwrite the destination (merge)</br>
121
     */
122
123
    /**
124
     * @apiDefine _getNodes
125
     *
126
     * @apiParam (GET Parameter) {string[]} id Either a single id as string or multiple as an array or a single p (path) as string or multiple paths as array must be given.
127
     * @apiParam (GET Parameter) {string[]} p Either a single id as string or multiple as an array or a single p (path) as string or multiple paths as array must be given.
128
     * @apiError (General Error Response) {number} status Status Code
129
     * @apiError (General Error Response) {object[]} data Error body
130
     * @apiError (General Error Response) {string} data.error Exception
131
     * @apiError (General Error Response) {string} data.message Message
132
     * @apiError (General Error Response) {number} data.code General error messages of type  Balloon\\Exception do not usually have an error code
133
     *
134
     * @apiErrorExample {json} Error-Response (Invalid Parameter):
135
     * HTTP/1.1 400 Bad Request
136
     * {
137
     *      "status": 400,
138
     *      "data": {
139
     *          "error": "Balloon\\Exception\\InvalidArgument",
140
     *          "message": "invalid node id specified",
141
     *          "code": 0
142
     *      }
143
     * }
144
     *
145
     * @apiErrorExample {json} Error-Response (Insufficient Access):
146
     * HTTP/1.1 403 Forbidden
147
     * {
148
     *      "status": 403,
149
     *      "data": {
150
     *          "error": "Balloon\\Exception\\Forbidden",
151
     *          "message": "not allowed to read node 51354d073c58891f058b4580",
152
     *          "code": 40
153
     *      }
154
     * }
155
     *
156
     * @apiErrorExample {json} Error-Response (Not found):
157
     * HTTP/1.1 404 Not Found
158
     * {
159
     *      "status": 404,
160
     *      "data": {
161
     *          "error": "Balloon\\Exception\\NotFound",
162
     *          "message": "node 51354d073c58891f058b4580 not found",
163
     *          "code": 49
164
     *      }
165
     * }
166
     */
167
168
    /**
169
     * Load node with path.
170
     */
171
    public function findNodeByPath(string $path = '', ?string $class = null): NodeInterface
172
    {
173
        if (empty($path) || '/' !== $path[0]) {
174
            $path = '/'.$path;
175
        }
176
177
        $last = strlen($path) - 1;
178
        if ('/' === $path[$last]) {
179
            $path = substr($path, 0, -1);
180
        }
181
182
        $parts = explode('/', $path);
183
        $parent = $this->fs->getRoot();
184
        array_shift($parts);
185
        $count = count($parts);
186
187
        $i = 0;
188
        $filter = [];
189
190
        foreach ($parts as $node) {
191
            ++$i;
192
193
            if ($count === $i && $class !== null) {
194
                $filter = [
195
                    'directory' => ($class === Collection::class),
196
                ];
197
            }
198
199
            try {
200
                $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE, $filter);
201
            } catch (Exception\NotFound $e) {
202
                if ($count == $i) {
203
                    $parent = $parent->getChild($node, NodeInterface::DELETED_INCLUDE, $filter);
204
                } else {
205
                    throw $e;
206
                }
207
            }
208
        }
209
210
        if (null !== $class && !($parent instanceof $class)) {
211
            throw new Exception('node is not instance of '.$class);
212
        }
213
214
        return $parent;
215
    }
216
217
    /**
218
     * Load nodes by id.
219
     */
220
    public function findNodesByPath(array $path = [], ?string $class = null): Generator
221
    {
222
        $find = [];
0 ignored issues
show
Unused Code introduced by
$find is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
223
        foreach ($path as $p) {
0 ignored issues
show
Bug introduced by
The expression $path of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
224
            if (empty($path) || '/' !== $path[0]) {
225
                $path = '/'.$path;
226
            }
227
228
            $last = strlen($path) - 1;
229
            if ('/' === $path[$last]) {
230
                $path = substr($path, 0, -1);
231
            }
232
233
            $parts = explode('/', $path);
234
            $parent = $this->fs->getRoot();
235
            array_shift($parts);
236
            foreach ($parts as $node) {
237
                $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE);
238
            }
239
240
            if (null !== $class && !($parent instanceof $class)) {
241
                throw new Exception('node is not an instance of '.$class);
242
            }
243
244
            yield $parent;
245
        }
246
    }
247
248
    /**
249
     * Load nodes by id.
250
     *
251
     * @param null|mixed $class
252
     */
253
    public function getNodes(?array $id = null, ?array $path = null, $class = null, int $deleted = NodeInterface::DELETED_EXCLUDE): Generator
254
    {
255
        if (null === $id && null === $path) {
256
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
257
        }
258
        if (null !== $id && null !== $path) {
259
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
260
        }
261
        if (null !== $id) {
262
            if (null === $deleted) {
263
                $deleted = NodeInterface::DELETED_INCLUDE;
264
            }
265
266
            return $this->fs->findNodesById($id, $class, $deleted);
267
        }
268
        if (null !== $path) {
269
            if (null === $deleted) {
270
                $deleted = NodeInterface::DELETED_EXCLUDE;
0 ignored issues
show
Unused Code introduced by
$deleted is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
271
            }
272
273
            return $this->findNodesByPath($path, $class);
274
        }
275
    }
276
277
    /**
278
     * Load node.
279
     *
280
     * @param null|mixed $id
281
     * @param null|mixed $path
282
     * @param null|mixed $class
283
     */
284
    public function getNode($id = null, $path = null, $class = null, bool $multiple = false, bool $allow_root = false, ?int $deleted = null): NodeInterface
285
    {
286
        if (empty($id) && empty($path)) {
287
            if (true === $allow_root) {
288
                return $this->fs->getRoot();
289
            }
290
291
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
292
        }
293
        if (null !== $id && null !== $path) {
294
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
295
        }
296
        if (null !== $id) {
297
            if (null === $deleted) {
298
                $deleted = NodeInterface::DELETED_INCLUDE;
299
            }
300
301
            if (true === $multiple && is_array($id)) {
302
                return $this->fs->findNodesById($id, $class, $deleted);
303
            }
304
305
            return $this->fs->findNodeById($id, $class, $deleted);
306
        }
307
        if (null !== $path) {
308
            if (null === $deleted) {
309
                $deleted = NodeInterface::DELETED_EXCLUDE;
0 ignored issues
show
Unused Code introduced by
$deleted is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
310
            }
311
312
            return $this->findNodeByPath($path, $class);
313
        }
314
    }
315
316
    /**
317
     * Do bulk operations.
318
     *
319
     * @param array|string $id
320
     * @param array|string $p
321
     */
322
    protected function bulk($id, $p, Closure $action): Response
323
    {
324
        if (is_array($id) || is_array($p)) {
325
            $body = [];
326
            foreach ($this->_getNodes($id, $p) as $node) {
0 ignored issues
show
Bug introduced by
It seems like $id defined by parameter $id on line 322 can also be of type array; however, Balloon\App\Api\v1\Controller::_getNodes() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $p defined by parameter $p on line 322 can also be of type array; however, Balloon\App\Api\v1\Controller::_getNodes() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
327
                try {
328
                    $body[(string) $node->getId()] = $action->call($this, $node);
329
                } catch (\Exception $e) {
330
                    $body[(string) $node->getId()] = [
331
                        'code' => $e instanceof ExceptionInterface ? $e->getStatusCode() : 400,
0 ignored issues
show
Bug introduced by
The class Micro\Http\ExceptionInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
332
                        'data' => [
333
                            'error' => get_class($e),
334
                            'message' => $e->getMessage(),
335
                            'code' => $e->getCode(),
336
                        ],
337
                    ];
338
                }
339
            }
340
341
            return (new Response())->setCode(207)->setBody($body);
342
        }
343
344
        $body = $action->call($this, $this->_getNode($id, $p));
345
        $response = (new Response())->setCode($body['code']);
346
347
        if (isset($body['data'])) {
348
            $response->setBody($body['data']);
349
        }
350
351
        return $response;
352
    }
353
354
    /**
355
     * Get node.
356
     *
357
     * @param string $id
358
     * @param string $path
359
     * @param string $class      Force set node type
360
     * @param bool   $multiple   Allow $id to be an array
361
     * @param bool   $allow_root Allow instance of root collection
362
     * @param bool   $deleted    How to handle deleted node
363
     */
364
    protected function _getNode(
365
        ?string $id = null,
366
        ?string $path = null,
367
        ?string $class = null,
368
        bool $multiple = false,
369
        bool $allow_root = false,
370
        int $deleted = 2
371
    ): NodeInterface {
372
        if (null === $class) {
373
            switch (get_class($this)) {
374
                case ApiFile::class:
375
                    $class = File::class;
376
377
                break;
378
                case ApiCollection::class:
379
                    $class = Collection::class;
380
381
                break;
382
            }
383
        }
384
385
        return $this->fs->getNode($id, $path, $class, $multiple, $allow_root, $deleted);
0 ignored issues
show
Unused Code introduced by
The call to Filesystem::getNode() has too many arguments starting with $deleted.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
386
    }
387
388
    /**
389
     * Get nodes.
390
     *
391
     * @param string $id
392
     * @param string $path
393
     * @param string $class   Force set node type
394
     * @param bool   $deleted How to handle deleted node
395
     */
396
    protected function _getNodes(
397
        $id = null,
398
        $path = null,
399
        ?string $class = null,
400
        int $deleted = 2
401
    ): Generator {
402
        if (null === $class) {
403
            switch (get_class($this)) {
404
                case ApiFile::class:
405
                    $class = File::class;
406
407
                break;
408
                case ApiCollection::class:
409
                    $class = Collection::class;
410
411
                break;
412
            }
413
        }
414
415
        return $this->fs->getNodes($id, $path, $class, $deleted);
0 ignored issues
show
Unused Code introduced by
The call to Filesystem::getNodes() has too many arguments starting with $deleted.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
416
    }
417
}
418