Passed
Pull Request — master (#39)
by Dmitriy
20:30
created

withTemporaryErrorHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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