• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ICanBoogie / 12085837680

29 Nov 2024 01:44PM UTC coverage: 31.646% (-4.7%) from 36.313%
12085837680

push

github

olvlvl
Require PHP 8.4+

0 of 1 new or added line in 1 file covered. (0.0%)

53 existing lines in 2 files now uncovered.

50 of 158 relevant lines covered (31.65%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

20.34
/lib/Application.php
1
<?php
2

3
namespace ICanBoogie;
4

5
use ICanBoogie\Application\BootEvent;
6
use ICanBoogie\Application\ClearCacheEvent;
7
use ICanBoogie\Application\InvalidState;
8
use ICanBoogie\Application\RunEvent;
9
use ICanBoogie\Application\TerminateEvent;
10
use ICanBoogie\Autoconfig\Autoconfig;
11
use ICanBoogie\Binding\SymfonyDependencyInjection\ContainerFactory;
12
use ICanBoogie\Config\Builder;
13
use ICanBoogie\ConfigProvider\BasicConfigProvider;
14
use ICanBoogie\HTTP\Request;
15
use ICanBoogie\HTTP\Responder;
16
use ICanBoogie\HTTP\Response;
17
use ICanBoogie\HTTP\ResponseStatus;
18
use ICanBoogie\Storage\Storage;
19
use Symfony\Component\DependencyInjection\ContainerInterface;
20

21
use function asort;
22
use function assert;
23
use function date_default_timezone_get;
24
use function date_default_timezone_set;
25
use function header;
26
use function headers_sent;
27
use function http_response_code;
28
use function microtime;
29
use function set_error_handler;
30
use function set_exception_handler;
31

32
use const SORT_NUMERIC;
33

34
/**
35
 * The application singleton.
36
 */
37
final class Application implements ConfigProvider, ServiceProvider
38
{
39
    /**
40
     * Status of the application.
41
     */
42
    private const int STATUS_VOID = 0;
43
    private const int STATUS_BOOTING = 5;
44
    private const int STATUS_BOOTED = 6;
45
    private const int STATUS_RUNNING = 7;
46
    private const int STATUS_TERMINATING = 8;
47
    private const int STATUS_TERMINATED = 9;
48

49
    private static Application $instance;
50

51
    /**
52
     * @throws InvalidState
53
     */
54
    public static function new(Autoconfig $autoconfig): self
55
    {
56
        if (isset(self::$instance)) {
1✔
57
            throw InvalidState::already_instantiated();
1✔
58
        }
59

UNCOV
60
        return self::$instance = new self($autoconfig);
×
61
    }
62

63
    /**
64
     * Returns the unique instance of the application.
65
     *
66
     * @throws InvalidState
67
     */
68
    public static function get(): Application
69
    {
UNCOV
70
        return self::$instance
×
UNCOV
71
            ?? throw InvalidState::not_instantiated();
×
72
    }
73

74
    /**
75
     * One of `STATUS_*`.
76
     */
77
    private int $status = self::STATUS_VOID;
78

79
    /**
80
     * Whether the application is booting.
81
     */
82
    public bool $is_booting {
83
        get => $this->status === self::STATUS_BOOTING;
84
    }
85

86
    /**
87
     * Whether the application is booted.
88
     */
89
    public bool $is_booted {
90
        get => $this->status === self::STATUS_BOOTED;
91
    }
92

93
    /**
94
     * Whether the application is running.
95
     */
96
    public bool $is_running {
97
        get => $this->status === self::STATUS_RUNNING;
98
    }
99

100
    /**
101
     * Whether the application is terminating.
102
     */
103
    public bool $is_terminating {
104
        get => $this->status === self::STATUS_TERMINATING;
105
    }
106

107
    /**
108
     * Whether the application is terminated.
109
     */
110
    public bool $is_terminated {
111
        get => $this->status === self::STATUS_TERMINATED;
112
    }
113

114
    public readonly Autoconfig $autoconfig;
115

116
    /**
117
     * @var Storage<string, mixed>
118
     */
119
    public Storage $storage_for_configs {
120
        get => $this->storage_for_configs
121
            ??= $this->create_storage($this->config->storage_for_config);
122
    }
123

124
    /**
125
     * Returns the non-volatile variables registry.
126
     *
127
     * @return Storage<string, mixed>
128
     */
129
    public Storage $vars {
130
        get => $this->vars
131
            ??= $this->create_storage($this->config->storage_for_vars);
132
    }
133

134
    public readonly ConfigProvider $configs;
135
    public readonly AppConfig $config;
136
    public readonly EventCollection $events;
137
    public readonly ContainerInterface $container;
138

139
    private function __construct(Autoconfig $autoconfig)
140
    {
UNCOV
141
        $this->autoconfig = $autoconfig;
×
142

UNCOV
143
        if (!date_default_timezone_get()) {
×
144
            date_default_timezone_set('UTC');
×
145
        }
146

UNCOV
147
        $this->configs = $this->create_config_provider(
×
UNCOV
148
            $autoconfig->config_paths,
×
UNCOV
149
            $autoconfig->config_builders,
×
UNCOV
150
        );
×
UNCOV
151
        $this->config = $this->configs->config_for_class(AppConfig::class);
×
UNCOV
152
        $this->apply_config($this->config);
×
153

154
        // The container can be created once configurations are available.
UNCOV
155
        $this->container = ContainerFactory::from($this);
×
156

157
        // Enable the usage of `ref()`.
UNCOV
158
        \ICanBoogie\Service\ServiceProvider::define($this->container->get(...));
×
159

160
        // Events can be set up once the container is available.
UNCOV
161
        $this->events = $this->service_for_class(EventCollection::class);
×
162

163
        // Enable the usage of `emit()`.
164
        EventCollectionProvider::define(fn() => $this->events);
2✔
165
    }
166

167
    public function config_for_class(string $class): object
168
    {
UNCOV
169
        return $this->configs->config_for_class($class);
×
170
    }
171

172
    /**
173
     * @template T of object
174
     *
175
     * @param class-string<T> $class
176
     *
177
     * @return T
178
     */
179
    public function service_for_class(string $class): object
180
    {
181
        /** @var T */
182
        return $this->container->get($class);
1✔
183
    }
184

185
    public function service_for_id(string $id, string $class): object
186
    {
187
        $service = $this->container->get($id);
3✔
188

189
        assert($service instanceof $class, "The service is not of the expected class");
3✔
190

191
        return $service;
2✔
192
    }
193

194
    /**
195
     * Creates the configuration provider.
196
     *
197
     * @param array<string, int> $paths Path list.
198
     * @param array<class-string, class-string<Builder<object>>> $builders
199
     */
200
    private function create_config_provider(array $paths, array $builders): ConfigProvider
201
    {
UNCOV
202
        asort($paths, SORT_NUMERIC);
×
203

UNCOV
204
        return new BasicConfigProvider(array_keys($paths), $builders);
×
205
    }
206

207
    /**
208
     * Applies low-level configuration.
209
     */
210
    private function apply_config(AppConfig $config): void
211
    {
212
        $error_handler = $config->error_handler;
×
213

UNCOV
214
        if ($error_handler) {
×
215
            set_error_handler($error_handler);
×
216
        }
217

UNCOV
218
        $exception_handler = $config->exception_handler;
×
219

UNCOV
220
        if ($exception_handler) {
×
UNCOV
221
            set_exception_handler($exception_handler);
×
222
        }
223

UNCOV
224
        if ($config->cache_configs && $this->configs instanceof BasicConfigProvider) {
×
NEW
225
            $this->configs->cache = $this->storage_for_configs;
×
226
        }
227
    }
228

229
    /**
230
     * Creates a storage engine, using a factory.
231
     *
232
     * @param callable(Application): Storage<string, mixed> $factory
233
     *
234
     * @return Storage<string, mixed>
235
     */
236
    private function create_storage(callable $factory): Storage
237
    {
238
        return $factory($this);
2✔
239
    }
240

241
    /**
242
     * Boot the modules and configure Debug, Prototype, and Events.
243
     *
244
     * Emits {@see BootEvent} after the boot is finished.
245
     *
246
     * The `ICANBOOGIE_READY_TIME_FLOAT` key is added to the `$_SERVER` super global with the
247
     * micro-time at which the boot finished.
248
     */
249
    public function boot(): void
250
    {
251
        $this->assert_can_boot();
1✔
252

UNCOV
253
        $this->status = self::STATUS_BOOTING;
×
254

UNCOV
255
        Binding\Prototype\AutoConfig::configure($this);
×
256

UNCOV
257
        emit(new BootEvent($this));
×
258

UNCOV
259
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
260

UNCOV
261
        $this->status = self::STATUS_BOOTED;
×
262
    }
263

264
    /**
265
     * Asserts that the application is not booted yet.
266
     *
267
     * @throws InvalidState
268
     */
269
    private function assert_can_boot(): void
270
    {
271
        if ($this->status >= self::STATUS_BOOTING) {
1✔
272
            throw InvalidState::already_booted();
1✔
273
        }
274
    }
275

276
    /**
277
     * The initial request.
278
     */
279
    public Request $request {
280
        get => $this->request ??= Request::from($_SERVER); // @phpstan-ignore argument.type
281
    }
282

283
    /**
284
     * Run the application.
285
     *
286
     * To avoid error messages triggered by PHP fatal errors to be sent with a 200 (Ok) HTTP code, the HTTP code is
287
     * changed to 500 before the application is run (and booted). When the process runs properly, the response changes
288
     * the HTTP code to the appropriate value.
289
     *
290
     * @param Request|null $request The request to handle. If `null`, a request is created from `$_SERVER`.
291
     */
292
    public function run(?Request $request = null): void
293
    {
294
        $this->initialize_response_header();
×
UNCOV
295
        $this->assert_can_run();
×
296

UNCOV
297
        $this->status = self::STATUS_RUNNING;
×
298

UNCOV
299
        $this->request = $request ??= Request::from($_SERVER); // @phpstan-ignore argument.type
×
300

UNCOV
301
        emit(new RunEvent($this, $request));
×
302

UNCOV
303
        $response = $this->service_for_class(Responder::class)->respond($request);
×
UNCOV
304
        $response();
×
305

306
        $this->terminate($request, $response);
×
307
    }
308

309
    /**
310
     * Asserts that the application is not running yet.
311
     *
312
     * @throws InvalidState
313
     */
314
    private function assert_can_run(): void
315
    {
UNCOV
316
        if ($this->status < self::STATUS_BOOTED) {
×
UNCOV
317
            throw InvalidState::not_booted();
×
318
        }
319

UNCOV
320
        if ($this->status >= self::STATUS_RUNNING) {
×
UNCOV
321
            throw InvalidState::already_running();
×
322
        }
323
    }
324

325
    /**
326
     * Initializes default response header.
327
     *
328
     * The default response has the {@see ResponseStatus::STATUS_INTERNAL_SERVER_ERROR} status code and the appropriate
329
     * header fields, so it is not cached. That way, if something goes wrong and an error message is displayed, it won't
330
     * be cached by a proxy.
331
     */
332
    private function initialize_response_header(): void
333
    {
UNCOV
334
        http_response_code(ResponseStatus::STATUS_INTERNAL_SERVER_ERROR);
×
335

336
        // @codeCoverageIgnoreStart
337
        if (!headers_sent()) {
338
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
339
            header('Pragma: no-cache');
340
            header('Expires: 0');
341
        }
342
        // @codeCoverageIgnoreEnd
343
    }
344

345
    /**
346
     * Terminate the application.
347
     *
348
     * Emits {@see TerminateEvent}.
349
     */
350
    private function terminate(Request $request, Response $response): void
351
    {
UNCOV
352
        $this->status = self::STATUS_TERMINATING;
×
353

UNCOV
354
        emit(new TerminateEvent($this, $request, $response));
×
355

UNCOV
356
        $this->status = self::STATUS_TERMINATED;
×
357
    }
358

359
    /**
360
     * Emits {@see ClearCacheEvent}
361
     */
362
    public function clear_cache(): void
363
    {
364
        emit(new ClearCacheEvent($this));
1✔
365
    }
366
}
367

368
/*
369
 * Possessions don't touch you in your heart.
370
 * Possessions only tear you apart.
371
 * Possessions cannot kiss you good night.
372
 * Possessions will never hold you tight.
373
 */
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc