Passed
Pull Request — master (#39)
by Alexander
23:54 queued 21:17
created

RoadRunnerApplicationRunner::__construct()   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 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 3
b 0
f 0
nc 1
nop 3
dl 0
loc 5
ccs 4
cts 4
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 RuntimeException;
15
use Spiral\RoadRunner\Environment;
16
use Spiral\RoadRunner\Environment\Mode;
17
use Spiral\RoadRunner\Http\PSR7WorkerInterface;
18
use Temporal\Worker\WorkerOptions;
19
use Temporal\WorkerFactory;
20
use Throwable;
21
use Yiisoft\Definitions\Exception\CircularReferenceException;
22
use Yiisoft\Definitions\Exception\InvalidConfigException;
23
use Yiisoft\Definitions\Exception\NotInstantiableException;
24
use Yiisoft\Di\NotFoundException;
25
use Yiisoft\Di\StateResetter;
26
use Yiisoft\ErrorHandler\ErrorHandler;
27
use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
28
use Yiisoft\Log\Logger;
29
use Yiisoft\Log\Target\File\FileTarget;
30
use Yiisoft\Yii\Http\Application;
31
use Yiisoft\Yii\Runner\ApplicationRunner;
32
use Yiisoft\Yii\Runner\Http\HttpApplicationRunner;
33
34
use function gc_collect_cycles;
35
36
/**
37
 * `RoadRunnerApplicationRunner` runs the Yii HTTP application using RoadRunner.
38
 */
39
final class RoadRunnerApplicationRunner 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 string|null $environment The environment name.
49
     */
50 9
    public function __construct(string $rootPath, bool $debug, ?string $environment)
51
    {
52 9
        parent::__construct($rootPath, $debug, $environment);
53 9
        $this->bootstrapGroup = 'bootstrap-web';
54 9
        $this->eventsGroup = 'events-web';
55
    }
56
57
    /**
58
     * Returns a new instance with the specified temporary error handler instance {@see ErrorHandler}.
59
     *
60
     * A temporary error handler is needed to handle the creation of configuration and container instances,
61
     * then the error handler configured in your application configuration will be used.
62
     *
63
     * @param ErrorHandler $temporaryErrorHandler The temporary error handler instance.
64
     */
65 2
    public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self
66
    {
67 2
        $new = clone $this;
68 2
        $new->temporaryErrorHandler = $temporaryErrorHandler;
69 2
        return $new;
70
    }
71
72
    /**
73
     * Returns a new instance with the specified PSR-7 worker instance {@see PSR7WorkerInterface}.
74
     *
75
     * @param PSR7WorkerInterface $worker The PSR-7 worker instance.
76
     */
77 9
    public function withPsr7Worker(PSR7WorkerInterface $worker): self
78
    {
79 9
        $new = clone $this;
80 9
        $new->psr7Worker = $worker;
81 9
        return $new;
82
    }
83
84
    /**
85
     * Returns a new instance with enabled temporal support.
86
     */
87
    public function withEnabledTemporal(bool $value): self
88
    {
89
        if (!$this->isTemporalSDKInstalled()) {
90
            throw new Exception('Temporal SDK is not installed. To install the SDK run `composer require temporal/sdk`.');
91
        }
92
        $new = clone $this;
93
        $new->isTemporalEnabled = $value;
94
        return $new;
95
    }
96
97
    /**
98
     * {@inheritDoc}
99
     *
100
     * @throws CircularReferenceException|ErrorException|InvalidConfigException|JsonException
101
     * @throws ContainerExceptionInterface|NotFoundException|NotFoundExceptionInterface|NotInstantiableException
102
     */
103 8
    public function run(): void
104
    {
105
        // Register temporary error handler to catch error while container is building.
106 8
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
107 8
        $this->registerErrorHandler($temporaryErrorHandler);
108
109 8
        $config = $this->getConfig();
110 8
        $container = $this->getContainer($config, 'web');
111
112
        // Register error handler with real container-configured dependencies.
113
        /** @var ErrorHandler $actualErrorHandler */
114 8
        $actualErrorHandler = $container->get(ErrorHandler::class);
115 8
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
116
117 8
        $this->runBootstrap($config, $container);
118 8
        $this->checkEvents($config, $container);
119
120 7
        $env = Environment::fromGlobals();
121
122 7
        if ($env->getMode() === Mode::MODE_TEMPORAL) {
123
            if (!$this->isTemporalEnabled) {
124
                throw new RuntimeException(
125
                    'Temporal support is disabled. You should call `withEnabledTemporal(true)` to enable temporal support.',
126
                );
127
            }
128
            $this->runTemporal($container);
129
            return;
130
        }
131 7
        if ($env->getMode() === Mode::MODE_HTTP) {
132 7
            $this->runRoadRunner($container);
133 7
            return;
134
        }
135
136
        // Leave support to run the application with built-in php server with: php -S 127.0.0.0:8080 public/index.php
137
        $runner = new HttpApplicationRunner($this->rootPath, $this->debug, $this->environment);
138
        $runner->run();
139
    }
140
141 8
    private function createTemporaryErrorHandler(): ErrorHandler
142
    {
143 8
        if ($this->temporaryErrorHandler !== null) {
144 1
            return $this->temporaryErrorHandler;
145
        }
146
147 7
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
148 7
        return new ErrorHandler($logger, new HtmlRenderer());
149
    }
150
151
    /**
152
     * @throws ErrorException
153
     */
154 8
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
155
    {
156 8
        $unregistered?->unregister();
157
158 8
        if ($this->debug) {
159 8
            $registered->debug();
160
        }
161
162 8
        $registered->register();
163
    }
164
165 6
    private function afterRespond(
166
        Application $application,
167
        ContainerInterface $container,
168
        ?ResponseInterface $response,
169
    ): void {
170 6
        $application->afterEmit($response);
171
        /** @psalm-suppress MixedMethodCall */
172
        $container
173 6
            ->get(StateResetter::class)
174 6
            ->reset(); // We should reset the state of such services every request.
175 6
        gc_collect_cycles();
176
    }
177
178 7
    private function runRoadRunner(ContainerInterface $container): void
179
    {
180 7
        $worker = new RoadRunnerWorker($container, $this->psr7Worker);
181
182
        /** @var Application $application */
183 7
        $application = $container->get(Application::class);
184 7
        $application->start();
185
186 7
        while (true) {
187 7
            $request = $worker->waitRequest();
188 7
            $response = null;
189
190 7
            if ($request === null) {
191 7
                break;
192
            }
193
194 6
            if ($request instanceof Throwable) {
195 1
                $response = $worker->respondWithError($request);
196 1
                $this->afterRespond($application, $container, $response);
197 1
                continue;
198
            }
199
200
            try {
201 5
                $response = $application->handle($request);
202 4
                $worker->respond($response);
203 1
            } catch (Throwable $t) {
204 1
                $response = $worker->respondWithError($t, $request);
205
            } finally {
206 5
                $this->afterRespond($application, $container, $response);
207
            }
208
        }
209
210 7
        $application->shutdown();
211
    }
212
213
    private function runTemporal(ContainerInterface $container): void
214
    {
215
        /**
216
         * @var WorkerFactory $factory
217
         */
218
        $factory = $container->get(WorkerFactory::class);
219
        /**
220
         * @var WorkerOptions $workerOptions
221
         */
222
        $workerOptions = $container->get(WorkerOptions::class);
223
224
        $worker = $factory->newWorker(
225
            'default',
226
            $workerOptions,
227
        );
228
        /**
229
         * @var object[] $workflows
230
         */
231
        $workflows = $container->get('[email protected]');
232
        /**
233
         * @var object[] $activities
234
         */
235
        $activities = $container->get('[email protected]');
236
237
        foreach ($workflows as $workflow) {
238
            $worker->registerWorkflowTypes($workflow::class);
239
        }
240
241
        foreach ($activities as $activity) {
242
            $worker->registerActivity($activity::class);
243
        }
244
245
        $factory->run();
246
    }
247
248
    private function isTemporalSDKInstalled(): bool
249
    {
250
        return class_exists(WorkerFactory::class);
251
    }
252
}
253