Passed
Pull Request — master (#4)
by Evgeniy
05:38 queued 03:31
created

HttpApplicationRunner::emit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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