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

ICanBoogie / ICanBoogie / 11651834427

03 Nov 2024 01:39PM UTC coverage: 36.313%. Remained the same
11651834427

push

github

olvlvl
Use property hooks

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

41 existing lines in 1 file now uncovered.

65 of 179 relevant lines covered (36.31%)

0.88 hits per line

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

24.59
/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 string $language Locale language.
37
 * @property-read Request $request
38
 */
39
final class Application implements ConfigProvider, ServiceProvider
40
{
41
    /**
42
     * Status of the application.
43
     */
44
    private const int STATUS_VOID = 0;
45
    private const int STATUS_BOOTING = 5;
46
    private const int STATUS_BOOTED = 6;
47
    private const int STATUS_RUNNING = 7;
48
    private const int STATUS_TERMINATING = 8;
49
    private const int STATUS_TERMINATED = 9;
50

51
    private static Application $instance;
52

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

UNCOV
62
        return self::$instance = new self($autoconfig);
×
63
    }
64

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

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

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

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

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

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

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

116
    public readonly Autoconfig $autoconfig;
117

118
    public string $timezone_name {
119
        get => $this->timezone->name;
120
        set => $this->timezone = TimeZone::from($value);
121
    }
122

123
    /**
124
     * The working time zone.
125
     *
126
     * When the time zone is set the default time zone is also set with
127
     * {@see date_default_timezone_set()}.
128
     */
129
    public TimeZone $timezone {
130
        set {
131
            $this->timezone = $value;
132
            date_default_timezone_set($value->name);
133
        }
134
        get => $this->timezone ??= TimeZone::from(date_default_timezone_get() ?: 'UTC');
135
    }
136

137
    /**
138
     * @var Storage<string, mixed>
139
     */
140
    public Storage $storage_for_configs {
141
        get => $this->storage_for_configs
142
            ??= $this->create_storage($this->config->storage_for_config);
143
    }
144

145
    /**
146
     * Returns the non-volatile variables registry.
147
     *
148
     * @return Storage<string, mixed>
149
     */
150
    public Storage $vars {
151
        get => $this->vars
152
            ??= $this->create_storage($this->config->storage_for_vars);
153
    }
154

155
    public readonly Config $configs;
156
    public readonly AppConfig $config;
157
    public readonly EventCollection $events;
158
    public readonly ContainerInterface $container;
159

160
    private function __construct(Autoconfig $autoconfig)
161
    {
UNCOV
162
        $this->autoconfig = $autoconfig;
×
163

UNCOV
164
        if (!date_default_timezone_get()) {
×
UNCOV
165
            date_default_timezone_set('UTC');
×
166
        }
167

UNCOV
168
        $this->configs = $this->create_config_provider(
×
UNCOV
169
            $autoconfig->config_paths,
×
UNCOV
170
            $autoconfig->config_builders,
×
UNCOV
171
        );
×
UNCOV
172
        $this->config = $this->configs->config_for_class(AppConfig::class);
×
UNCOV
173
        $this->apply_config($this->config);
×
174

175
        // The container can be created once configurations are available.
176

UNCOV
177
        $this->container = ContainerFactory::from($this);
×
178

179
        // Enable the usage of `ref()`.
180

181
        \ICanBoogie\Service\ServiceProvider::define(
2✔
182
            fn (string $id): object => $this->container->get($id)
2✔
183
        );
2✔
184

185
        // Events can be set up once the container is available.
186

UNCOV
187
        $this->events = $this->service_for_class(EventCollection::class);
×
188

189
        // Enable the usage of `emit()`.
190

191
        EventCollectionProvider::define(fn() => $this->events);
2✔
192
    }
193

194
    /**
195
     * User's session.
196
     */
197
    public Session $session {
198
        get => $this->session ??= SessionWithEvent::for_app($this);
199
    }
200

201
    public function config_for_class(string $class): object
202
    {
203
        return $this->configs->config_for_class($class);
×
204
    }
205

206
    /**
207
     * @template T of object
208
     *
209
     * @param class-string<T> $class
210
     *
211
     * @return T
212
     */
213
    public function service_for_class(string $class): object
214
    {
215
        /** @var T */
216
        return $this->container->get($class);
1✔
217
    }
218

219
    public function service_for_id(string $id, string $class): object
220
    {
221
        $service = $this->container->get($id);
3✔
222

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

225
        return $service;
2✔
226
    }
227

228
    /**
229
     * Creates the configuration provider.
230
     *
231
     * @param array<string, int> $paths Path list.
232
     * @param array<class-string, class-string<Builder<object>>> $builders
233
     */
234
    private function create_config_provider(array $paths, array $builders): Config
235
    {
UNCOV
236
        asort($paths, SORT_NUMERIC);
×
237

UNCOV
238
        return new Config(array_keys($paths), $builders);
×
239
    }
240

241
    /**
242
     * Applies low-level configuration.
243
     */
244
    private function apply_config(AppConfig $config): void
