Passed
Push — master ( 72123f...f100b7 )
by Evgeniy
02:12
created

HttpApplicationRunner::createDefaultContainer()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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