Passed
Push — master ( fe231c...9971ce )
by Evgeniy
06:31 queued 04:00
created

RoadRunnerApplicationRunner   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 83
c 8
b 0
f 0
dl 0
loc 241
ccs 0
cts 88
cp 0
rs 10
wmc 24

13 Methods

Rating   Name   Duplication   Size   Complexity  
A withTemporaryErrorHandler() 0 5 1
A withoutEvents() 0 5 1
A registerErrorHandler() 0 9 2
A createDefaultContainer() 0 17 4
A __construct() 0 5 1
A createTemporaryErrorHandler() 0 8 2
A withConfig() 0 5 1
A withoutBootstrap() 0 5 1
A withEvents() 0 5 1
A runBootstrap() 0 3 1
A withBootstrap() 0 5 1
B run() 0 64 7
A withContainer() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Runner\RoadRunner;
6
7
use ErrorException;
8
use Psr\Container\ContainerExceptionInterface;
9
use Psr\Container\ContainerInterface;
10
use Psr\Container\NotFoundExceptionInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\ServerRequestFactoryInterface;
13
use Psr\Http\Message\StreamFactoryInterface;
14
use Psr\Http\Message\UploadedFileFactoryInterface;
15
use Spiral\RoadRunner;
16
use Throwable;
17
use Yiisoft\Config\Config;
18
use Yiisoft\Di\Container;
19
use Yiisoft\Di\ContainerConfig;
20
use Yiisoft\Di\NotFoundException;
21
use Yiisoft\Di\StateResetter;
22
use Yiisoft\ErrorHandler\ErrorHandler;
23
use Yiisoft\ErrorHandler\Middleware\ErrorCatcher;
24
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
25
use Yiisoft\Definitions\Exception\CircularReferenceException;
26
use Yiisoft\Definitions\Exception\InvalidConfigException;
27
use Yiisoft\Definitions\Exception\NotInstantiableException;
28
use Yiisoft\Log\Logger;
29
use Yiisoft\Log\Target\File\FileTarget;
30
use Yiisoft\Yii\Event\ListenerConfigurationChecker;
31
use Yiisoft\Yii\Http\Application;
32
use Yiisoft\Yii\Http\Handler\ThrowableHandler;
33
use Yiisoft\Yii\Runner\BootstrapRunner;
34
use Yiisoft\Yii\Runner\ConfigFactory;
35
use Yiisoft\Yii\Runner\RunnerInterface;
36
37
use function gc_collect_cycles;
38
use function microtime;
39
40
/**
41
 * `RoadRunnerApplicationRunner` runs the Yii HTTP application for RoadRunner.
42
 */
