Passed
Pull Request — master (#128)
by Dmitriy
04:21 queued 01:20
created

InspectController   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 26
Bugs 2 Features 0
Metric Value
wmc 62
eloc 253
c 26
b 2
f 0
dl 0
loc 475
ccs 0
cts 303
cp 0
rs 3.44

22 Methods

Rating   Name   Duplication   Size   Complexity  
C files() 0 81 14
A putTranslation() 0 42 4
A eventListeners() 0 9 1
A classes() 0 31 6
A getTables() 0 3 1
A params() 0 6 1
A request() 0 18 1
A routes() 0 18 2
A getTable() 0 5 1
A phpinfo() 0 8 1
A object() 0 20 3
A checkRoute() 0 40 5
A __construct() 0 4 1
A session() 0 10 1
B getTranslations() 0 36 6
A config() 0 13 1
A readFile() 0 14 1
A getGroupOwner() 0 10 4
A removeBasePath() 0 7 1
A serializeFileInfo() 0 10 1
A buildCurl() 0 26 2
A getUserOwner() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like InspectController 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.

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 InspectController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\Controller;
6
7
use Alexkart\CurlBuilder\Command;
8
use FilesystemIterator;
9
use GuzzleHttp\Client;
10
use GuzzleHttp\Psr7\Message;
11
use InvalidArgumentException;
12
use Psr\Container\ContainerInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\ServerRequestFactoryInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
use RecursiveDirectoryIterator;
17
use ReflectionClass;
18
use RuntimeException;
19
use SplFileInfo;
20
use Throwable;
21
use Yiisoft\Aliases\Aliases;
22
use Yiisoft\Config\ConfigInterface;
23
use Yiisoft\DataResponse\DataResponse;
24
use Yiisoft\DataResponse\DataResponseFactoryInterface;
25
use Yiisoft\Http\Method;
26
use Yiisoft\Router\CurrentRoute;
27
use Yiisoft\Router\RouteCollectionInterface;
28
use Yiisoft\Router\UrlMatcherInterface;
29
use Yiisoft\Session\SessionInterface;
30
use Yiisoft\Translator\CategorySource;
31
use Yiisoft\VarDumper\VarDumper;
32
use Yiisoft\Yii\Debug\Api\Inspector\ApplicationState;
33
use Yiisoft\Yii\Debug\Api\Inspector\Database\SchemaProviderInterface;
34
use Yiisoft\Yii\Debug\Api\Repository\CollectorRepositoryInterface;
35
use Yiisoft\Yii\Debug\Collector\Web\RequestCollector;
36
37
class InspectController
38
{
39
    public function __construct(
40
        private DataResponseFactoryInterface $responseFactory,
41
        private Aliases $aliases,
42
    ) {
43
    }
44
45
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
46
    {
47
        $config = $container->get(ConfigInterface::class);
48
49
        $request = $request->getQueryParams();
50
        $group = $request['group'] ?? 'di';
51
52
        $data = $config->get($group);
53
        ksort($data);
54
55
        $response = VarDumper::create($data)->asPrimitives(255);
56
57
        return $this->responseFactory->createResponse($response);
58
    }
59
60
    public function getTranslations(ContainerInterface $container): ResponseInterface
61
    {
62
        /** @var CategorySource[] $categorySources */
63
        $categorySources = $container->get('[email protected]');
64
65
        $params = ApplicationState::$params;
66
67
        /** @var string[] $locales */
68
        $locales = array_keys($params['locale']['locales']);
69
        if ($locales === []) {
70
            throw new RuntimeException(
71
                'Unable to determine list of available locales. ' .
72
                'Make sure that "$params[\'locale\'][\'locales\']" contains all available locales.'
73
            );
74
        }
75
        $messages = [];
76
        foreach ($categorySources as $categorySource) {
77
            $categoryName = $categorySource->getName();
78
79
            if (!isset($messages[$categoryName])) {
80
                $messages[$categoryName] = [];
81
            }
82
83
            try {
84
                foreach ($locales as $locale) {
85
                    $messages[$categoryName][$locale] = array_merge(
86
                        $messages[$categoryName][$locale] ?? [],
87
                        $categorySource->getMessages($locale)
88
                    );
89
                }
90
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
91
            }
92
        }
93
94
        $response = VarDumper::create($messages)->asPrimitives(255);
95
        return $this->responseFactory->createResponse($response);
96
    }
97
98
    public function putTranslation(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
99
    {
100
        /**
101
         * @var CategorySource[] $categorySources
102
         */
103
        $categorySources = $container->get('[email protected]');
104
105
        $body = \json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
106
        $categoryName = $body['category'] ?? '';
107
        $locale = $body['locale'] ?? '';
108
        $translationId = $body['translation'] ?? '';
109
        $newMessage = $body['message'] ?? '';
110
111
        $categorySource = null;
112
        foreach ($categorySources as $possibleCategorySource) {
113
            if ($possibleCategorySource->getName() === $categoryName) {
114
                $categorySource = $possibleCategorySource;
115
            }
116
        }
117
        if ($categorySource === null) {
118
            throw new InvalidArgumentException(
119
                sprintf(
120
                    'Invalid category name "%s". Only the following categories are available: "%s"',
121
                    $categoryName,
122
                    implode(
123
                        '", "',
124
                        array_map(fn (CategorySource $categorySource) => $categorySource->getName(), $categorySources)
125
                    )
126
                )
127
            );
128
        }
129
        $messages = $categorySource->getMessages($locale);
130
        $messages = array_replace_recursive($messages, [
131
            $translationId => [
132
                'message' => $newMessage,
133
            ],
134
        ]);
135
        $categorySource->write($locale, $messages);
136
137
        $result = [$locale => $messages];
138
        $response = VarDumper::create($result)->asPrimitives(255);
139
        return $this->responseFactory->createResponse($response);
140
    }
141
142
    public function params(): ResponseInterface
143
    {
144
        $params = ApplicationState::$params;
145
        ksort($params);
146
147
        return $this->responseFactory->createResponse($params);
148
    }
149
150
    public function files(ServerRequestInterface $request): ResponseInterface
151
    {
152
        $request = $request->getQueryParams();
153
        $class = $request['class'] ?? '';
154
        $method = $request['method'] ?? '';
155
156
        if (!empty($class) && class_exists($class)) {
157
            $reflection = new ReflectionClass($class);
158
            $destination = $reflection->getFileName();
159
            if ($method !== '' && $reflection->hasMethod($method)) {
160
                $reflectionMethod = $reflection->getMethod($method);
161
                $startLine = $reflectionMethod->getStartLine();
162
                $endLine = $reflectionMethod->getEndLine();
163
            }
164
            if ($destination === false) {
165
                return $this->responseFactory->createResponse([
166
                    'message' => sprintf('Cannot find source of class "%s".', $class),
167
                ], 404);
168
            }
169
            return $this->readFile($destination, [
170
                'startLine' => $startLine ?? null,
171
                'endLine' => $endLine ?? null,
172
            ]);
173
        }
174
175
        $path = $request['path'] ?? '';
176
177
        $rootPath = $this->aliases->get('@root');
178
179
        $destination = $this->removeBasePath($rootPath, $path);
180
181
        if (!str_starts_with($destination, '/')) {
182
            $destination = '/' . $destination;
183
        }
184
185
        $destination = realpath($rootPath . $destination);
186
187
        if ($destination === false) {
188
            return $this->responseFactory->createResponse([
189
                'message' => sprintf('Destination "%s" does not exist', $path),
190
            ], 404);
191
        }
192
193
        if (!is_dir($destination)) {
194
            return $this->readFile($destination);
195
        }
196
197
        $directoryIterator = new RecursiveDirectoryIterator(
198
            $destination,
199
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
200
        );
201
202
        $files = [];
203
        foreach ($directoryIterator as $file) {
204
            if ($file->getBasename() === '.') {
205
                continue;
206
            }
207
208
            $path = $file->getPathName();
209
            if ($file->isDir()) {
210
                if ($file->getBasename() === '..') {
211
                    $path = realpath($path);
212
                }
213
                $path .= '/';
214
            }
215
            /**
216
             * Check if path is inside the application directory
217
             */
218
            if (!str_starts_with($path, $rootPath)) {
219
                continue;
220
            }
221
            $path = $this->removeBasePath($rootPath, $path);
222
            $files[] = array_merge(
223
                [
224
                    'path' => $path,
225
                ],
226
                $this->serializeFileInfo($file)
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type string; however, parameter $file of Yiisoft\Yii\Debug\Api\Co...er::serializeFileInfo() does only seem to accept SplFileInfo, maybe add an additional type check? ( Ignorable by Annotation )

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

226
                $this->serializeFileInfo(/** @scrutinizer ignore-type */ $file)
Loading history...
227
            );
228
        }
229
230
        return $this->responseFactory->createResponse($files);
231
    }
232
233
    public function classes(): ResponseInterface
234
    {
235
        // TODO: how to get params for console or other param groups?
236
        $classes = [];
237
238
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
239
        // TODO: think how to ignore heavy objects
240
        $patterns = [
241
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
242
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
243
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
244
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
245
            fn (string $class) => !str_contains($class, '@anonymous'),
246
            fn (string $class) => !is_subclass_of($class, Throwable::class),
247
        ];
248
        foreach ($patterns as $patternFunction) {
249
            $inspected = array_filter($inspected, $patternFunction);
250
        }
251
252
        foreach ($inspected as $className) {
253
            $class = new ReflectionClass($className);
254
255
            if ($class->isInternal() || $class->isAbstract() || $class->isAnonymous()) {
256
                continue;
257
            }
258
259
            $classes[] = $className;
260
        }
261
        sort($classes);
262
263
        return $this->responseFactory->createResponse($classes);
264
    }
265
266
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
267
    {
268
        $queryParams = $request->getQueryParams();
269
        $className = $queryParams['classname'];
270
271
        $reflection = new ReflectionClass($className);
272
273
        if ($reflection->isInternal()) {
274
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
275
        }
276
        if ($reflection->implementsInterface(Throwable::class)) {
277
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
278
        }
279
280
        $variable = $container->get($className);
281
        $result = VarDumper::create($variable)->asPrimitives(3);
282
283
        return $this->responseFactory->createResponse([
284
            'object' => $result,
285
            'path' => $reflection->getFileName(),
286
        ]);
287
    }
288
289
    public function phpinfo(): ResponseInterface
290
    {
291
        ob_start();
292
        phpinfo();
293
        $phpinfo = ob_get_contents();
294
        ob_get_clean();
295
296
        return $this->responseFactory->createResponse($phpinfo);
297
    }
298
299
    public function routes(RouteCollectionInterface $routeCollection): ResponseInterface
300
    {
301
        $routes = [];
302
        foreach ($routeCollection->getRoutes() as $route) {
303
            $data = $route->__debugInfo();
304
            $routes[] = [
305
                'name' => $data['name'],
306
                'hosts' => $data['hosts'],
307
                'pattern' => $data['pattern'],
308
                'methods' => $data['methods'],
309
                'defaults' => $data['defaults'],
310
                'override' => $data['override'],
311
                'middlewares' => $data['middlewareDefinitions'],
312
            ];
313
        }
314
        $response = VarDumper::create($routes)->asPrimitives(5);
315
316
        return $this->responseFactory->createResponse($response);
317
    }
318
319
    public function checkRoute(
320
        ServerRequestInterface $request,
321
        UrlMatcherInterface $matcher,
322
        ServerRequestFactoryInterface $serverRequestFactory
323
    ): ResponseInterface {
324
        $queryParams = $request->getQueryParams();
325
        $path = $queryParams['route'] ?? null;
326
        if ($path === null) {
327
            return $this->responseFactory->createResponse([
328
                'message' => 'Path is not specified.',
329
            ], 422);
330
        }
331
        $path = trim($path);
332
333
        $method = 'GET';
334
        if (str_contains($path, ' ')) {
335
            [$possibleMethod, $restPath] = explode(' ', $path, 2);
336
            if (in_array($possibleMethod, Method::ALL, true)) {
337
                $method = $possibleMethod;
338
                $path = $restPath;
339
            }
340
        }
341
        $request = $serverRequestFactory->createServerRequest($method, $path);
342
343
        $result = $matcher->match($request);
344
        if (!$result->isSuccess()) {
345
            return $this->responseFactory->createResponse([
346
                'result' => false,
347
            ]);
348
        }
349
350
        $route = $result->route();
351
        $reflection = new \ReflectionObject($route);
352
        $property = $reflection->getProperty('middlewareDefinitions');
353
        $middlewareDefinitions = $property->getValue($route);
354
        $action = end($middlewareDefinitions);
355
356
        return $this->responseFactory->createResponse([
357
            'result' => true,
358
            'action' => $action,
359
        ]);
360
    }
361
362
    public function getTables(SchemaProviderInterface $schemaProvider): ResponseInterface
363
    {
364
        return $this->responseFactory->createResponse($schemaProvider->getTables());
365
    }
366
367
    public function getTable(SchemaProviderInterface $schemaProvider, CurrentRoute $currentRoute): ResponseInterface
368
    {
369
        $tableName = $currentRoute->getArgument('name');
370
371
        return $this->responseFactory->createResponse($schemaProvider->getTable($tableName));
0 ignored issues
show
Bug introduced by
It seems like $tableName can also be of type null; however, parameter $tableName of Yiisoft\Yii\Debug\Api\In...erInterface::getTable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

371
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
372
    }
373
374
    public function request(
375
        ServerRequestInterface $request,
376
        CollectorRepositoryInterface $collectorRepository
377
    ): ResponseInterface {
378
        $request = $request->getQueryParams();
379
        $debugEntryId = $request['debugEntryId'] ?? null;
380
381
        $data = $collectorRepository->getDetail($debugEntryId);
0 ignored issues
show
Bug introduced by
It seems like $debugEntryId can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\Re...yInterface::getDetail() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

381
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
382
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
383
384
        $request = Message::parseRequest($rawRequest);
385
386
        $client = new Client();
387
        $response = $client->send($request);
388
389
        $result = VarDumper::create($response)->asPrimitives();
390
391
        return $this->responseFactory->createResponse($result);
392
    }
393
394
    public function eventListeners(ContainerInterface $container)
395
    {
396
        $config = $container->get(ConfigInterface::class);
397
398
        return $this->responseFactory->createResponse([
399
            'common' => VarDumper::create($config->get('events'))->asPrimitives(),
400
            // TODO: change events-web to events-web when it will be possible
401
            'console' => [], //VarDumper::create($config->get('events-web'))->asPrimitives(),
402
            'web' => VarDumper::create($config->get('events-web'))->asPrimitives(),
403
        ]);
404
    }
405
406
    public function session(ContainerInterface $container): ResponseInterface
407
    {
408
        $session = $container->get(SessionInterface::class);
409
        $data = $session->all();
410
411
        return $this->responseFactory->createResponse([
412
            'id' => $session->getId(),
413
            'name' => $session->getName(),
414
            'cookieParameters' => $session->getCookieParameters(),
415
            'data' => $data,
416
        ]);
417
    }
418
419
    public function buildCurl(
420
        ServerRequestInterface $request,
421
        CollectorRepositoryInterface $collectorRepository
422
    ): ResponseInterface {
423
        $request = $request->getQueryParams();
424
        $debugEntryId = $request['debugEntryId'] ?? null;
425
426
427
        $data = $collectorRepository->getDetail($debugEntryId);
0 ignored issues
show
Bug introduced by
It seems like $debugEntryId can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\Re...yInterface::getDetail() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

427
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
428
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
429
430
        $request = Message::parseRequest($rawRequest);
431
432
        try {
433
            $output = (new Command())
434
                ->setRequest($request)
435
                ->build();
436
        } catch (Throwable $e) {
437
            return $this->responseFactory->createResponse([
438
                'command' => null,
439
                'exception' => (string)$e,
440
            ]);
441
        }
442
443
        return $this->responseFactory->createResponse([
444
            'command' => $output,
445
        ]);
446
    }
447
448
    private function removeBasePath(string $rootPath, string $path): string|array|null
449
    {
450
        return preg_replace(
451
            '/^' . preg_quote($rootPath, '/') . '/',
452
            '',
453
            $path,
454
            1
455
        );
456
    }
457
458
    private function getUserOwner(int $uid): array
459
    {
460
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
461
            return [
462
                'id' => $uid,
463
            ];
464
        }
465
        return [
466
            'uid' => $info['uid'],
467
            'gid' => $info['gid'],
468
            'name' => $info['name'],
469
        ];
470
    }
471
472
    private function getGroupOwner(int $gid): array
473
    {
474
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
475
            return [
476
                'id' => $gid,
477
            ];
478
        }
479
        return [
480
            'gid' => $info['gid'],
481
            'name' => $info['name'],
482
        ];
483
    }
484
485
    private function serializeFileInfo(SplFileInfo $file): array
486
    {
487
        return [
488
            'baseName' => $file->getBasename(),
489
            'extension' => $file->getExtension(),
490
            'user' => $this->getUserOwner((int) $file->getOwner()),
491
            'group' => $this->getGroupOwner((int) $file->getGroup()),
492
            'size' => $file->getSize(),
493
            'type' => $file->getType(),
494
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
495
        ];
496
    }
497
498
    private function readFile(string $destination, array $extra = []): DataResponse
499
    {
500
        $rootPath = $this->aliases->get('@root');
501
        $file = new SplFileInfo($destination);
502
        return $this->responseFactory->createResponse(
503
            array_merge(
504
                $extra,
505
                [
506
                    'directory' => $this->removeBasePath($rootPath, dirname($destination)),
507
                    'content' => file_get_contents($destination),
508
                    'path' => $this->removeBasePath($rootPath, $destination),
509
                    'absolutePath' => $destination,
510
                ],
511
                $this->serializeFileInfo($file)
512
            )
513
        );
514
    }
515
}
516