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

ICanBoogie / ICanBoogie / 11880254438

17 Nov 2024 03:59PM UTC coverage: 42.0% (+2.1%) from 39.896%
11880254438

push

github

olvlvl
Use PHPStan 2.0

10 of 11 new or added lines in 2 files covered. (90.91%)

33 existing lines in 1 file now uncovered.

84 of 200 relevant lines covered (42.0%)

0.92 hits per line

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

36.0
/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
 * @property-read bool $is_booting `true` if the application is booting, `false` otherwise.
38
 * @property-read bool $is_booted `true` if the application is booted, `false` otherwise.
39
 * @property-read bool $is_running `true` if the application is running, `false` otherwise.
40
 * @property-read bool $is_terminating `true` if the application is terminating, `false` otherwise.
41
 * @property-read bool $is_terminated `true` if the application is terminated, `false` otherwise.
42
 * @property Storage $vars Persistent variables registry.
43
 * @property Session $session User's session.
44
 * @property string $language Locale language.
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

UNCOV
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
×
UNCOV
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
    {
UNCOV
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
    {
UNCOV
142
        return $this->status === self::STATUS_TERMINATED;
×
143
    }
144

145
    public readonly Autoconfig $autoconfig;
146

147
    /**
148
     * @var Storage<string, mixed>
149
     */
150
    private Storage $storage_for_configs;
151

152
    /**
153
     * @return Storage<string, mixed>
154
     */
155
    private function get_storage_for_configs(): Storage
156
    {
157
        return $this->storage_for_configs
2✔
158
            /** @phpstan-ignore-next-line */
2✔
159
            ??= $this->create_storage($this->config->storage_for_config);
2✔
160
    }
161

162
    private Storage $vars;
163

164
    /**
165
     * Returns the non-volatile variables registry.
166
     *
167
     * @return Storage<string, mixed>
168
     */
169
    private function get_vars(): Storage
170
    {
171
        return $this->vars
1✔
172
            /** @phpstan-ignore-next-line */
1✔
173
            ??= $this->create_storage($this->config->storage_for_vars);
1✔
174
    }
175

176
    public readonly ConfigProvider $configs;
177
    public readonly AppConfig $config;
178
    public readonly EventCollection $events;
179
    public readonly ContainerInterface $container;
180

181
    private function __construct(Autoconfig $autoconfig)
182
    {
UNCOV
183
        $this->autoconfig = $autoconfig;
×
184

185
        if (!date_default_timezone_get()) {
×
UNCOV
186
            date_default_timezone_set('UTC');
×
187
        }
188

189
        $this->configs = $this->create_config_provider(
×
190
            $autoconfig->config_paths,
×
191
            $autoconfig->config_builders,
×
192
        );
×
193
        $this->config = $this->configs->config_for_class(AppConfig::class);
×
UNCOV
194
        $this->apply_config($this->config);
×
195

196
        // The container can be created once configurations are available.
197

UNCOV
198
        $this->container = ContainerFactory::from($this);
×
199

200
        // Enable the usage of `ref()`.
201

202
        \ICanBoogie\Service\ServiceProvider::define(
2✔
203
            fn (string $id): object => $this->container->get($id)
2✔
204
        );
2✔
205

206
        // Events can be set up once the container is available.
207

UNCOV
208
        $this->events = $this->service_for_class(EventCollection::class);
×
209

210
        // Enable the usage of `emit()`.
211

212
        EventCollectionProvider::define(fn() => $this->events);
2✔
213
    }
214

215
    private function get_session(): Session
216
    {
217
        static $session;
3✔
218

219
        /** @var Session */
220
        return $session ??= SessionWithEvent::for_app($this);
3✔
221
    }
222

223
    public function config_for_class(string $class): object
224
    {
UNCOV
225
        return $this->configs->config_for_class($class);
×
226
    }
227

228
    /**
229
     * @template T of object
230
     *
231
     * @param class-string<T> $class
232
     *
233
     * @return T
234
     */
235
    public function service_for_class(string $class): object
236
    {
237
        /** @var T */
238
        return $this->container->get($class);
1✔
239
    }
240

241
    public function service_for_id(string $id, string $class): object
242
    {
243
        $service = $this->container->get($id);
3✔
244

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

247
        return $service;
2✔
248
    }
249

250
    /**
251
     * Creates the configuration provider.
252
     *
253
     * @param array<string, int> $paths Path list.
254
     * @param array<class-string, class-string<Builder<object>>> $builders
255
     */
256
    private function create_config_provider(array $paths, array $builders): ConfigProvider
257
    {
UNCOV
258
        asort($paths, SORT_NUMERIC);
×
259

UNCOV
260
        return new BasicConfigProvider(array_keys($paths), $builders);
×
261
    }
262

263
    /**
264
     * Applies low-level configuration.
265
     */
266
    private function apply_config(AppConfig $config): void
267
    {
UNCOV
268
        $error_handler = $config->error_handler;
×
269

270
        if ($error_handler) {
×
UNCOV
271
            set_error_handler($error_handler);
×
272
        }
273

UNCOV
274
        $exception_handler = $config->exception_handler;
×
275

276
        if ($exception_handler) {
×
UNCOV
277
            set_exception_handler($exception_handler);
×
278
        }
279

280
        if ($config->cache_configs && $this->configs instanceof BasicConfigProvider) {
×
UNCOV
281
            $this->configs->cache = $this->get_storage_for_configs();
×
282
        }
283
    }
284

285
    /**
286
     * Creates a storage engine, using a factory.
287
     *
288
     * @param callable(Application): Storage<string, mixed> $factory
289
     *
290
     * @return Storage<string, mixed>
291
     */
292
    private function create_storage(callable $factory): Storage
293
    {
294
        return $factory($this);
2✔
295
    }
296

297
    /**
298
     * Boot the modules and configure Debug, Prototype, and Events.
299
     *
300
     * Emits {@see BootEvent} after the boot is finished.
301
     *
302
     * The `ICANBOOGIE_READY_TIME_FLOAT` key is added to the `$_SERVER` super global with the
303
     * micro-time at which the boot finished.
304
     */
305
    public function boot(): void
306
    {
307
        $this->assert_can_boot();
1✔
308

UNCOV
309
        $this->status = self::STATUS_BOOTING;
×
310

UNCOV
311
        Binding\Prototype\AutoConfig::configure($this);
×
312

UNCOV
313
        emit(new BootEvent($this));
×
314

UNCOV
315
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
316

UNCOV
317
        $this->status = self::STATUS_BOOTED;
×
318
    }
319

320
    /**
321
     * Asserts that the application is not booted yet.
322
     *
323
     * @throws InvalidState
324
     */
325
    private function assert_can_boot(): void
326
    {
327
        if ($this->status >= self::STATUS_BOOTING) {
1✔
328
            throw InvalidState::already_booted();
1✔
329
        }
330
    }
331

332
    private Request $request;
333

334
    private function get_request(): Request
335
    {
336
        /** @var Request */
337
        return $this->request ??= Request::from($_SERVER); // @phpstan-ignore argument.type
1✔
338
    }
339

340
    /**
341
     * Run the application.
342
     *
343
     * To avoid error messages triggered by PHP fatal errors to be sent with a 200 (Ok) HTTP code, the HTTP code is
344
     * changed to 500 before the application is run (and booted). When the process runs properly, the response changes
345
     * the HTTP code to the appropriate value.
346
     *
347
     * @param Request|null $request The request to handle. If `null`, a request is created from `$_SERVER`.
348
     */
349
    public function run(?Request $request = null): void
350
    {
351
        $this->initialize_response_header();
×
UNCOV
352
        $this->assert_can_run();
×
353

UNCOV
354
        $this->status = self::STATUS_RUNNING;
×
355

NEW
356
        $this->request = $request ??= Request::from($_SERVER); // @phpstan-ignore argument.type
×
357

UNCOV
358
        emit(new RunEvent($this, $request));
×
359

360
        $response = $this->service_for_class(Responder::class)->respond($request);
×
UNCOV
361
        $response();
×
362

UNCOV
363
        $this->terminate($request, $response);
×
364
    }
365

366
    /**
367
     * Asserts that the application is not running yet.
368
     *
369
     * @throws InvalidState
370
     */
371
    private function assert_can_run(): void
372
    {
373
        if ($this->status < self::STATUS_BOOTED) {
×
UNCOV
374
            throw InvalidState::not_booted();
×
375
        }
376

377
        if ($this->status >= self::STATUS_RUNNING) {
×
UNCOV
378
            throw InvalidState::already_running();
×
379
        }
380
    }
381

382
    /**
383
     * Initializes default response header.
384
     *
385
     * The default response has the {@see ResponseStatus::STATUS_INTERNAL_SERVER_ERROR} status code and the appropriate
386
     * header fields, so it is not cached. That way, if something goes wrong and an error message is displayed, it won't
387
     * be cached by a proxy.
388
     */
389
    private function initialize_response_header(): void
390
    {
UNCOV
391
        http_response_code(ResponseStatus::STATUS_INTERNAL_SERVER_ERROR);
×
392

393
        // @codeCoverageIgnoreStart
394
        if (!headers_sent()) {
395
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
396
            header('Pragma: no-cache');
397
            header('Expires: 0');
398
        }
399
        // @codeCoverageIgnoreEnd
400
    }
401

402
    /**
403
     * Terminate the application.
404
     *
405
     * Emits {@see TerminateEvent}.
406
     */
407
    private function terminate(Request $request, Response $response): void
408
    {
UNCOV
409
        $this->status = self::STATUS_TERMINATING;
×
410

UNCOV
411
        emit(new TerminateEvent($this, $request, $response));
×
412

UNCOV
413
        $this->status = self::STATUS_TERMINATED;
×
414
    }
415

416
    /**
417
     * Emits {@see ClearCacheEvent}
418
     */
419
    public function clear_cache(): void
420
    {
421
        emit(new ClearCacheEvent($this));
1✔
422
    }
423
}
424

425
/*
426
 * Possessions don't touch you in your heart.
427
 * Possessions only tear you apart.
428
 * Possessions cannot kiss you good night.
429
 * Possessions will never hold you tight.
430
 */
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