43
final class RoadRunnerApplicationRunner implements RunnerInterface
44
{
45
    private bool $debug;
46
    private string $rootPath;
47
    private ?string $environment;
48
    private ?Config $config = null;
49
    private ?ContainerInterface $container = null;
50
    private ?ErrorHandler $temporaryErrorHandler = null;
51
    private ?string $bootstrapGroup = 'bootstrap-web';
52
    private ?string $eventsGroup = 'events-web';
53
54
    /**
55
     * @param string $rootPath The absolute path to the project root.
56
     * @param bool $debug Whether the debug mode is enabled.
57
     * @param string|null $environment The environment name.
58
     */
59
    public function __construct(string $rootPath, bool $debug, ?string $environment)
60
    {
61
        $this->rootPath = $rootPath;
62
        $this->debug = $debug;
63
        $this->environment = $environment;
64
    }
65
66
    /**
67
     * Returns a new instance with the specified bootstrap configuration group name.
68
     *
69
     * @param string $bootstrapGroup The bootstrap configuration group name.
70
     *
71
     * @return self
72
     */
73
    public function withBootstrap(string $bootstrapGroup): self
74
    {
75
        $new = clone $this;
76
        $new->bootstrapGroup = $bootstrapGroup;
77
        return $new;
78
    }
79
80
    /**
81
     * Returns a new instance and disables the use of bootstrap configuration group.
82
     *
83
     * @return self
84
     */
85
    public function withoutBootstrap(): self
86
    {
87
        $new = clone $this;
88
        $new->bootstrapGroup = null;
89
        return $new;
90
    }
91
92
    /**
93
     * Returns a new instance with the specified events configuration group name.
94
     *
95
     * @param string $eventsGroup The events configuration group name.
96
     *
97
     * @return self
98
     */
99
    public function withEvents(string $eventsGroup): self
100
    {
101
        $new = clone $this;
102
        $new->eventsGroup = $eventsGroup;
103
        return $new;
104
    }
105
106
    /**
107
     * Returns a new instance and disables the use of events configuration group.
108
     *
109
     * @return self
110
     */
111
    public function withoutEvents(): self
112
    {
113
        $new = clone $this;
114
        $new->eventsGroup = null;
115
        return $new;
116
    }
117
118
    /**
119
     * Returns a new instance with the specified config instance {@see Config}.
120
     *
121
     * @param Config $config The config instance.
122
     *
123
     * @return self
124
     */
125
    public function withConfig(Config $config): self
126
    {
127
        $new = clone $this;
128
        $new->config = $config;
129
        return $new;
130
    }
131
132
    /**
133
     * Returns a new instance with the specified container instance {@see ContainerInterface}.
134
     *
135
     * @param ContainerInterface $container The container instance.
136
     *
137
     * @return self
138
     */
139
    public function withContainer(ContainerInterface $container): self
140
    {
141
        $new = clone $this;
142
        $new->container = $container;
143
        return $new;
144
    }
145
146
    /**
147
     * Returns a new instance with the specified temporary error handler instance {@see ErrorHandler}.
148
     *
149
     * A temporary error handler is needed to handle the creation of configuration and container instances,
150
     * then the error handler configured in your application configuration will be used.
151
     *
152
     * @param ErrorHandler $temporaryErrorHandler The temporary error handler instance.
153
     *
154
     * @return self
155
     */
156
    public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self
157
    {
158
        $new = clone $this;
159
        $new->temporaryErrorHandler = $temporaryErrorHandler;
160
        return $new;
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     *
166
     * @throws CircularReferenceException|ErrorException|InvalidConfigException
167
     * @throws ContainerExceptionInterface|NotFoundException|NotFoundExceptionInterface|NotInstantiableException
168
     */
169
    public function run(): void
170
    {
171
        // Register temporary error handler to catch error while container is building.
172
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
173
        $this->registerErrorHandler($temporaryErrorHandler);
174
175
        $config = $this->config ?? ConfigFactory::create($this->rootPath, $this->environment);
176
        $container = $this->container ?? $this->createDefaultContainer($config);
177
178
        // Register error handler with real container-configured dependencies.
179
        /** @var ErrorHandler $actualErrorHandler */
180
        $actualErrorHandler = $container->get(ErrorHandler::class);
181
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
182
183
        if ($container instanceof Container) {
184
            $container = $container->get(ContainerInterface::class);
185
        }
186
187
        // Run bootstrap
188
        if ($this->bootstrapGroup !== null) {
189
            $this->runBootstrap($container, $config->get($this->bootstrapGroup));
190
        }
191
192
        if ($this->debug && $this->eventsGroup !== null) {
193
            /** @psalm-suppress MixedMethodCall */
194
            $container->get(ListenerConfigurationChecker::class)->check($config->get($this->eventsGroup));
195
        }
196
197
        $worker = RoadRunner\Worker::create();
198
        /** @var ServerRequestFactoryInterface $serverRequestFactory */
199
        $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
200
        /** @var StreamFactoryInterface $streamFactory */
201
        $streamFactory = $container->get(StreamFactoryInterface::class);
202
        /** @var UploadedFileFactoryInterface $uploadsFactory */
203
        $uploadsFactory = $container->get(UploadedFileFactoryInterface::class);
204
        $worker = new RoadRunner\Http\PSR7Worker($worker, $serverRequestFactory, $streamFactory, $uploadsFactory);
205
206
        /** @var Application $application */
207
        $application = $container->get(Application::class);
208
        $application->start();
209
210
        while ($request = $worker->waitRequest()) {
211
            $request = $request->withAttribute('applicationStartTime', microtime(true));
212
            $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
213
            try {
214
                $response = $application->handle($request);
215
                $worker->respond($response);
216
            } catch (Throwable $t) {
217
                $handler = new ThrowableHandler($t);
218
                /**
219
                 * @var ResponseInterface
220
                 * @psalm-suppress MixedMethodCall
221
                 */
222
                $response = $container->get(ErrorCatcher::class)->process($request, $handler);
223
                $worker->respond($response);
224
            } finally {
225
                $application->afterEmit($response ?? null);
226
                /** @psalm-suppress MixedMethodCall */
227
                $container->get(StateResetter::class)->reset(); // We should reset the state of such services every request.
228
                gc_collect_cycles();
229
            }
230
        }
231
232
        $application->shutdown();
233
    }
234
235
    /**
236
     * @throws ErrorException|InvalidConfigException
237
     */
238
    private function createDefaultContainer(Config $config): Container
239
    {
240
        $containerConfig = ContainerConfig::create()->withValidate($this->debug);
241
242
        if ($config->has('web')) {
243
            $containerConfig = $containerConfig->withDefinitions($config->get('web'));
244
        }
245
246
        if ($config->has('providers-web')) {
247
            $containerConfig = $containerConfig->withProviders($config->get('providers-web'));
248
        }
249
250
        if ($config->has('delegates-web')) {
251
            $containerConfig = $containerConfig->withDelegates($config->get('delegates-web'));
252
        }
253
254
        return new Container($containerConfig);
255
    }
256
257
    private function createTemporaryErrorHandler(): ErrorHandler
258
    {
259
        if ($this->temporaryErrorHandler !== null) {
260
            return $this->temporaryErrorHandler;
261
        }
262
263
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
264
        return new ErrorHandler($logger, new PlainTextRenderer());
265
    }
266
267
    /**
268
     * @throws ErrorException
269
     */
270
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
271
    {
272
        $unregistered?->unregister();
273
274
        if ($this->debug) {
275
            $registered->debug();
276
        }
277
278
        $registered->register();
279
    }
280
281
    private function runBootstrap(ContainerInterface $container, array $bootstrapList): void
282
    {
283
        (new BootstrapRunner($container, $bootstrapList))->run();
284
    }
285
}
286