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

ICanBoogie / ICanBoogie / 11626221351

01 Nov 2024 07:54AM UTC coverage: 41.117%. Remained the same
11626221351

push

github

olvlvl
Rename PingController as PingResponder

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
 * The application singleton.
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
     * @uses get_session
62
     */
63
    use PrototypeTrait;
64

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

75
    private static Application $instance;
76

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

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

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

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

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

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

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

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

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

145
    public readonly Autoconfig $autoconfig;
146

147
    private ?TimeZone $timezone = null;
148

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

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

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

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

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

193
    private Storage $vars;
194

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

277
        return $service;
2✔
278
    }
279

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

362
    private Request $request;
363

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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