Passed
Push — master ( 1ffb2d...f18857 )
by Dmitriy
05:56 queued 02:52
created

DebugController   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 96
dl 0
loc 428
ccs 0
cts 118
cp 0
rs 10
c 0
b 0
f 0
wmc 23

9 Methods

Rating   Name   Duplication   Size   Complexity  
A object() 0 10 1
A view() 0 23 4
A dump() 0 15 3
A summary() 0 4 1
A eventStream() 0 47 4
A createJsPanelResponse() 0 47 5
A __construct() 0 4 1
A createHtmlPanelResponse() 0 24 3
A index() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\Debug\Controller;
6
7
use Psr\Container\ContainerInterface;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Yiisoft\Assets\AssetManager;
12
use Yiisoft\Assets\AssetPublisherInterface;
13
use Yiisoft\DataResponse\DataResponse;
14
use Yiisoft\DataResponse\DataResponseFactoryInterface;
15
use Yiisoft\Router\CurrentRoute;
16
use Yiisoft\Yii\Debug\Api\Debug\Exception\NotFoundException;
17
use Yiisoft\Yii\Debug\Api\Debug\Exception\PackageNotInstalledException;
18
use Yiisoft\Yii\Debug\Api\Debug\HtmlViewProviderInterface;
19
use Yiisoft\Yii\Debug\Api\Debug\ModuleFederationProviderInterface;
20
use Yiisoft\Yii\Debug\Api\Debug\Repository\CollectorRepositoryInterface;
21
use Yiisoft\Yii\Debug\Api\ServerSentEventsStream;
22
use Yiisoft\Yii\Debug\Storage\StorageInterface;
23
use Yiisoft\Yii\View\ViewRenderer;
24
25
/**
26
 * Debug controller provides endpoints that expose information about requests processed that debugger collected.
27
 *
28
 * @OA\Tag(
29
 *     name="yii-debug-api",
30
 *     description="Yii Debug API"
31
 * )
32
 */
