Passed
Pull Request — master (#68)
by Dmitriy
12:07
created

InspectController::getCommands()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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