Passed
Push — master ( 5c9806...4102cb )
by Alexander
04:15 queued 01:10
created

HttpApplicationRunner   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 171
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 73
c 0
b 0
f 0
dl 0
loc 171
ccs 82
cts 82
cp 1
rs 10
wmc 20

13 Methods

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