Passed
Pull Request — master (#68)
by Dmitriy
03:02
created

InspectController::files()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 71
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 37
c 5
b 1
f 0
dl 0
loc 71
ccs 0
cts 34
cp 0
rs 7.7724
cc 9
nc 20
nop 2
crap 90

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\Controller;
6
7
use FilesystemIterator;
8
use InvalidArgumentException;
9
use LogicException;
10
use Psr\Container\ContainerInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use RecursiveDirectoryIterator;
14
use ReflectionClass;
15
use SplFileInfo;
16
use Throwable;
17
use Yiisoft\ActiveRecord\ActiveRecordFactory;
0 ignored issues
show
Bug introduced by
The type Yiisoft\ActiveRecord\ActiveRecordFactory 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...
18
use Yiisoft\Aliases\Aliases;
19
use Yiisoft\Config\ConfigInterface;
20
use Yiisoft\DataResponse\DataResponseFactoryInterface;
21
use Yiisoft\Db\Connection\ConnectionInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Connection\ConnectionInterface 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...
22
use Yiisoft\Db\Schema\ColumnSchemaInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\ColumnSchemaInterface 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...
23
use Yiisoft\Db\Schema\TableSchemaInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\TableSchemaInterface 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...
24
use Yiisoft\Router\CurrentRoute;
25
use Yiisoft\VarDumper\VarDumper;
26
use Yiisoft\Yii\Debug\Api\Inspector\ActiveRecord\Common;
27
use Yiisoft\Yii\Debug\Api\Inspector\ApplicationState;
28
use Yiisoft\Yii\Debug\Api\Inspector\CommandInterface;
29
30
class InspectController
31
{
32
    public function __construct(
33
        private DataResponseFactoryInterface $responseFactory,
34
    ) {
35
    }
36
37
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
38
    {
39
        $config = $container->get(ConfigInterface::class);
40
41
        $request = $request->getQueryParams();
42
        $group = $request['group'] ?? 'web';
43
44
        $data = $config->get($group);
45
        ksort($data);
46
47
        $response = VarDumper::create($data)->asJson(false, 255);
48
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
49
    }
50
51
    public function params(): ResponseInterface
52
    {
53
        $params = ApplicationState::$params;
54
        ksort($params);
55
56
        return $this->responseFactory->createResponse($params);
57
    }
58
59
    public function files(Aliases $aliases, ServerRequestInterface $request): ResponseInterface
60
    {
61
        $request = $request->getQueryParams();
62
        $path = $request['path'] ?? '';
63
64
        $rootPath = $aliases->get('@root');
65
66
        $destination = $this->removeBasePath($rootPath, $path);
67
68
        if (!str_starts_with('/', $destination)) {
69
            $destination = '/' . $destination;
70
        }
71
72
        $destination = realpath($rootPath . $destination);
73
74
        if (!file_exists($destination)) {
75
            throw new InvalidArgumentException(sprintf('Destination "%s" does not exist', $destination));
76
        }
77
78
        if (!is_dir($destination)) {
79
            $file = new SplFileInfo($destination);
80
            return $this->responseFactory->createResponse(
81
                array_merge(
82
                    [
83
                        'directory' => $this->removeBasePath($rootPath, dirname($destination)),
84
                        'content' => file_get_contents($destination),
85
                        'path' => $this->removeBasePath($rootPath, $destination),
86
                        'absolutePath' => $destination,
87
                    ],
88
                    $this->serializeFileInfo($file)
89
                )
90
            );
91
        }
92
93
        /**
94
         * @var $directoryIterator SplFileInfo[]
95
         */
96
        $directoryIterator = new RecursiveDirectoryIterator(
97
            $destination,
98
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
99
        );
100
101
        $files = [];
102
        foreach ($directoryIterator as $file) {
103
            if ($file->getBasename() === '.') {
104
                continue;
105
            }
106
107
            $path = $file->getPathName();
108
            if ($file->isDir()) {
109
                if ($file->getBasename() === '..') {
110
                    $path = realpath($path);
111
                }
112
                $path .= '/';
113
            }
114
            /**
115
             * Check if path is inside the application directory
116
             */
117
            if (!str_starts_with($path, $rootPath)) {
118
                continue;
119
            }
120
            $path = $this->removeBasePath($rootPath, $path);
121
            $files[] = array_merge(
122
                [
123
                    'path' => $path,
124
                ],
125
                $this->serializeFileInfo($file)
126
            );
127
        }
128
129
        return $this->responseFactory->createResponse($files);
130
    }
131
132
    public function classes(ContainerInterface $container): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $container is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

132
    public function classes(/** @scrutinizer ignore-unused */ ContainerInterface $container): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
133
    {
134
        // TODO: how to get params for console or other param groups?
135
        $classes = [];
136
137
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
138
        // TODO: think how to ignore heavy objects
139
        $patterns = [
140
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
141
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
142
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
143
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
144
            fn (string $class) => !str_contains($class, '@anonymous'),
145
            fn (string $class) => !is_subclass_of($class, Throwable::class),
146
        ];
147
        foreach ($patterns as $patternFunction) {
148
            $inspected = array_filter($inspected, $patternFunction);
149
        }
150
151
        foreach ($inspected as $className) {
152
            $class = new ReflectionClass($className);
153
154
            if ($class->isInternal()) {
155
                continue;
156
            }
157
158
            $classes[] = $className;
159
        }
160
        sort($classes);
161
162
        return $this->responseFactory->createResponse($classes);
163
    }
164
165
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
166
    {
167
        $queryParams = $request->getQueryParams();
168
        $className = $queryParams['classname'];
169
170
        $reflection = new ReflectionClass($className);
171
172
        if ($reflection->isInternal()) {
173
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
174
        }
175
        if ($reflection->implementsInterface(Throwable::class)) {
176
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
177
        }
178
179
        $variable = $container->get($className);
180
        $result = VarDumper::create($variable)->asJson(false, 3);
181
182
        return $this->responseFactory->createResponse([
183
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
184
            'path' => $reflection->getFileName(),
185
        ]);
186
    }
187
188
    public function getCommands(ConfigInterface $config): ResponseInterface
189
    {
190
        $params = $config->get('params');
191
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
192
193
        $result = [];
194
        foreach ($commandMap as $groupName => $commands) {
195
            foreach ($commands as $name => $command) {
196
                if (!is_subclass_of($command, CommandInterface::class)) {
197
                    continue;
198
                }
199
                $result[] = [
200
                    'name' => $name,
201
                    'title' => $command::getTitle(),
202
                    'group' => $groupName,
203
                    'description' => $command::getDescription(),
204
                ];
205
            }
206
        }
207
208
        return $this->responseFactory->createResponse($result);
209
    }
210
211
    public function runCommand(
212
        ServerRequestInterface $request,
213
        ContainerInterface $container,
214
        ConfigInterface $config
215
    ): ResponseInterface {
216
        $params = $config->get('params');
217
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
218
219
        /**
220
         * @var array<string, class-string<CommandInterface>> $commandList
221
         */
222
        $commandList = [];
223
        foreach ($commandMap as $commands) {
224
            foreach ($commands as $name => $command) {
225
                if (!is_subclass_of($command, CommandInterface::class)) {
226
                    continue;
227
                }
228
                $commandList[$name] = $command;
229
            }
230
        }
231
232
        $request = $request->getQueryParams();
233
        $commandName = $request['command'] ?? null;
234
235
        if ($commandName === null) {
236
            throw new InvalidArgumentException(
237
                sprintf(
238
                    'Command must not be null. Available commands: "%s".',
239
                    implode('", "', $commandList)
240
                )
241
            );
242
        }
243
244
        if (!array_key_exists($commandName, $commandList)) {
245
            throw new InvalidArgumentException(
246
                sprintf(
247
                    'Unknown command "%s". Available commands: "%s".',
248
                    $commandName,
249
                    implode('", "', $commandList)
250
                )
251
            );
252
        }
253
254
        $commandClass = $commandList[$commandName];
255
        /**
256
         * @var $command CommandInterface
257
         */
258
        $command = $container->get($commandClass);
259
260
        $result = $command->run();
261
262
        return $this->responseFactory->createResponse([
263
            'status' => $result->getStatus(),
264
            'result' => $result->getResult(),
265
            'error' => $result->getErrors(),
266
        ]);
267
    }
268
269
    public function getTables(
270
        ContainerInterface $container,
271
        ActiveRecordFactory $arFactory,
272
    ): ResponseInterface {
273
        if ($container->has(ConnectionInterface::class)) {
274
            $connection = $container->get(ConnectionInterface::class);
275
            $r = $connection->getSchema();
276
            /** @var TableSchemaInterface[] $tableSchemas */
277
            $tableSchemas = $r->getTableSchemas();
278
279
            $tables = [];
280
            foreach ($tableSchemas as $schema) {
281
                $activeQuery = $arFactory->createQueryTo(Common::class, $schema->getName());
282
283
                /**
284
                 * @var Common[] $records
285
                 */
286
                $records = $activeQuery->count();
287
288
                $tables[] = [
289
                    'table' => $schema->getName(),
290
                    'primaryKeys' => $schema->getPrimaryKey(),
291
                    'columns' => $this->serializeARColumnsSchemas($schema->getColumns()),
292
                    'records' => $records,
293
                ];
294
            }
295
296
            return $this->responseFactory->createResponse($tables);
297
        }
298
299
        throw new LogicException(sprintf(
300
            'Inspecting database is not available. Configure "%s" service to be able to inspect database.',
301
            ConnectionInterface::class,
302
        ));
303
    }
304
305
    public function getTable(
306
        ContainerInterface $container,
307
        ActiveRecordFactory $arFactory,
308
        CurrentRoute $currentRoute,
309
    ): ResponseInterface {
310
        $tableName = $currentRoute->getArgument('name');
311
        if ($container->has(ConnectionInterface::class)) {
312
            $connection = $container->get(ConnectionInterface::class);
313
            $r = $connection->getSchema();
314
            /** @var TableSchemaInterface[] $tableSchemas */
315
            $schema = $r->getTableSchema($tableName);
316
317
            $activeQuery = $arFactory->createQueryTo(Common::class, $schema->getName());
318
319
            /**
320
             * @var Common[] $records
321
             */
322
            $records = $activeQuery->all();
323
324
            $data = [];
325
            // TODO: add pagination
326
            foreach ($records as $n => $record) {
327
                foreach ($record->attributes() as $attribute) {
328
                    $data[$n][$attribute] = $record->{$attribute};
329
                }
330
            }
331
332
            $result = [
333
                'table' => $schema->getName(),
334
                'primaryKeys' => $schema->getPrimaryKey(),
335
                'columns' => $this->serializeARColumnsSchemas($schema->getColumns()),
336
                'records' => $data,
337
            ];
338
339
            return $this->responseFactory->createResponse($result);
340
        }
341
342
        throw new LogicException(sprintf(
343
            'Inspecting database is not available. Configure "%s" service to be able to inspect database.',
344
            ConnectionInterface::class,
345
        ));
346
    }
347
348
    private function removeBasePath(string $rootPath, string $path): string|array|null
349
    {
350
        return preg_replace(
351
            '/^' . preg_quote($rootPath, '/') . '/',
352
            '',
353
            $path,
354
            1
355
        );
356
    }
357
358
    private function getUserOwner(int $uid): array
359
    {
360
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
361
            return [
362
                'id' => $uid,
363
            ];
364
        }
365
        return [
366
            'uid' => $info['uid'],
367
            'gid' => $info['gid'],
368
            'name' => $info['name'],
369
        ];
370
    }
371
372
    private function getGroupOwner(int $gid): array
373
    {
374
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
375
            return [
376
                'id' => $gid,
377
            ];
378
        }
379
        return [
380
            'gid' => $info['gid'],
381
            'name' => $info['name'],
382
        ];
383
    }
384
385
    private function serializeFileInfo(SplFileInfo $file): array
386
    {
387
        return [
388
            'baseName' => $file->getBasename(),
389
            'extension' => $file->getExtension(),
390
            'user' => $this->getUserOwner((int) $file->getOwner()),
391
            'group' => $this->getGroupOwner((int) $file->getGroup()),
392
            'size' => $file->getSize(),
393
            'type' => $file->getType(),
394
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
395
        ];
396
    }
397
398
    /**
399
     * @param ColumnSchemaInterface[] $columns
400
     *
401
     * @return array
402
     */
403
    private function serializeARColumnsSchemas(array $columns): array
404
    {
405
        $result = [];
406
        foreach ($columns as $columnSchema) {
407
            $result[] = [
408
                'name' => $columnSchema->getName(),
409
                'size' => $columnSchema->getSize(),
410
                'type' => $columnSchema->getType(),
411
                'dbType' => $columnSchema->getDbType(),
412
                'defaultValue' => $columnSchema->getDefaultValue(),
413
                'comment' => $columnSchema->getComment(),
414
                'allowNull' => $columnSchema->isAllowNull(),
415
            ];
416
        }
417
        return $result;
418
    }
419
}
420