Test Failed
Pull Request — master (#39)
by Dmitriy
34:22 queued 14:17
created

RoadRunnerHttpApplicationRunner::run()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 36
rs 9.584
ccs 20
cts 20
cp 1
cc 4
nc 4
nop 0
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Runner\RoadRunner;
6
7
use ErrorException;
8
use Exception;
9
use JsonException;
10
use Psr\Container\ContainerExceptionInterface;
11
use Psr\Container\ContainerInterface;
12
use Psr\Container\NotFoundExceptionInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use RuntimeException;
15
use Spiral\RoadRunner\Environment;
16
use Spiral\RoadRunner\Environment\Mode;
17
use Spiral\RoadRunner\Http\PSR7WorkerInterface;
18
use Temporal\Worker\WorkerFactoryInterface;
19
use Throwable;
20
use Yiisoft\Definitions\Exception\CircularReferenceException;
21
use Yiisoft\Definitions\Exception\InvalidConfigException;
22
use Yiisoft\Definitions\Exception\NotInstantiableException;
23
use Yiisoft\Di\NotFoundException;
24
use Yiisoft\Di\StateResetter;
25
use Yiisoft\ErrorHandler\ErrorHandler;
0 ignored issues
show
Bug introduced by
The type Yiisoft\ErrorHandler\ErrorHandler 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\ErrorHandler\Renderer\HtmlRenderer;
27
use Yiisoft\Log\Logger;
28
use Yiisoft\Log\Target\File\FileTarget;
29
use Yiisoft\Yii\Http\Application;
30
use Yiisoft\Yii\Runner\ApplicationRunner;
31
use Yiisoft\Yii\Runner\RoadRunner\Temporal\TemporalDeclarationProvider;
32
33
use function gc_collect_cycles;
34
35
/**
36
 * `RoadRunnerHttpApplicationRunner` runs the Yii HTTP application using RoadRunner.
37
 */
38
final class RoadRunnerHttpApplicationRunner extends ApplicationRunner
39
{
40
    private ?ErrorHandler $temporaryErrorHandler = null;
41
    private ?PSR7WorkerInterface $psr7Worker = null;
42
    private bool $isTemporalEnabled = false;
43
44
    /**
45
     * @param string $rootPath The absolute path to the project root.
46
     * @param bool $debug Whether the debug mode is enabled.
47
     * @param bool $checkEvents Whether to check events' configuration.
48
     * @param string|null $environment The environment name.
49
     * @param string $bootstrapGroup The bootstrap configuration group name.
50
     * @param string $eventsGroup The events' configuration group name.
51
     * @param string $diGroup The container definitions' configuration group name.
52
     * @param string $diProvidersGroup The container providers' configuration group name.
53
     * @param string $diDelegatesGroup The container delegates' configuration group name.
54
     * @param string $diTagsGroup The container tags' configuration group name.
55
     * @param string $paramsGroup The configuration parameters group name.
56
     * @param array $nestedParamsGroups Configuration group names that are included into configuration parameters group.
57 9
     * This is needed for recursive merging of parameters.
58
     * @param array $nestedEventsGroups Configuration group names that are included into events' configuration group.
59
     * This is needed for reverse and recursive merge of events' configurations.
60
     *
61
     * @psalm-param list<string> $nestedParamsGroups
62
     * @psalm-param list<string> $nestedEventsGroups
63
     */
64
    public function __construct(
65
        string $rootPath,
66
        bool $debug = false,
67
        bool $checkEvents = false,
68
        ?string $environment = null,
69
        string $bootstrapGroup = 'bootstrap-web',
70
        string $eventsGroup = 'events-web',
71
        string $diGroup = 'di-web',
72 9
        string $diProvidersGroup = 'di-providers-web',
73 9
        string $diDelegatesGroup = 'di-delegates-web',
74 9
        string $diTagsGroup = 'di-tags-web',
75 9
        string $paramsGroup = 'params-web',
76 9
        array $nestedParamsGroups = ['params'],
77 9
        array $nestedEventsGroups = ['events'],
78 9
    ) {
79 9
        parent::__construct(
80 9
            $rootPath,
81 9
            $debug,
82 9
            $checkEvents,
83 9
            $environment,
84 9
            $bootstrapGroup,
85 9
            $eventsGroup,
86 9
            $diGroup,
87
            $diProvidersGroup,
88
            $diDelegatesGroup,
89
            $diTagsGroup,
90
            $paramsGroup,
91
            $nestedParamsGroups,
92
            $nestedEventsGroups,
93
        );
94
    }
95
96
    /**
97 2
     * Returns a new instance with the specified temporary error handler instance {@see ErrorHandler}.
98
     *
99 2
     * A temporary error handler is needed to handle the creation of configuration and container instances,
100 2
     * then the error handler configured in your application configuration will be used.
101 2
     *
102
     * @param ErrorHandler $temporaryErrorHandler The temporary error handler instance.
103
     */
104
    public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self
105
    {
106
        $new = clone $this;
107
        $new->temporaryErrorHandler = $temporaryErrorHandler;
108
        return $new;
109 9
    }
110
111 9
    /**
112 9
     * Returns a new instance with the specified PSR-7 worker instance {@see PSR7WorkerInterface}.
113 9
     *
114
     * @param PSR7WorkerInterface $worker The PSR-7 worker instance.
115
     */
116
    public function withPsr7Worker(PSR7WorkerInterface $worker): self
117
    {
118
        $new = clone $this;
119
        $new->psr7Worker = $worker;
120
        return $new;
121
    }
122 8
123
    /**
124
     * Returns a new instance with enabled temporal support.
125 8
     */
126 8
    public function withEnabledTemporal(bool $value): self
127
    {
128 8
        if (!$this->isTemporalSDKInstalled()) {
129
            throw new Exception('Temporal SDK is not installed. To install the SDK run `composer require temporal/sdk`.');
130
        }
131
        $new = clone $this;
132 8
        $new->isTemporalEnabled = $value;
133 8
        return $new;
134
    }
135 8
136 8
    /**
137
     * {@inheritDoc}
138 7
     *
139
     * @throws CircularReferenceException|ErrorException|InvalidConfigException|JsonException
140
     * @throws ContainerExceptionInterface|NotFoundException|NotFoundExceptionInterface|NotInstantiableException
141 7
     */
142 7
    public function run(): void
143
    {
144 7
        // Register temporary error handler to catch error while container is building.
145 7
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
146 7
        $this->registerErrorHandler($temporaryErrorHandler);
147
148 7
        $container = $this->getContainer();
149 7
150
        // Register error handler with real container-configured dependencies.
151
        /** @var ErrorHandler $actualErrorHandler */
152 6
        $actualErrorHandler = $container->get(ErrorHandler::class);
153 1
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
154 1
155 1
        $this->runBootstrap();
156
        $this->checkEvents();
157
158
        $env = Environment::fromGlobals();
159 5
160 4
        if ($env->getMode() === Mode::MODE_TEMPORAL) {
161 1
            if (!$this->isTemporalEnabled) {
162 1
                throw new RuntimeException(
163
                    'Temporal support is disabled. You should call `withEnabledTemporal(true)` to enable temporal support.',
164 5
                );
165
            }
166
            $this->runTemporal($container);
167
            return;
168 7
        }
169
        if ($env->getMode() === Mode::MODE_HTTP) {
170
            $this->runRoadRunner($container);
171 8
            return;
172
        }
173 8
174 1
        throw new RuntimeException(sprintf(
175
            'Unsupported mode "%s", modes are supported: "%s".',
176
            $env->getMode(),
177 7
            implode('", "', [Mode::MODE_HTTP, Mode::MODE_TEMPORAL]),
178 7
        ));
179
    }
180
181
    private function createTemporaryErrorHandler(): ErrorHandler
182
    {
183
        if ($this->temporaryErrorHandler !== null) {
184 8
            return $this->temporaryErrorHandler;
185
        }
186 8
187
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
188 8
        return new ErrorHandler($logger, new HtmlRenderer());
189 8
    }
190
191
    /**
192 8
     * @throws ErrorException
193
     */
194
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
195 6
    {
196
        $unregistered?->unregister();
197
198
        if ($this->debug) {
199
            $registered->debug();
200 6
        }
201
202 6
        $registered->register();
203 6
    }
204 6
205 6
    private function afterRespond(
206
        Application $application,
207
        ContainerInterface $container,
208
        ?ResponseInterface $response,
209
    ): void {
210
        $application->afterEmit($response);
211
        /** @psalm-suppress MixedMethodCall */
212
        $container
213
            ->get(StateResetter::class)
214
            ->reset(); // We should reset the state of such services every request.
215
        gc_collect_cycles();
216
    }
217
218
    private function runRoadRunner(ContainerInterface $container): void
219
    {
220
        $worker = new RoadRunnerHttpWorker($container, $this->psr7Worker);
221
222
        /** @var Application $application */
223
        $application = $container->get(Application::class);
224
        $application->start();
225
226
        while (true) {
227
            $request = $worker->waitRequest();
228
            $response = null;
229
230
            if ($request === null) {
231
                break;
232
            }
233
234
            if ($request instanceof Throwable) {
235
                $response = $worker->respondWithError($request);
236
                $this->afterRespond($application, $container, $response);
237
                continue;
238
            }
239
240
            try {
241
                $response = $application->handle($request);
242
                $worker->respond($response);
243
            } catch (Throwable $t) {
244
                $response = $worker->respondWithError($t, $request);
245
            } finally {
246
                $this->afterRespond($application, $container, $response);
247
            }
248
        }
249
250
        $application->shutdown();
251
    }
252
253
    private function runTemporal(ContainerInterface $container): void
254
    {
255
        /** @var TemporalDeclarationProvider $temporalDeclarationProvider */
256
        $temporalDeclarationProvider = $container->get(TemporalDeclarationProvider::class);
257
258
        /** @var WorkerFactoryInterface $factory */
259
        $factory = $container->get(WorkerFactoryInterface::class);
260
        $worker = $factory->newWorker('default');
261
262
        $workflows = $temporalDeclarationProvider->getWorkflows();
263
        $activities = $temporalDeclarationProvider->getActivities();
264
265
        $worker->registerWorkflowTypes(...$workflows);
266
267
        /** @psalm-suppress MixedReturnStatement,MixedInferredReturnType */
268
        $activityFactory = static fn (\ReflectionClass $class): object => $container->get($class->getName());
269
        $activityFinalizer = static function () use ($container): void {
270
            /** @psalm-suppress MixedMethodCall */
271
            $container
272
                ->get(StateResetter::class)
273
                ->reset(); // We should reset the state of such services every request.
274
            gc_collect_cycles();
275
        };
276
277
        foreach ($activities as $activity) {
278
            $worker->registerActivity($activity, $activityFactory);
279
        }
280
        $worker->registerActivityFinalizer($activityFinalizer);
281
282
        $factory->run();
283
    }
284
285
    private function isTemporalSDKInstalled(): bool
286
    {
287
        return class_exists(WorkerFactoryInterface::class);
288
    }
289
}
290