Passed
Pull Request — master (#39)
by Alexander
05:12 queued 02:26
created

RoadRunnerApplicationRunner   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 191
Duplicated Lines 0 %

Test Coverage

Coverage 74.39%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 79
dl 0
loc 191
ccs 61
cts 82
cp 0.7439
rs 10
c 10
b 0
f 0
wmc 21

10 Methods

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