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

ICanBoogie / ICanBoogie / 11621009836

31 Oct 2024 10:42PM UTC coverage: 41.117% (+3.5%) from 37.657%
11621009836

push

github

olvlvl
Remove function excerpt

81 of 197 relevant lines covered (41.12%)

0.92 hits per line

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

39.24
/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\HTTP\Request;
14
use ICanBoogie\HTTP\Responder;
15
use ICanBoogie\HTTP\Response;
16
use ICanBoogie\HTTP\ResponseStatus;
17
use ICanBoogie\Storage\Storage;
18
use Symfony\Component\DependencyInjection\ContainerInterface;
19

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

31
use const SORT_NUMERIC;
32

33
/**
34
 * Application abstract.
35
 *
36
 * @property-read bool $is_booting `true` if the application is booting, `false` otherwise.
37
 * @property-read bool $is_booted `true` if the application is booted, `false` otherwise.
38
 * @property-read bool $is_running `true` if the application is running, `false` otherwise.
39
 * @property-read bool $is_terminating `true` if the application is terminating, `false` otherwise.
40
 * @property-read bool $is_terminated `true` if the application is terminated, `false` otherwise.
41
 * @property Storage $vars Persistent variables registry.
42
 * @property Session $session User's session.
43
 * @property string $language Locale language.
44
 * @property string|int $timezone Time zone.
45
 * @property-read Storage $storage_for_configs
46
 * @property-read Request $request
47
 */
