Passed
Pull Request — master (#68)
by Dmitriy
13:09 queued 10:03
created

InspectController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 0
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 1
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\Controller;
6
7
use Cycle\Database\ColumnInterface;
8
use Cycle\Database\DatabaseProviderInterface;
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 RuntimeException;
18
use SplFileInfo;
19
use Throwable;
20
use Yiisoft\ActiveRecord\ActiveRecordFactory;
21
use Yiisoft\Aliases\Aliases;
22
use Yiisoft\Config\ConfigInterface;
23
use Yiisoft\DataResponse\DataResponseFactoryInterface;
24
use Yiisoft\Translator\CategorySource;
25
use Yiisoft\Db\Connection\ConnectionInterface;
26
use Yiisoft\Db\Schema\ColumnSchemaInterface;
27
use Yiisoft\Db\Schema\TableSchemaInterface;
28
use Yiisoft\Router\CurrentRoute;
29
use Yiisoft\VarDumper\VarDumper;
30
use Yiisoft\Yii\Debug\Api\Inspector\ActiveRecord\Common;
31
use Yiisoft\Yii\Debug\Api\Inspector\ApplicationState;
32
use Yiisoft\Yii\Debug\Api\Inspector\CommandInterface;
33
34
class InspectController
35
{
36
    public function __construct(
37
        private DataResponseFactoryInterface $responseFactory,
38
    ) {
39
    }
40
41
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
42
    {
43
        $config = $container->get(ConfigInterface::class);
44
45
        $request = $request->getQueryParams();
46
        $group = $request['group'] ?? 'web';
47
48
        $data = $config->get($group);
49
        ksort($data);
50
51
        $response = VarDumper::create($data)->asJson(false, 255);
52
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
53
    }
54
55
    public function getTranslations(ContainerInterface $container): ResponseInterface
56
    {
57
        /**
58
         * @var $categorySources CategorySource[]
59
         */
60
        $categorySources = $container->get('[email protected]');
61
62
        $params = ApplicationState::$params;
63
64
        $locales = array_keys($params['locale']['locales']);
65
        if ($locales === []) {
66
            throw new RuntimeException(
67
                'Unable to determine list of available locales. ' .
68
                'Make sure that "$params[\'locale\'][\'locales\']" contains all available locales.'
69
            );
70
        }
71
        $messages = [];
72
        foreach ($categorySources as $categorySource) {
73
            $messages[$categorySource->getName()] = [];
74
75
            try {
76
                foreach ($locales as $locale) {
77
                    $messages[$categorySource->getName()][$locale] = $categorySource->getMessages($locale);
78
                }
79
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80
            }
81
        }
82
83
        $response = VarDumper::create($messages)->asPrimitives(255);
84
        return $this->responseFactory->createResponse($response);
85
    }
86
87
    public function putTranslation(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
88
    {
89
        /**
90
         * @var $categorySources CategorySource[]
91
         */
92
        $categorySources = $container->get('[email protected]');
93
94
        $body = $request->getParsedBody();
95
        $categoryName = $body['category'] ?? '';
96
        $locale = $body['locale'] ?? '';
97
        $translationId = $body['translation'] ?? '';
98
        $newMessage = $body['message'] ?? '';
99
100
        $categorySource = null;
101
        foreach ($categorySources as $possibleCategorySource) {
102
            if ($possibleCategorySource->getName() === $categoryName) {
103
                $categorySource = $possibleCategorySource;
104
            }
105
        }
106
        if ($categorySource === null) {
107
            throw new InvalidArgumentException(
108
                sprintf(
109
                    'Invalid category name "%s". Only the following categories are available: "%s"',
110
                    $categoryName,
111
                    implode(
112
                        '", "',
113
                        array_map(fn (CategorySource $categorySource) => $categorySource->getName(), $categorySources)
114
                    )
115
                )
116
            );
117
        }
118
        $messages = $categorySource->getMessages($locale);
119
        $messages = array_replace_recursive($messages, [
120
            $translationId => [
121
                'message' => $newMessage,
122
            ],
123
        ]);
124
        $categorySource->write($locale, $messages);
125
126
        $result = [$locale => $messages];
127
        $response = VarDumper::create($result)->asPrimitives(255);
128
        return $this->responseFactory->createResponse($response);
129
    }
130
131
    public function params(): ResponseInterface
132
    {
133
        $params = ApplicationState::$params;
134
        ksort($params);
135
136
        return $this->responseFactory->createResponse($params);
137
    }
138
139
    public function files(Aliases $aliases, ServerRequestInterface $request): ResponseInterface
140
    {
141
        $request = $request->getQueryParams();
142
        $path = $request['path'] ?? '';
143
144
        $rootPath = $aliases->get('@root');
145
146
        $destination = $this->removeBasePath($rootPath, $path);
147
148
        if (!str_starts_with('/', $destination)) {
149
            $destination = '/' . $destination;
150
        }
151
152
        $destination = realpath($rootPath . $destination);
153
154
        if (!file_exists($destination)) {
155
            throw new InvalidArgumentException(sprintf('Destination "%s" does not exist', $destination));
156
        }
157
158
        if (!is_dir($destination)) {
159
            $file = new SplFileInfo($destination);
160
            return $this->responseFactory->createResponse(
161
                array_merge(
162
                    [
163
                        'directory' => $this->removeBasePath($rootPath, dirname($destination)),
164
                        'content' => file_get_contents($destination),
165
                        'path' => $this->removeBasePath($rootPath, $destination),
166
                        'absolutePath' => $destination,
167
                    ],
168
                    $this->serializeFileInfo($file)
169
                )
170
            );
171
        }
172
173
        /**
174
         * @var $directoryIterator SplFileInfo[]
175
         */
176
        $directoryIterator = new RecursiveDirectoryIterator(
177
            $destination,
178
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
179
        );
180
181
        $files = [];
182
        foreach ($directoryIterator as $file) {
183
            if ($file->getBasename() === '.') {
184
                continue;
185
            }
186
187
            $path = $file->getPathName();
188
            if ($file->isDir()) {
189
                if ($file->getBasename() === '..') {
190
                    $path = realpath($path);
191
                }
192
                $path .= '/';
193
            }
194
            /**
195
             * Check if path is inside the application directory
196
             */
197
            if (!str_starts_with($path, $rootPath)) {
198
                continue;
199
            }
200
            $path = $this->removeBasePath($rootPath, $path);
201
            $files[] = array_merge(
202
                [
203
                    'path' => $path,
204
                ],
205
                $this->serializeFileInfo($file)
206
            );
207
        }
208
209
        return $this->responseFactory->createResponse($files);
210
    }
211
212
    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

212
    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...
213
    {
214
        // TODO: how to get params for console or other param groups?
215
        $classes = [];
216
217
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
218
        // TODO: think how to ignore heavy objects
219
        $patterns = [
220
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
221
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
222
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
223
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
224
            fn (string $class) => !str_contains($class, '@anonymous'),
225
            fn (string $class) => !is_subclass_of($class, Throwable::class),
226
        ];
227
        foreach ($patterns as $patternFunction) {
228
            $inspected = array_filter($inspected, $patternFunction);
229
        }
230
231
        foreach ($inspected as $className) {
232
            $class = new ReflectionClass($className);
233
234
            if ($class->isInternal()) {
235
                continue;
236
            }
237
238
            $classes[] = $className;
239
        }
240
        sort($classes);
241
242
        return $this->responseFactory->createResponse($classes);
243
    }
244
245
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
246
    {
247
        $queryParams = $request->getQueryParams();
248
        $className = $queryParams['classname'];
249
250
        $reflection = new ReflectionClass($className);
251
252
        if ($reflection->isInternal()) {
253
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
254
        }
255
        if ($reflection->implementsInterface(Throwable::class)) {
256
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
257
        }
258
259
        $variable = $container->get($className);
260
        $result = VarDumper::create($variable)->asJson(false, 3);
261
262
        return $this->responseFactory->createResponse([
263
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
264
            'path' => $reflection->getFileName(),
265
        ]);
266
    }
267
268
    public function getCommands(ConfigInterface $config): ResponseInterface
269
    {
270
        $params = $config->get('params');
271
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
272
273
        $result = [];
274
        foreach ($commandMap as $groupName => $commands) {
275
            foreach ($commands as $name => $command) {
276
                if (!is_subclass_of($command, CommandInterface::class)) {
277
                    continue;
278
                }
279
                $result[] = [
280
                    'name' => $name,
281
                    'title' => $command::getTitle(),
282
                    'group' => $groupName,
283
                    'description' => $command::getDescription(),
284
                ];
285
            }
286
        }
287
288
        return $this->responseFactory->createResponse($result);
289
    }
290
291
    public function runCommand(
292
        ServerRequestInterface $request,
293
        ContainerInterface $container,
294
        ConfigInterface $config
295
    ): ResponseInterface {
296
        $params = $config->get('params');
297
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
298
299
        /**
300
         * @var array<string, class-string<CommandInterface>> $commandList
301
         */
302
        $commandList = [];
303
        foreach ($commandMap as $commands) {
304
            foreach ($commands as $name => $command) {
305
                if (!is_subclass_of($command, CommandInterface::class)) {
306
                    continue;
307
                }
308
                $commandList[$name] = $command;
309
            }
310
        }
311
312
        $request = $request->getQueryParams();
313
        $commandName = $request['command'] ?? null;
314
315
        if ($commandName === null) {
316
            throw new InvalidArgumentException(
317
                sprintf(
318
                    'Command must not be null. Available commands: "%s".',
319
                    implode('", "', $commandList)
320
                )
321
            );
322
        }
323
324
        if (!array_key_exists($commandName, $commandList)) {
325
            throw new InvalidArgumentException(
326
                sprintf(
327
                    'Unknown command "%s". Available commands: "%s".',
328
                    $commandName,
329
                    implode('", "', $commandList)
330
                )
331
            );
332
        }
333
334
        $commandClass = $commandList[$commandName];
335
        /**
336
         * @var $command CommandInterface
337
         */
338
        $command = $container->get($commandClass);
339
340
        $result = $command->run();
341
342
        return $this->responseFactory->createResponse([
343
            'status' => $result->getStatus(),
344
            'result' => $result->getResult(),
345
            'error' => $result->getErrors(),
346
        ]);
347
    }
348
349
    public function getTables(
350
        ContainerInterface $container,
351
        ActiveRecordFactory $arFactory,
352
    ): ResponseInterface {
353
        if ($container->has(DatabaseProviderInterface::class)) {
354
            $databaseProvider = $container->get(DatabaseProviderInterface::class);
355
            $database = $databaseProvider->database();
356
            $tableSchemas = $database->getTables();
357
358
            $tables = [];
359
            foreach ($tableSchemas as $schema) {
360
                $records = $database->select()->from($schema->getName())->count();
361
                $tables[] = [
362
                    'table' => $schema->getName(),
363
                    'primaryKeys' => $schema->getPrimaryKeys(),
364
                    'columns' => $this->serializeCycleColumnsSchemas($schema->getColumns()),
365
                    'records' => $records,
366
                ];
367
            }
368
369
            return $this->responseFactory->createResponse($tables);
370
        }
371
372
        if ($container->has(ConnectionInterface::class)) {
373
            $connection = $container->get(ConnectionInterface::class);
374
            /** @var TableSchemaInterface[] $tableSchemas */
375
            $tableSchemas = $connection->getSchema()->getTableSchemas();
376
377
            $tables = [];
378
            foreach ($tableSchemas as $schema) {
379
                $activeQuery = $arFactory->createQueryTo(Common::class, $schema->getName());
380
381
                /**
382
                 * @var Common[] $records
383
                 */
384
                $records = $activeQuery->count();
385
386
                $tables[] = [
387
                    'table' => $schema->getName(),
388
                    'primaryKeys' => $schema->getPrimaryKey(),
389
                    'columns' => $this->serializeARColumnsSchemas($schema->getColumns()),
390
                    'records' => $records,
391
                ];
392
            }
393
394
            return $this->responseFactory->createResponse($tables);
395
        }
396
397
        throw new LogicException(sprintf(
398
            'Inspecting database is not available. Configure "%s" service to be able to inspect database.',
399
            ConnectionInterface::class,
400
        ));
401
    }
402
403
    public function getTable(
404
        ContainerInterface $container,
405
        ActiveRecordFactory $arFactory,
406
        CurrentRoute $currentRoute,
407
    ): ResponseInterface {
408
        $tableName = $currentRoute->getArgument('name');
409
410
        if ($container->has(DatabaseProviderInterface::class)) {
411
            $databaseProvider = $container->get(DatabaseProviderInterface::class);
412
            $database = $databaseProvider->database();
413
            $schema = $database->table($tableName);
414
415
            $result = [
416
                'table' => $schema->getName(),
417
                'primaryKeys' => $schema->getPrimaryKeys(),
418
                'columns' => $this->serializeCycleColumnsSchemas($schema->getColumns()),
419
                'records' => $database->select()->from($tableName)->fetchAll(),
420
            ];
421
422
            return $this->responseFactory->createResponse($result);
423
        }
424
425
        if ($container->has(ConnectionInterface::class)) {
426
            $connection = $container->get(ConnectionInterface::class);
427
            /** @var TableSchemaInterface[] $tableSchemas */
428
            $schema = $connection->getSchema()->getTableSchema($tableName);
429
430
            $activeQuery = $arFactory->createQueryTo(Common::class, $schema->getName());
431
432
            /**
433
             * @var Common[] $records
434
             */
435
            $records = $activeQuery->all();
436
437
            $data = [];
438
            // TODO: add pagination
439
            foreach ($records as $n => $record) {
440
                foreach ($record->attributes() as $attribute) {
441
                    $data[$n][$attribute] = $record->{$attribute};
442
                }
443
            }
444
445
            $result = [
446
                'table' => $schema->getName(),
447
                'primaryKeys' => $schema->getPrimaryKey(),
448
                'columns' => $this->serializeARColumnsSchemas($schema->getColumns()),
449
                'records' => $data,
450
            ];
451
452
            return $this->responseFactory->createResponse($result);
453
        }
454
455
        throw new LogicException(sprintf(
456
            'Inspecting database is not available. Configure "%s" service to be able to inspect database.',
457
            ConnectionInterface::class,
458
        ));
459
    }
460
461
    private function removeBasePath(string $rootPath, string $path): string|array|null
462
    {
463
        return preg_replace(
464
            '/^' . preg_quote($rootPath, '/') . '/',
465
            '',
466
            $path,
467
            1
468
        );
469
    }
470
471
    private function getUserOwner(int $uid): array
472
    {
473
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
474
            return [
475
                'id' => $uid,
476
            ];
477
        }
478
        return [
479
            'uid' => $info['uid'],
480
            'gid' => $info['gid'],
481
            'name' => $info['name'],
482
        ];
483
    }
484
485
    private function getGroupOwner(int $gid): array
486
    {
487
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
488
            return [
489
                'id' => $gid,
490
            ];
491
        }
492
        return [
493
            'gid' => $info['gid'],
494
            'name' => $info['name'],
495
        ];
496
    }
497
498
    private function serializeFileInfo(SplFileInfo $file): array
499
    {
500
        return [
501
            'baseName' => $file->getBasename(),
502
            'extension' => $file->getExtension(),
503
            'user' => $this->getUserOwner((int) $file->getOwner()),
504
            'group' => $this->getGroupOwner((int) $file->getGroup()),
505
            'size' => $file->getSize(),
506
            'type' => $file->getType(),
507
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
508
        ];
509
    }
510
511
    /**
512
     * @param ColumnSchemaInterface[] $columns
513
     */
514
    private function serializeARColumnsSchemas(array $columns): array
515
    {
516
        $result = [];
517
        foreach ($columns as $columnSchema) {
518
            $result[] = [
519
                'name' => $columnSchema->getName(),
520
                'size' => $columnSchema->getSize(),
521
                'type' => $columnSchema->getType(),
522
                'dbType' => $columnSchema->getDbType(),
523
                'defaultValue' => $columnSchema->getDefaultValue(),
524
                'comment' => $columnSchema->getComment(),
525
                'allowNull' => $columnSchema->isAllowNull(),
526
            ];
527
        }
528
        return $result;
529
    }
530
531
    /**
532
     * @param ColumnInterface[] $columns
533
     */
534
    private function serializeCycleColumnsSchemas(array $columns): array
535
    {
536
        $result = [];
537
        foreach ($columns as $columnSchema) {
538
            $result[] = [
539
                'name' => $columnSchema->getName(),
540
                'size' => $columnSchema->getSize(),
541
                'type' => $columnSchema->getInternalType(),
542
                'dbType' => $columnSchema->getType(),
543
                'defaultValue' => $columnSchema->getDefaultValue(),
544
                'comment' => null, // unsupported for now
545
                'allowNull' => $columnSchema->isNullable(),
546
            ];
547
        }
548
        return $result;
549
    }
550
}
551