Passed
Pull Request — master (#18)
by Evgeniy
02:02
created

HttpApplicationRunner::withEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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