Passed
Pull Request — master (#122)
by
unknown
14:51
created

DebugController::createHtmlPanelResponse()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 24
ccs 0
cts 15
cp 0
rs 9.8666
cc 3
nc 2
nop 3
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\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\Exception\NotFoundException;
17
use Yiisoft\Yii\Debug\Api\Exception\PackageNotInstalledException;
18
use Yiisoft\Yii\Debug\Api\HtmlViewProviderInterface;
19
use Yiisoft\Yii\Debug\Api\ModuleFederationProviderInterface;
20
use Yiisoft\Yii\Debug\Api\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\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

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