245
    {
UNCOV
246
        $error_handler = $config->error_handler;
×
247

UNCOV
248
        if ($error_handler) {
×
UNCOV
249
            set_error_handler($error_handler);
×
250
        }
251

UNCOV
252
        $exception_handler = $config->exception_handler;
×
253

UNCOV
254
        if ($exception_handler) {
×
UNCOV
255
            set_exception_handler($exception_handler);
×
256
        }
257

UNCOV
258
        if ($config->cache_configs) {
×
NEW
259
            $this->configs->cache = $this->storage_for_configs;
×
260
        }
261
    }
262

263
    /**
264
     * Creates a storage engine, using a factory.
265
     *
266
     * @param callable(Application): Storage<string, mixed> $factory
267
     *
268
     * @return Storage<string, mixed>
269
     */
270
    private function create_storage(callable $factory): Storage
271
    {
272
        return $factory($this);
2✔
273
    }
274

275
    /**
276
     * Boot the modules and configure Debug, Prototype, and Events.
277
     *
278
     * Emits {@see BootEvent} after the boot is finished.
279
     *
280
     * The `ICANBOOGIE_READY_TIME_FLOAT` key is added to the `$_SERVER` super global with the
281
     * micro-time at which the boot finished.
282
     */
283
    public function boot(): void
284
    {
285
        $this->assert_can_boot();
1✔
286

UNCOV
287
        $this->status = self::STATUS_BOOTING;
×
288

UNCOV
289
        Binding\Prototype\AutoConfig::configure($this);
×
290

UNCOV
291
        emit(new BootEvent($this));
×
292

UNCOV
293
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
294

UNCOV
295
        $this->status = self::STATUS_BOOTED;
×
296
    }
297

298
    /**
299
     * Asserts that the application is not booted yet.
300
     *
301
     * @throws InvalidState
302
     */
303
    private function assert_can_boot(): void
304
    {
305
        if ($this->status >= self::STATUS_BOOTING) {
1✔
306
            throw InvalidState::already_booted();
1✔
307
        }
308
    }
309

310
    /**
311
     * The initial request.
312
     */
313
    public Request $request {
314
        get => $this->request ??= Request::from($_SERVER);
315
    }
316

317
    /**
318
     * Run the application.
319
     *
320
     * To avoid error messages triggered by PHP fatal errors to be sent with a 200 (Ok) HTTP code, the HTTP code is
321
     * changed to 500 before the application is run (and booted). When the process runs properly, the response changes
322
     * the HTTP code to the appropriate value.
323
     *
324
     * @param Request|null $request The request to handle. If `null`, a request is created from `$_SERVER`.
325
     */
326
    public function run(?Request $request = null): void
327
    {
UNCOV
328
        $this->initialize_response_header();
×
UNCOV
329
        $this->assert_can_run();
×
330

UNCOV
331
        $this->status = self::STATUS_RUNNING;
×
332

333
        $this->request = $request ??= Request::from($_SERVER);
×
334

335
        emit(new RunEvent($this, $request));
×
336

337
        $response = $this->service_for_class(Responder::class)->respond($request);
×
UNCOV
338
        $response();
×
339

UNCOV
340
        $this->terminate($request, $response);
×
341
    }
342

343
    /**
344
     * Asserts that the application is not running yet.
345
     *
346
     * @throws InvalidState
347
     */
348
    private function assert_can_run(): void
349
    {
UNCOV
350
        if ($this->status < self::STATUS_BOOTED) {
×
UNCOV
351
            throw InvalidState::not_booted();
×
352
        }
353

UNCOV
354
        if ($this->status >= self::STATUS_RUNNING) {
×
UNCOV
355
            throw InvalidState::already_running();
×
356
        }
357
    }
358

359
    /**
360
     * Initializes default response header.
361
     *
362
     * The default response has the {@see ResponseStatus::STATUS_INTERNAL_SERVER_ERROR} status code and the appropriate
363
     * header fields, so it is not cached. That way, if something goes wrong and an error message is displayed, it won't
364
     * be cached by a proxy.
365
     */
366
    private function initialize_response_header(): void
367
    {
UNCOV
368
        http_response_code(ResponseStatus::STATUS_INTERNAL_SERVER_ERROR);
×
369

370
        // @codeCoverageIgnoreStart
371
        if (!headers_sent()) {
372
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
373
            header('Pragma: no-cache');
374
            header('Expires: 0');
375
        }
376
        // @codeCoverageIgnoreEnd
377
    }
378

379
    /**
380
     * Terminate the application.
381
     *
382
     * Emits {@see TerminateEvent}.
383
     */
384
    private function terminate(Request $request, Response $response): void
385
    {
UNCOV
386
        $this->status = self::STATUS_TERMINATING;
×
387

UNCOV
388
        emit(new TerminateEvent($this, $request, $response));
×
389

UNCOV
390
        $this->status = self::STATUS_TERMINATED;
×
391
    }
392

393
    /**
394
     * Emits {@see ClearCacheEvent}
395
     */
396
    public function clear_cache(): void
397
    {
398
        emit(new ClearCacheEvent($this));
1✔
399
    }
400
}
401

402
/*
403
 * Possessions don't touch you in your heart.
404
 * Possessions only tear you apart.
405
 * Possessions cannot kiss you good night.
406
 * Possessions will never hold you tight.
407
 */
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