Passed
Push — master ( fc9108...a14873 )
by Alexander
04:55 queued 02:17
created

WebApplicationRunner::withBootstrap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 4
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Runner\Web;
6
7
use ErrorException;
8
use Psr\Container\ContainerInterface;
9
use Psr\Http\Message\RequestInterface;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Throwable;
13
use Yiisoft\Config\Config;
14
use Yiisoft\Di\Container;
15
use Yiisoft\ErrorHandler\ErrorHandler;
16
use Yiisoft\ErrorHandler\Middleware\ErrorCatcher;
17
use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
18
use Yiisoft\Definitions\Exception\CircularReferenceException;
19
use Yiisoft\Definitions\Exception\InvalidConfigException;
20
use Yiisoft\Definitions\Exception\NotFoundException;
21
use Yiisoft\Definitions\Exception\NotInstantiableException;
22
use Yiisoft\Http\Method;
23
use Yiisoft\Log\Logger;
24
use Yiisoft\Log\Target\File\FileTarget;
25
use Yiisoft\Yii\Event\ListenerConfigurationChecker;
26
use Yiisoft\Yii\Runner\BootstrapRunner;
27
use Yiisoft\Yii\Runner\ConfigFactory;
28
use Yiisoft\Yii\Runner\RunnerInterface;
29
use Yiisoft\Yii\Runner\ThrowableHandler;
30
use Yiisoft\Yii\Web\Application;
31
use Yiisoft\Yii\Web\Exception\HeadersHaveBeenSentException;
32
use Yiisoft\Yii\Web\SapiEmitter;
33
use Yiisoft\Yii\Web\ServerRequestFactory;
34
35
use function microtime;
36
37
final class WebApplicationRunner implements RunnerInterface
38
{
39
    private bool $debug;
40
    private string $rootPath;
41
    private ?string $environment;
42
    private ?Config $config = null;
43
    private ?ContainerInterface $container = null;
44
    private ?string $bootstrapGroup = 'bootstrap-web';
45
    private ?string $eventGroup = 'event-web';
46
47
    public function __construct(string $rootPath, bool $debug, ?string $environment)
48
    {
49
        $this->rootPath = $rootPath;
50
        $this->debug = $debug;
51
        $this->environment = $environment;
52
    }
53
54
    public function withBootstrap(string $bootstrapGroup): self
55
    {
56
        $new = clone $this;
57
        $new->bootstrapGroup = $bootstrapGroup;
58
        return $new;
59
    }
60
61
    public function withoutBootstrap(): self
62
    {
63
        $new = clone $this;
64
        $new->bootstrapGroup = null;
65
        return $new;
66
    }
67
68
    public function withEvent(string $eventGroup): self
69
    {
70
        $new = clone $this;
71
        $new->eventGroup = $eventGroup;
72
        return $new;
73
    }
74
75
    public function withoutEvent(): self
76
    {
77
        $new = clone $this;
78
        $new->eventGroup = null;
79
        return $new;
80
    }
81
82
    public function withConfig(Config $config): self
83
    {
84
        $new = clone $this;
85
        $new->config = $config;
86
        return $new;
87
    }
88
89
    public function withContainer(ContainerInterface $container): self
90
    {
91
        $new = clone $this;
92
        $new->container = $container;
93
        return $new;
94
    }
95
96
    /**
97
     * @throws CircularReferenceException|ErrorException|HeadersHaveBeenSentException|InvalidConfigException
98
     * @throws NotFoundException|NotInstantiableException|
99
     */
100
    public function run(): void
101
    {
102
        $startTime = microtime(true);
103
104
        // Register temporary error handler to catch error while container is building.
105
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
106
        $this->registerErrorHandler($temporaryErrorHandler);
107
108
        $config = $this->config ?? ConfigFactory::create($this->rootPath, $this->environment);
109
110
        $container = $this->container ?? new Container(
111
            $config->get('web'),
112
            $config->get('providers-web'),
113
            [],
114
            $this->debug,
115
            $config->get('delegates-web')
116
        );
117
118
        // Register error handler with real container-configured dependencies.
119
        /** @var ErrorHandler $actualErrorHandler */
120
        $actualErrorHandler = $container->get(ErrorHandler::class);
121
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
122
123
        if ($container instanceof Container) {
124
            $container = $container->get(ContainerInterface::class);
125
        }
126
127
        // Run bootstrap
128
        if ($this->bootstrapGroup !== null) {
129
            $this->runBootstrap($container, $config->get($this->bootstrapGroup));
130
        }
131
132
        if ($this->debug && $this->eventGroup !== null) {
133
            /** @psalm-suppress MixedMethodCall */
134
            $container->get(ListenerConfigurationChecker::class)->check($config->get($this->eventGroup));
135
        }
136
137
        /** @var Application */
138
        $application = $container->get(Application::class);
139
140
        /**
141
         * @var ServerRequestInterface
142
         * @psalm-suppress MixedMethodCall
143
         */
144
        $serverRequest = $container->get(ServerRequestFactory::class)->createFromGlobals();
145
        $request = $serverRequest->withAttribute('applicationStartTime', $startTime);
146
147
        try {
148
            $application->start();
149
            $response = $application->handle($request);
150
            $this->emit($request, $response);
151
        } catch (Throwable $throwable) {
152
            $handler = new ThrowableHandler($throwable);
153
            /**
154
             * @var ResponseInterface
155
             * @psalm-suppress MixedMethodCall
156
             */
157
            $response = $container->get(ErrorCatcher::class)->process($request, $handler);
158
            $this->emit($request, $response);
159
        } finally {
160
            $application->afterEmit($response ?? null);
161
            $application->shutdown();
162
        }
163
    }
164
165
    private function createTemporaryErrorHandler(): ErrorHandler
166
    {
167
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
168
        return new ErrorHandler($logger, new HtmlRenderer());
169
    }
170
171
    /**
172
     * @throws HeadersHaveBeenSentException
173
     */
174
    private function emit(RequestInterface $request, ResponseInterface $response): void
175
    {
176
        (new SapiEmitter())->emit($response, $request->getMethod() === Method::HEAD);
177
    }
178
179
    /**
180
     * @throws ErrorException
181
     */
182
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
183
    {
184
        if ($unregistered !== null) {
185
            $unregistered->unregister();
186
        }
187
188
        if ($this->debug) {
189
            $registered->debug();
190
        }
191
192
        $registered->register();
193
    }
194
195
    private function runBootstrap(ContainerInterface $container, array $bootstrapList): void
196
    {
197
        (new BootstrapRunner($container, $bootstrapList))->run();
198
    }
199
}
200