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

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

288
        $factory->/** @scrutinizer ignore-call */ 
289
                  run($host);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
289
    }
290
291
    private function isTemporalSDKInstalled(): bool
292
    {
293
        return interface_exists(WorkerFactoryInterface::class);
294
    }
295
}
296