Passed
Push — master ( 7b08dc...bf85a0 )
by Alexander
20:07
created

isTemporalSDKInstalled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
     * @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 12
    public function __construct(
68
        string $rootPath,
69
        bool $debug = false,
70
        bool $checkEvents = false,
71
        ?string $environment = null,
72
        string $bootstrapGroup = 'bootstrap-web',
73
        string $eventsGroup = 'events-web',
74
        string $diGroup = 'di-web',
75
        string $diProvidersGroup = 'di-providers-web',
76
        string $diDelegatesGroup = 'di-delegates-web',
77
        string $diTagsGroup = 'di-tags-web',
78
        string $paramsGroup = 'params-web',
79
        array $nestedParamsGroups = ['params'],
80
        array $nestedEventsGroups = ['events'],
81
    ) {
82 12
        parent::__construct(
83 12
            $rootPath,
84 12
            $debug,
85 12
            $checkEvents,
86 12
            $environment,
87 12
            $bootstrapGroup,
88 12
            $eventsGroup,
89 12
            $diGroup,
90 12
            $diProvidersGroup,
91 12
            $diDelegatesGroup,
92 12
            $diTagsGroup,
93 12
            $paramsGroup,
94 12
            $nestedParamsGroups,
95 12
            $nestedEventsGroups,
96 12
        );
97
    }
98
99
    /**
100
     * Returns a new instance with the specified temporary error handler instance {@see ErrorHandler}.
101
     *
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 2
    public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self
108
    {
109 2
        $new = clone $this;
110 2
        $new->temporaryErrorHandler = $temporaryErrorHandler;
111 2
        return $new;
112
    }
113
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 12
    public function withPsr7Worker(PSR7WorkerInterface $worker): self
120
    {
121 12
        $new = clone $this;
122 12
        $new->psr7Worker = $worker;
123 12
        return $new;
124
    }
125
126
    /**
127
     * Returns a new instance with enabled temporal support.
128
     */
129 2
    public function withTemporalEnabled(bool $value): self
130
    {
131 2
        if (!$this->isTemporalSDKInstalled()) {
132
            throw new Exception('Temporal SDK is not installed. To install the SDK run `composer require temporal/sdk`.');
133
        }
134 2
        $new = clone $this;
135 2
        $new->isTemporalEnabled = $value;
136 2
        return $new;
137
    }
138
139
    /**
140
     * {@inheritDoc}
141
     *
142
     * @throws CircularReferenceException|ErrorException|InvalidConfigException|JsonException
143
     * @throws ContainerExceptionInterface|NotFoundException|NotFoundExceptionInterface|NotInstantiableException
144
     */
145 11
    public function run(): void
146
    {
147
        // Register temporary error handler to catch error while container is building.
148 11
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
149 11
        $this->registerErrorHandler($temporaryErrorHandler);
150
151 11
        $container = $this->getContainer();
152
153
        // Register error handler with real container-configured dependencies.
154
        /** @var ErrorHandler $actualErrorHandler */
155 11
        $actualErrorHandler = $container->get(ErrorHandler::class);
156 11
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
157
158 11
        $this->runBootstrap();
159 11
        $this->checkEvents();
160
161 10
        $env = Environment::fromGlobals();
162
163 10
        if ($env->getMode() === Mode::MODE_TEMPORAL) {
164 2
            if (!$this->isTemporalEnabled) {
165 1
                throw new RuntimeException(
166 1
                    'Temporal support is disabled. You should call `withTemporalEnabled(true)` to enable temporal support.',
167 1
                );
168
            }
169 1
            $this->runTemporal($container);
170 1
            return;
171
        }
172 8
        if ($env->getMode() === Mode::MODE_HTTP) {
173 7
            $this->runRoadRunner($container);
174 7
            return;
175
        }
176
177 1
        throw new RuntimeException(sprintf(
178 1
            'Unsupported mode "%s", supported modes are: "%s".',
179 1
            $env->getMode(),
180 1
            implode('", "', [Mode::MODE_HTTP, Mode::MODE_TEMPORAL]),
181 1
        ));
182
    }
183
184 11
    private function createTemporaryErrorHandler(): ErrorHandler
185
    {
186 11
        if ($this->temporaryErrorHandler !== null) {
187 1
            return $this->temporaryErrorHandler;
188
        }
189
190 10
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
191 10
        return new ErrorHandler($logger, new HtmlRenderer());
192
    }
193
194
    /**
195
     * @throws ErrorException
196
     */
197 11
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
198
    {
199 11
        $unregistered?->unregister();
200
201 11
        if ($this->debug) {
202 11
            $registered->debug();
203
        }
204
205 11
        $registered->register();
206
    }
207
208 6
    private function afterRespond(
209
        Application $application,
210
        ContainerInterface $container,
211
        ?ResponseInterface $response,
212
    ): void {
213 6
        $application->afterEmit($response);
214
        /** @psalm-suppress MixedMethodCall */
215 6
        $container
216 6
            ->get(StateResetter::class)
217 6
            ->reset(); // We should reset the state of such services every request.
218 6
        gc_collect_cycles();
219
    }
220
221 7
    private function runRoadRunner(ContainerInterface $container): void
222
    {
223 7
        $worker = new RoadRunnerHttpWorker($container, $this->psr7Worker);
224
225
        /** @var Application $application */
226 7
        $application = $container->get(Application::class);
227 7
        $application->start();
228
229 7
        while (true) {
230 7
            $request = $worker->waitRequest();
231 7
            $response = null;
232
233 7
            if ($request === null) {
234 7
                break;
235
            }
236
237 6
            if ($request instanceof Throwable) {
238 1
                $response = $worker->respondWithError($request);
239 1
                $this->afterRespond($application, $container, $response);
240 1
                continue;
241
            }
242
243
            try {
244 5
                $response = $application->handle($request);
245 4
                $worker->respond($response);
246 1
            } catch (Throwable $t) {
247 1
                $response = $worker->respondWithError($t, $request);
248
            } finally {
249 5
                $this->afterRespond($application, $container, $response);
250
            }
251
        }
252
253 7
        $application->shutdown();
254
    }
255
256 1
    private function runTemporal(ContainerInterface $container): void
257
    {
258
        /** @var TemporalDeclarationProvider $temporalDeclarationProvider */
259 1
        $temporalDeclarationProvider = $container->get(TemporalDeclarationProvider::class);
260
        /** @var HostConnectionInterface $host */
261 1
        $host = $container->get(HostConnectionInterface::class);
262
263
        /** @var WorkerFactoryInterface $factory */
264 1
        $factory = $container->get(WorkerFactoryInterface::class);
265 1
        $worker = $factory->newWorker('default');
266
267 1
        $workflows = $temporalDeclarationProvider->getWorkflows();
268 1
        $activities = $temporalDeclarationProvider->getActivities();
269
270 1
        $worker->registerWorkflowTypes(...$workflows);
271
272
        /** @psalm-suppress MixedReturnStatement,MixedInferredReturnType */
273 1
        $activityFactory = static fn (ReflectionClass $class): object => $container->get($class->getName());
274 1
        $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 1
        };
281
282 1
        foreach ($activities as $activity) {
283
            $worker->registerActivity($activity, $activityFactory);
284
        }
285 1
        $worker->registerActivityFinalizer($activityFinalizer);
286
287
        /** @psalm-suppress TooManyArguments */
288 1
        $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 2
    private function isTemporalSDKInstalled(): bool
292
    {
293 2
        return interface_exists(WorkerFactoryInterface::class);
294
    }
295
}
296