48
final class Application implements ConfigProvider, ServiceProvider
49
{
50
    /**
51
     * @uses get_is_booting
52
     * @uses get_is_booted
53
     * @uses get_is_running
54
     * @uses get_is_terminating
55
     * @uses get_is_terminated
56
     * @uses get_timezone
57
     * @uses set_timezone
58
     * @uses get_storage_for_configs
59
     * @uses get_vars
60
     * @uses get_request
61
     */
62
    use PrototypeTrait;
63

64
    /**
65
     * Status of the application.
66
     */
67
    public const STATUS_VOID = 0;
68
    public const STATUS_BOOTING = 5;
69
    public const STATUS_BOOTED = 6;
70
    public const STATUS_RUNNING = 7;
71
    public const STATUS_TERMINATING = 8;
72
    public const STATUS_TERMINATED = 9;
73

74
    private static Application $instance;
75

76
    /**
77
     * @throws InvalidState
78
     */
79
    public static function new(Autoconfig $autoconfig): self
80
    {
81
        if (isset(self::$instance)) {
1✔
82
            throw InvalidState::already_instantiated();
1✔
83
        }
84

85
        return self::$instance = new self($autoconfig);
×
86
    }
87

88
    /**
89
     * Returns the unique instance of the application.
90
     *
91
     * @throws InvalidState
92
     */
93
    public static function get(): Application
94
    {
95
        return self::$instance
×
96
            ?? throw InvalidState::not_instantiated();
×
97
    }
98

99
    /**
100
     * One of `STATUS_*`.
101
     */
102
    private int $status = self::STATUS_VOID;
103

104
    /**
105
     * Whether the application is booting.
106
     */
107
    private function get_is_booting(): bool
108
    {
109
        return $this->status === self::STATUS_BOOTING;
1✔
110
    }
111

112
    /**
113
     * Whether the application is booted.
114
     */
115
    private function get_is_booted(): bool
116
    {
117
        return $this->status >= self::STATUS_BOOTED;
1✔
118
    }
119

120
    /**
121
     * Whether the application is running.
122
     */
123
    private function get_is_running(): bool
124
    {
125
        return $this->status === self::STATUS_RUNNING;
1✔
126
    }
127

128
    /**
129
     * Whether the application is terminating.
130
     */
131
    private function get_is_terminating(): bool
132
    {
133
        return $this->status === self::STATUS_TERMINATING;
×
134
    }
135

136
    /**
137
     * Whether the application is terminated.
138
     */
139
    private function get_is_terminated(): bool
140
    {
141
        return $this->status === self::STATUS_TERMINATED;
×
142
    }
143

144
    public readonly Autoconfig $autoconfig;
145

146
    private ?TimeZone $timezone = null;
147

148
    /**
149
     * Sets the working time zone.
150
     *
151
     * When the time zone is set the default time zone is also set with
152
     * {@link date_default_timezone_set()}.
153
     *
154
     * @param string|TimeZone $timezone An instance of {@link TimeZone},
155
     * or the name of a time zone.
156
     */
157
    private function set_timezone(string|TimeZone $timezone): void
158
    {
159
        $this->timezone = TimeZone::from($timezone);
1✔
160

161
        date_default_timezone_set((string) $this->timezone);
1✔
162
    }
163

164
    /**
165
     * Returns the working time zone.
166
     *
167
     * If the time zone is not defined yet, it defaults to the value of
168
     * {@link date_default_timezone_get()} or "UTC".
169
     */
170
    private function get_timezone(): TimeZone
171
    {
172
        /** @var TimeZone */
173
        return $this->timezone
2✔
174
            ??= TimeZone::from(date_default_timezone_get() ?: 'UTC');
2✔
175
    }
176

177
    /**
178
     * @var Storage<string, mixed>|null
179
     */
180
    private Storage|null $storage_for_configs;
181

182
    /**
183
     * @return Storage<string, mixed>
184
     */
185
    private function get_storage_for_configs(): Storage
186
    {
187
        return $this->storage_for_configs
2✔
188
            /** @phpstan-ignore-next-line */
2✔
189
            ??= $this->create_storage($this->config->storage_for_config);
2✔
190
    }
191

192
    private Storage $vars;
193

194
    /**
195
     * Returns the non-volatile variables registry.
196
     *
197
     * @return Storage<string, mixed>
198
     */
199
    private function get_vars(): Storage
200
    {
201
        return $this->vars
1✔
202
            /** @phpstan-ignore-next-line */
1✔
203
            ??= $this->create_storage($this->config->storage_for_vars);
1✔
204
    }
205

206
    public readonly Config $configs;
207
    public readonly AppConfig $config;
208
    public readonly EventCollection $events;
209
    public readonly ContainerInterface $container;
210

211
    private function __construct(Autoconfig $autoconfig)
212
    {
213
        $this->autoconfig = $autoconfig;
×
214

215
        if (!date_default_timezone_get()) {
×
216
            date_default_timezone_set('UTC');
×
217
        }
218

219
        $this->configs = $this->create_config_provider(
×
220
            $autoconfig->config_paths,
×
221
            $autoconfig->config_builders,
×
222
        );
×
223
        $this->config = $this->configs->config_for_class(AppConfig::class);
×
224
        $this->apply_config($this->config);
×
225

226
        // The container can be created once configurations are available.
227

228
        $this->container = ContainerFactory::from($this);
×
229

230
        // Enable the usage of `ref()`.
231

232
        \ICanBoogie\Service\ServiceProvider::define(
2✔
233
            fn (string $id): object => $this->container->get($id)
2✔
234
        );
2✔
235

236
        // Events can be set up once the container is available.
237

238
        $this->events = $this->service_for_class(EventCollection::class);
×
239

240
        // Enable the usage of `emit()`.
241

242
        EventCollectionProvider::define(fn() => $this->events);
2✔
243
    }
244

245
    private function get_session(): Session
246
    {
247
        static $session;
3✔
248

249
        return $session ??= SessionWithEvent::for_app($this);
3✔
250
    }
251

252
    public function config_for_class(string $class): object
253
    {
254
        return $this->configs->config_for_class($class);
×
255
    }
256

257
    /**
258
     * @template T of object
259
     *
260
     * @param class-string<T> $class
261
     *
262
     * @return T
263
     */
264
    public function service_for_class(string $class): object
265
    {
266
        /** @var T */
267
        return $this->container->get($class);
1✔
268
    }
269

270
    public function service_for_id(string $id, string $class): object
271
    {
272
        $service = $this->container->get($id);
3✔
273

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

276
        return $service;
2✔
277
    }
278

279
    /**
280
     * Creates the configuration provider.
281
     *
282
     * @param array<string, int> $paths Path list.
283
     * @param array<class-string, class-string<Builder<object>>> $builders
284
     */
285
    private function create_config_provider(array $paths, array $builders): Config
286
    {
287
        asort($paths, SORT_NUMERIC);
×
288

289
        return new Config(array_keys($paths), $builders);
×
290
    }
291

292
    /**
293
     * Applies low-level configuration.
294
     */
295
    private function apply_config(AppConfig $config): void
296
    {
297
        $error_handler = $config->error_handler;
×
298

299
        if ($error_handler) {
×
300
            set_error_handler($error_handler);
×
301
        }
302

303
        $exception_handler = $config->exception_handler;
×
304

305
        if ($exception_handler) {
×
306
            set_exception_handler($exception_handler);
×
307
        }
308

309
        if ($config->cache_configs) {
×
310
            $this->configs->cache = $this->get_storage_for_configs();
×
311
        }
312
    }
313

314
    /**
315
     * Creates a storage engine, using a factory.
316
     *
317
     * @param callable(Application): Storage<string, mixed> $factory
318
     *
319
     * @return Storage<string, mixed>
320
     */
321
    private function create_storage(callable $factory): Storage
322
    {
323
        return $factory($this);
2✔
324
    }
325

326
    /**
327
     * Boot the modules and configure Debug, Prototype, and Events.
328
     *
329
     * Emits {@link BootEvent} after the boot is finished.
330
     *
331
     * The `ICANBOOGIE_READY_TIME_FLOAT` key is added to the `$_SERVER` super global with the
332
     * micro-time at which the boot finished.
333
     */
334
    public function boot(): void
335
    {
336
        $this->assert_can_boot();
1✔
337

338
        $this->status = self::STATUS_BOOTING;
×
339

340
        Binding\Prototype\AutoConfig::configure($this);
×
341

342
        emit(new BootEvent($this));
×
343

344
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
345

346
        $this->status = self::STATUS_BOOTED;
×
347
    }
348

349
    /**
350
     * Asserts that the application is not booted yet.
351
     *
352
     * @throws InvalidState
353
     */
354
    private function assert_can_boot(): void
355
    {
356
        if ($this->status >= self::STATUS_BOOTING) {
1✔
357
            throw InvalidState::already_booted();
1✔
358
        }
359
    }
360

361
    private Request $request;
362

363
    private function get_request(): Request
364
    {
365
        /** @var Request */
366
        return $this->request ??= Request::from($_SERVER);
1✔
367
    }
368

369
    /**
370
     * Run the application.
371
     *
372
     * To avoid error messages triggered by PHP fatal errors to be sent with a 200 (Ok) HTTP code, the HTTP code is
373
     * changed to 500 before the application is run (and booted). When the process runs properly, the response changes
374
     * the HTTP code to the appropriate value.
375
     *
376
     * @param Request|null $request The request to handle. If `null`, a request is created from `$_SERVER`.
377
     */
378
    public function run(Request $request = null): void
379
    {
380
        $this->initialize_response_header();
×
381
        $this->assert_can_run();
×
382

383
        $this->status = self::STATUS_RUNNING;
×
384

385
        $this->request = $request ??= Request::from($_SERVER);
×
386

387
        emit(new RunEvent($this, $request));
×
388

389
        $response = $this->service_for_class(Responder::class)->respond($request);
×
390
        $response();
×
391

392
        $this->terminate($request, $response);
×
393
    }
394

395
    /**
396
     * Asserts that the application is not running yet.
397
     *
398
     * @throws InvalidState
399
     */
400
    private function assert_can_run(): void
401
    {
402
        if ($this->status < self::STATUS_BOOTED) {
×
403
            throw InvalidState::not_booted();
×
404
        }
405

406
        if ($this->status >= self::STATUS_RUNNING) {
×
407
            throw InvalidState::already_running();
×
408
        }
409
    }
410

411
    /**
412
     * Initializes default response header.
413
     *
414
     * The default response has the {@link ResponseStatus::STATUS_INTERNAL_SERVER_ERROR} status code and the appropriate
415
     * header fields, so it is not cached. That way, if something goes wrong and an error message is displayed, it won't
416
     * be cached by a proxy.
417
     */
418
    private function initialize_response_header(): void
419
    {
420
        http_response_code(ResponseStatus::STATUS_INTERNAL_SERVER_ERROR);
×
421

422
        // @codeCoverageIgnoreStart
423
        if (!headers_sent()) {
424
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
425
            header('Pragma: no-cache');
426
            header('Expires: 0');
427
        }
428
        // @codeCoverageIgnoreEnd
429
    }
430

431
    /**
432
     * Terminate the application.
433
     *
434
     * Emits {@link TerminateEvent}.
435
     */
436
    private function terminate(Request $request, Response $response): void
437
    {
438
        $this->status = self::STATUS_TERMINATING;
×
439

440
        emit(new TerminateEvent($this, $request, $response));
×
441

442
        $this->status = self::STATUS_TERMINATED;
×
443
    }
444

445
    /**
446
     * Emits {@link ClearCacheEvent}
447
     */
448
    public function clear_cache(): void
449
    {
450
        emit(new ClearCacheEvent($this));
1✔
451
    }
452
}
453

454
/*
455
 * Possessions don't touch you in your heart.
456
 * Possessions only tear you apart.
457
 * Possessions cannot kiss you good night.
458
 * Possessions will never hold you tight.
459
 */
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