Test Failed
Pull Request — master (#39)
by Dmitriy
20:09
created

RoadRunnerHttpApplicationRunner::afterRespond()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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