Passed
Pull Request — master (#18)
by Evgeniy
02:06
created

HttpApplicationRunner   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 77
dl 0
loc 245
ccs 86
cts 86
cp 1
rs 10
c 2
b 0
f 0
wmc 24

14 Methods

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