33
final class DebugController
34
{
35
    public function __construct(
36
        private DataResponseFactoryInterface $responseFactory,
37
        private CollectorRepositoryInterface $collectorRepository
38
    ) {
39
    }
40
41
    /**
42
     * List of requests processed.
43
     *
44
     * @OA\Get(
45
     *     tags={"yii-debug-api"},
46
     *     path="/debug/api",
47
     *     description="List of requests processed",
48
     *
49
     *     @OA\Response(
50
     *          response="200",
51
     *          description="Success",
52
     *
53
     *          @OA\JsonContent(
54
     *              allOf={
55
     *
56
     *                  @OA\Schema(ref="#/components/schemas/DebugSuccessResponse")
57
     *              }
58
     *          )
59
     *     )
60
     * )
61
     */
62
    public function index(): ResponseInterface
63
    {
64
        return $this->responseFactory->createResponse($this->collectorRepository->getSummary());
65
    }
66
67
    /**
68
     * Summary about a processed request identified by ID specified.
69
     *
70
     * @OA\Get(
71
     *     tags={"yii-debug-api"},
72
     *     path="/debug/api/summary/{id}",
73
     *     description="Summary about a processed request identified by ID specified",
74
     *
75
     *     @OA\Parameter(
76
     *          name="id",
77
     *          required=true,
78
     *
79
     *          @OA\Schema(type="string"),
80
     *          in="path",
81
     *          parameter="id",
82
     *          description="Request ID for getting the summary"
83
     *     ),
84
     *
85
     *     @OA\Response(
86
     *          response="200",
87
     *          description="Success",
88
     *
89
     *          @OA\JsonContent(
90
     *              allOf={
91
     *
92
     *                  @OA\Schema(ref="#/components/schemas/DebugSuccessResponse")
93
     *              }
94
     *          )
95
     *     ),
96
     *
97
     *     @OA\Response(
98
     *          response="404",
99
     *          description="Not found",
100
     *
101
     *          @OA\JsonContent(
102
     *              allOf={
103
     *
104
     *                  @OA\Schema(ref="#/components/schemas/DebugNotFoundResponse")
105
     *              }
106
     *          )
107
     *     )
108
     * )
109
     */
110
    public function summary(CurrentRoute $currentRoute): ResponseInterface
111
    {
112
        $data = $this->collectorRepository->getSummary($currentRoute->getArgument('id'));
113
        return $this->responseFactory->createResponse($data);
114
    }
115
116
    /**
117
     * Detail information about a processed request identified by ID.
118
     *
119
     * @OA\Get(
120
     *     tags={"yii-debug-api"},
121
     *     path="/debug/api/view/{id}/?collector={collector}",
122
     *     description="Detail information about a processed request identified by ID",
123
     *
124
     *     @OA\Parameter(
125
     *          name="id",
126
     *          required=true,
127
     *
128
     *          @OA\Schema(type="string"),
129
     *          in="path",
130
     *          parameter="id",
131
     *          description="Request ID for getting the detail information"
132
     *     ),
133
     *
134
     *     @OA\Parameter(
135
     *          name="collector",
136
     *          allowEmptyValue=true,
137
     *
138
     *          @OA\Schema(type="string"),
139
     *          in="query",
140
     *          parameter="collector",
141
     *          description="Collector for getting the detail information"
142
     *     ),
143
     *
144
     *     @OA\Response(
145
     *          response="200",
146
     *          description="Success",
147
     *
148
     *          @OA\JsonContent(
149
     *              allOf={
150
     *
151
     *                  @OA\Schema(ref="#/components/schemas/DebugSuccessResponse")
152
     *              }
153
     *          )
154
     *     ),
155
     *
156
     *     @OA\Response(
157
     *          response="404",
158
     *          description="Not found",
159
     *
160
     *          @OA\JsonContent(
161
     *              allOf={
162
     *
163
     *                  @OA\Schema(ref="#/components/schemas/DebugNotFoundResponse")
164
     *              }
165
     *          )
166
     *     )
167
     * )
168
     */
169
    public function view(
170
        CurrentRoute $currentRoute,
171
        ServerRequestInterface $serverRequest,
172
        ContainerInterface $container,
173
    ): ResponseInterface {
174
        $data = $this->collectorRepository->getDetail(
175
            $currentRoute->getArgument('id')
0 ignored issues
show
Bug introduced by
It seems like $currentRoute->getArgument('id') can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\De...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

175
            /** @scrutinizer ignore-type */ $currentRoute->getArgument('id')
Loading history...
176
        );
177
178
        $collectorClass = $serverRequest->getQueryParams()['collector'] ?? null;
179
        if ($collectorClass !== null) {
180
            $data = $data[$collectorClass] ?? throw new NotFoundException(
181
                sprintf("Requested collector doesn't exist: %s.", $collectorClass)
182
            );
183
        }
184
        if (is_subclass_of($collectorClass, HtmlViewProviderInterface::class)) {
185
            return $this->createHtmlPanelResponse($container, $collectorClass, $data);
186
        }
187
        if (is_subclass_of($collectorClass, ModuleFederationProviderInterface::class)) {
188
            return $this->createJsPanelResponse($container, $collectorClass, $data);
189
        }
190
191
        return $this->responseFactory->createResponse($data);
192
    }
193
194
    /**
195
     * Dump information about a processed request identified by ID.
196
     *
197
     * @OA\Get(
198
     *     tags={"yii-debug-api"},
199
     *     path="/debug/api/dump/{id}/{collector}",
200
     *     description="Dump information about a processed request identified by ID",
201
     *
202
     *     @OA\Parameter(
203
     *          name="id",
204
     *          required=true,
205
     *
206
     *          @OA\Schema(type="string"),
207
     *          in="path",
208
     *          parameter="id",
209
     *          description="Request ID for getting the dump information"
210
     *     ),
211
     *
212
     *     @OA\Parameter(
213
     *          name="collector",
214
     *          allowEmptyValue=true,
215
     *          required=false,
216
     *
217
     *          @OA\Schema(type="string"),
218
     *          in="path",
219
     *          parameter="collector",
220
     *          description="Collector for getting the dump information"
221
     *     ),
222
     *
223
     *     @OA\Response(
224
     *          response="200",
225
     *          description="Success",
226
     *
227
     *          @OA\JsonContent(
228
     *              allOf={
229
     *
230
     *                  @OA\Schema(ref="#/components/schemas/DebugSuccessResponse")
231
     *              }
232
     *          )
233
     *     ),
234
     *
235
     *     @OA\Response(
236
     *          response="404",
237
     *          description="Not found",
238
     *
239
     *          @OA\JsonContent(
240
     *              allOf={
241
     *
242
     *                  @OA\Schema(ref="#/components/schemas/DebugNotFoundResponse")
243
     *              }
244
     *          )
245
     *     )
246
     * )
247
     *
248
     * @throws NotFoundException
249
     * @return ResponseInterface response.
250
     */
251
    public function dump(CurrentRoute $currentRoute): ResponseInterface
252
    {
253
        $data = $this->collectorRepository->getDumpObject(
254
            $currentRoute->getArgument('id')
0 ignored issues
show
Bug introduced by
It seems like $currentRoute->getArgument('id') can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\De...erface::getDumpObject() 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

254
            /** @scrutinizer ignore-type */ $currentRoute->getArgument('id')
Loading history...
255
        );
256
257
        if ($currentRoute->getArgument('collector') !== null) {
258
            if (isset($data[$currentRoute->getArgument('collector')])) {
259
                $data = $data[$currentRoute->getArgument('collector')];
260
            } else {
261
                throw new NotFoundException('Requested collector doesn\'t exists.');
262
            }
263
        }
264
265
        return $this->responseFactory->createResponse($data);
266
    }
267
268
    /**
269
     * Object information about a processed request identified by ID.
270
     *
271
     * @OA\Get(
272
     *     tags={"yii-debug-api"},
273
     *     path="/debug/api/object/{id}/{objectId}",
274
     *     description="Object information about a processed request identified by ID",
275
     *
276
     *     @OA\Parameter(
277
     *          name="id",
278
     *          required=true,
279
     *
280
     *          @OA\Schema(type="string"),
281
     *          in="path",
282
     *          parameter="id",
283
     *          description="Request ID for getting the object information"
284
     *     ),
285
     *
286
     *     @OA\Parameter(
287
     *          name="objectId",
288
     *          required=true,
289
     *
290
     *          @OA\Schema(type="string"),
291
     *          in="path",
292
     *          parameter="objectId",
293
     *          description="ID for getting the object information"
294
     *     ),
295
     *
296
     *     @OA\Response(
297
     *          response="200",
298
     *          description="Success",
299
     *
300
     *          @OA\JsonContent(
301
     *              allOf={
302
     *
303
     *                  @OA\Schema(ref="#/components/schemas/DebugSuccessResponse")
304
     *              }
305
     *          )
306
     *     ),
307
     *
308
     *     @OA\Response(
309
     *          response="404",
310
     *          description="Not found",
311
     *
312
     *          @OA\JsonContent(
313
     *              allOf={
314
     *
315
     *                  @OA\Schema(ref="#/components/schemas/DebugNotFoundResponse")
316
     *              }
317
     *          )
318
     *     )
319
     * )
320
     *
321
     * @return ResponseInterface response.
322
     */
323
    public function object(CurrentRoute $currentRoute): ResponseInterface
324
    {
325
        $data = $this->collectorRepository->getObject(
326
            $currentRoute->getArgument('id'),
0 ignored issues
show
Bug introduced by
It seems like $currentRoute->getArgument('id') can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\De...yInterface::getObject() 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

326
            /** @scrutinizer ignore-type */ $currentRoute->getArgument('id'),
Loading history...
327
            $currentRoute->getArgument('objectId')
0 ignored issues
show
Bug introduced by
It seems like $currentRoute->getArgument('objectId') can also be of type null; however, parameter $objectId of Yiisoft\Yii\Debug\Api\De...yInterface::getObject() 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

327
            /** @scrutinizer ignore-type */ $currentRoute->getArgument('objectId')
Loading history...
328
        );
329
330
        return $this->responseFactory->createResponse([
331
            'class' => $data[0],
332
            'value' => $data[1],
333
        ]);
334
    }
335
336
    public function eventStream(
337
        StorageInterface $storage,
338
        ResponseFactoryInterface $responseFactory
339
    ): ResponseInterface {
340
        // TODO implement OS signal handling
341
        $compareFunction = function () use ($storage) {
342
            $read = $storage->read(StorageInterface::TYPE_SUMMARY, null);
343
            return md5(json_encode($read, JSON_THROW_ON_ERROR));
344
        };
345
        $hash = $compareFunction();
346
        $maxRetries = 10;
347
        $retries = 0;
348
349
        return $responseFactory->createResponse()
350
            ->withHeader('Content-Type', 'text/event-stream')
351
            ->withHeader('Cache-Control', 'no-cache')
352
            ->withHeader('Connection', 'keep-alive')
353
            ->withBody(
354
                new ServerSentEventsStream(function (array &$buffer) use (
355
                    $compareFunction,
356
                    &$hash,
357
                    &$retries,
358
                    $maxRetries
359
                ) {
360
                    $newHash = $compareFunction();
361
362
                    if ($hash !== $newHash) {
363
                        $response = [
364
                            'type' => 'debug-updated',
365
                            'payload' => [],
366
                        ];
367
368
                        $buffer[] = json_encode($response);
369
                        $hash = $newHash;
370
                    }
371
372
                    // break the loop if the client aborted the connection (closed the page)
373
                    if (connection_aborted()) {
374
                        return false;
375
                    }
376
                    if ($retries++ > $maxRetries) {
377
                        return false;
378
                    }
379
380
                    sleep(1);
381
382
                    return true;
383
                })
384
            );
385
    }
386
387
    private function createJsPanelResponse(
388
        ContainerInterface $container,
389
        string $collectorClass,
390
        mixed $data
391
    ): DataResponse {
392
        $asset = $collectorClass::getAsset();
393
        $module = $asset->getModule();
394
        $scope = $asset->getScope();
395
        /**
396
         * @psalm-suppress UndefinedClass
397
         */
398
        if (
399
            !class_exists(AssetManager::class)
400
            || !class_exists(AssetPublisherInterface::class)
401
            || !$container->has(AssetManager::class)
402
            || !$container->has(AssetPublisherInterface::class)
403
        ) {
404
            throw new PackageNotInstalledException(
405
                'yiisoft/assets',
406
                sprintf(
407
                    '"%s" or "%s" is not defined in the dependency container.',
408
                    AssetManager::class,
409
                    AssetPublisherInterface::class,
410
                ),
411
            );
412
        }
413
        /**
414
         * @psalm-suppress UndefinedClass
415
         */
416
        $assetManager = $container->get(AssetManager::class);
417
        $assetManager->register($asset::class);
418
        /**
419
         * @psalm-suppress UndefinedClass
420
         */
421
        $assetPublisher = $container->get(AssetPublisherInterface::class);
422
        $assetPublisher->publish($asset);
423
424
        $js = $assetManager->getJsFiles();
425
426
        $urls = end($js);
427
428
        return $this->responseFactory->createResponse([
429
            '__isPanelRemote__' => true,
430
            'url' => $urls[0],
431
            'module' => $module,
432
            'scope' => $scope,
433
            'data' => $data,
434
        ]);
435
    }
436
437
    private function createHtmlPanelResponse(
438
        ContainerInterface $container,
439
        string $collectorClass,
440
        mixed $data
441
    ): DataResponse {
442
        if (!class_exists(ViewRenderer::class) || !$container->has(ViewRenderer::class)) {
443
            /**
444
             * @psalm-suppress UndefinedClass
445
             */
446
            throw new PackageNotInstalledException(
447
                'yiisoft/yii-view',
448
                sprintf(
449
                    '"%s" is not defined in the dependency container.',
450
                    ViewRenderer::class,
451
                )
452
            );
453
        }
454
        $viewRenderer = $container->get(ViewRenderer::class);
455
        $viewDirectory = dirname($collectorClass::getView());
456
        $viewPath = basename($collectorClass::getView());
457
458
        return $viewRenderer
459
            ->withViewPath($viewDirectory)
460
            ->renderPartial($viewPath, ['data' => $data, 'collectorClass' => $collectorClass]);
461
    }
462
}
463