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

ICanBoogie / ICanBoogie / 11651855083

03 Nov 2024 01:43PM UTC coverage: 36.313%. Remained the same
11651855083

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
final class Application implements ConfigProvider, ServiceProvider
37
{
38
    /**
39
     * Status of the application.
40
     */
41
    private const int STATUS_VOID = 0;
42
    private const int STATUS_BOOTING = 5;
43
    private const int STATUS_BOOTED = 6;
44
    private const int STATUS_RUNNING = 7;
45
    private const int STATUS_TERMINATING = 8;
46
    private const int STATUS_TERMINATED = 9;
47

48
    private static Application $instance;
49

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

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

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

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

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

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

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

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

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

113
    public readonly Autoconfig $autoconfig;
114

115
    public string $timezone_name {
116
        get => $this->timezone->name;
117
        set => $this->timezone = TimeZone::from($value);
118
    }
119

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

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

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

152
    public readonly Config $configs;
153
    public readonly AppConfig $config;
154
    public readonly EventCollection $events;
155
    public readonly ContainerInterface $container;
156

157
    private function __construct(Autoconfig $autoconfig)
158
    {
UNCOV
159
        $this->autoconfig = $autoconfig;
×
160

UNCOV
161
        if (!date_default_timezone_get()) {
×
UNCOV
162
            date_default_timezone_set('UTC');
×
163
        }
164

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

172
        // The container can be created once configurations are available.
173

UNCOV
174
        $this->container = ContainerFactory::from($this);
×
175

176
        // Enable the usage of `ref()`.
177

178
        \ICanBoogie\Service\ServiceProvider::define(
2✔
179
            fn (string $id): object => $this->container->get($id)
2✔
180
        );
2✔
181

182
        // Events can be set up once the container is available.
183

UNCOV
184
        $this->events = $this->service_for_class(EventCollection::class);
×
185

186
        // Enable the usage of `emit()`.
187

188
        EventCollectionProvider::define(fn() => $this->events);
2✔
189
    }
190

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

198
    public function config_for_class(string $class): object
199
    {
200
        return $this->configs->config_for_class($class);
×
201
    }
202

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

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

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

222
        return $service;
2✔
223
    }
224

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

235
        return new Config(array_keys($paths), $builders);
×
236
    }
237

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

UNCOV
245
        if ($error_handler) {
×
UNCOV
246
            set_error_handler($error_handler);
×
247
        }
248

UNCOV
249
        $exception_handler = $config->exception_handler;
×
250

UNCOV
251
        if ($exception_handler) {
×
UNCOV
252
            set_exception_handler($exception_handler);
×
253
        }
254

UNCOV
255
        if ($config->cache_configs) {
×
NEW
256
            $this->configs->cache = $this->storage_for_configs;
×
257
        }
258
    }
259

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

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

UNCOV
284
        $this->status = self::STATUS_BOOTING;
×
285

UNCOV
286
        Binding\Prototype\AutoConfig::configure($this);
×
287

UNCOV
288
        emit(new BootEvent($this));
×
289

UNCOV
290
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
291

292
        $this->status = self::STATUS_BOOTED;
×
293
    }
294

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

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

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

UNCOV
328
        $this->status = self::STATUS_RUNNING;
×
329

UNCOV
330
        $this->request = $request ??= Request::from($_SERVER);
×
331

UNCOV
332
        emit(new RunEvent($this, $request));
×
333

UNCOV
334
        $response = $this->service_for_class(Responder::class)->respond($request);
×
UNCOV
335
        $response();
×
336

UNCOV
337
        $this->terminate($request, $response);
×
338
    }
339

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

UNCOV
351
        if ($this->status >= self::STATUS_RUNNING) {
×
UNCOV
352
            throw InvalidState::already_running();
×
353
        }
354
    }
355

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

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

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

UNCOV
385
        emit(new TerminateEvent($this, $request, $response));
×
386

UNCOV
387
        $this->status = self::STATUS_TERMINATED;
×
388
    }
389

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

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