Passed
Push — master ( 494088...4aed76 )
by Alexander
02:15
created

WebApplicationRunner::withTemporaryErrorHandler()   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 ?ErrorHandler $temporaryErrorHandler = null;
45
    private ?string $bootstrapGroup = 'bootstrap-web';
46
    private ?string $eventGroup = 'event-web';
47
48
    public function __construct(string $rootPath, bool $debug, ?string $environment)
49
    {
50
        $this->rootPath = $rootPath;
51
        $this->debug = $debug;
52
        $this->environment = $environment;
53
    }
54
55
    public function withBootstrap(string $bootstrapGroup): self
56
    {
57
        $new = clone $this;
58
        $new->bootstrapGroup = $bootstrapGroup;
59
        return $new;
60
    }
61
62
    public function withoutBootstrap(): self
63
    {
64
        $new = clone $this;
65
        $new->bootstrapGroup = null;
66
        return $new;
67
    }
68
69
    public function withEvent(string $eventGroup): self
70
    {
71
        $new = clone $this;
72
        $new->eventGroup = $eventGroup;
73
        return $new;
74
    }
75
76
    public function withoutEvent(): self
77
    {
78
        $new = clone $this;
79
        $new->eventGroup = null;
80
        return $new;
81
    }
82
83
    public function withConfig(Config $config): self
84
    {
85
        $new = clone $this;
86
        $new->config = $config;
87
        return $new;
88
    }
89
90
    public function withContainer(ContainerInterface $container): self
91
    {
92
        $new = clone $this;
93
        $new->container = $container;
94
        return $new;
95
    }
96
97
    public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self
98
    {
99
        $new = clone $this;
100
        $new->temporaryErrorHandler = $temporaryErrorHandler;
101
        return $new;
102
    }
103
104
    /**
105
     * @throws CircularReferenceException|ErrorException|HeadersHaveBeenSentException|InvalidConfigException
106
     * @throws NotFoundException|NotInstantiableException|
107
     */
108
    public function run(): void
109
    {
110
        $startTime = microtime(true);
111
112
        // Register temporary error handler to catch error while container is building.
113
        $temporaryErrorHandler = $this->createTemporaryErrorHandler();
114
        $this->registerErrorHandler($temporaryErrorHandler);
115
116
        $config = $this->config ?? ConfigFactory::create($this->rootPath, $this->environment);
117
118
        $container = $this->container ?? new Container(
119
            $config->get('web'),
120
            $config->get('providers-web'),
121
            [],
122
            $this->debug,
123
            $config->get('delegates-web')
124
        );
125
126
        // Register error handler with real container-configured dependencies.
127
        /** @var ErrorHandler $actualErrorHandler */
128
        $actualErrorHandler = $container->get(ErrorHandler::class);
129
        $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler);
130
131
        if ($container instanceof Container) {
132
            $container = $container->get(ContainerInterface::class);
133
        }
134
135
        // Run bootstrap
136
        if ($this->bootstrapGroup !== null) {
137
            $this->runBootstrap($container, $config->get($this->bootstrapGroup));
138
        }
139
140
        if ($this->debug && $this->eventGroup !== null) {
141
            /** @psalm-suppress MixedMethodCall */
142
            $container->get(ListenerConfigurationChecker::class)->check($config->get($this->eventGroup));
143
        }
144
145
        /** @var Application */
146
        $application = $container->get(Application::class);
147
148
        /**
149
         * @var ServerRequestInterface
150
         * @psalm-suppress MixedMethodCall
151
         */
152
        $serverRequest = $container->get(ServerRequestFactory::class)->createFromGlobals();
153
        $request = $serverRequest->withAttribute('applicationStartTime', $startTime);
154
155
        try {
156
            $application->start();
157
            $response = $application->handle($request);
158
            $this->emit($request, $response);
159
        } catch (Throwable $throwable) {
160
            $handler = new ThrowableHandler($throwable);
161
            /**
162
             * @var ResponseInterface
163
             * @psalm-suppress MixedMethodCall
164
             */
165
            $response = $container->get(ErrorCatcher::class)->process($request, $handler);
166
            $this->emit($request, $response);
167
        } finally {
168
            $application->afterEmit($response ?? null);
169
            $application->shutdown();
170
        }
171
    }
172
173
    private function createTemporaryErrorHandler(): ErrorHandler
174
    {
175
        if ($this->temporaryErrorHandler !== null) {
176
            return $this->temporaryErrorHandler;
177
        }
178
179
        $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]);
180
        return new ErrorHandler($logger, new HtmlRenderer());
181
    }
182
183
    /**
184
     * @throws HeadersHaveBeenSentException
185
     */
186
    private function emit(RequestInterface $request, ResponseInterface $response): void
187
    {
188
        (new SapiEmitter())->emit($response, $request->getMethod() === Method::HEAD);
189
    }
190
191
    /**
192
     * @throws ErrorException
193
     */
194
    private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void
195
    {
196
        $unregistered?->unregister();
197
198
        if ($this->debug) {
199
            $registered->debug();
200
        }
201
202
        $registered->register();
203
    }
204
205
    private function runBootstrap(ContainerInterface $container, array $bootstrapList): void
206
    {
207
        (new BootstrapRunner($container, $bootstrapList))->run();
208
    }
209
}
210