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

ICanBoogie / ICanBoogie / 12061486068

28 Nov 2024 02:34AM UTC coverage: 36.628%. Remained the same
12061486068

push

github

olvlvl
Remove session concerns

including the responder for /api/ping

63 of 172 relevant lines covered (36.63%)

0.91 hits per line

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

34.25
/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 string $language Locale language.
44
 * @property-read Storage $storage_for_configs
45
 * @property-read Request $request
46
 */
47
final class Application implements ConfigProvider, ServiceProvider
48
{
49
    /**
50
     * @uses get_is_booting
51
     * @uses get_is_booted
52
     * @uses get_is_running
53
     * @uses get_is_terminating
54
     * @uses get_is_terminated
55
     * @uses get_timezone
56
     * @uses set_timezone
57
     * @uses get_storage_for_configs
58
     * @uses get_vars
59
     * @uses get_request
60
     */
61
    use PrototypeTrait;
62

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

73
    private static Application $instance;
74

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

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

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

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

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

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

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

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

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

143
    public readonly Autoconfig $autoconfig;
144

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

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

160
    private Storage $vars;
161

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

174
    public readonly ConfigProvider $configs;
175
    public readonly AppConfig $config;
176
    public readonly EventCollection $events;
177
    public readonly ContainerInterface $container;
178

179
    private function __construct(Autoconfig $autoconfig)
180
    {
181
        $this->autoconfig = $autoconfig;
×
182

183
        if (!date_default_timezone_get()) {
×
184
            date_default_timezone_set('UTC');
×
185
        }
186

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

194
        // The container can be created once configurations are available.
195

196
        $this->container = ContainerFactory::from($this);
×
197

198
        // Enable the usage of `ref()`.
199

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

204
        // Events can be set up once the container is available.
205

206
        $this->events = $this->service_for_class(EventCollection::class);
×
207

208
        // Enable the usage of `emit()`.
209

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

213
    public function config_for_class(string $class): object
214
    {
215
        return $this->configs->config_for_class($class);
×
216
    }
217

218
    /**
219
     * @template T of object
220
     *
221
     * @param class-string<T> $class
222
     *
223
     * @return T
224
     */
225
    public function service_for_class(string $class): object
226
    {
227
        /** @var T */
228
        return $this->container->get($class);
1✔
229
    }
230

231
    public function service_for_id(string $id, string $class): object
232
    {
233
        $service = $this->container->get($id);
3✔
234

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

237
        return $service;
2✔
238
    }
239

240
    /**
241
     * Creates the configuration provider.
242
     *
243
     * @param array<string, int> $paths Path list.
244
     * @param array<class-string, class-string<Builder<object>>> $builders
245
     */
246
    private function create_config_provider(array $paths, array $builders): ConfigProvider
247
    {
248
        asort($paths, SORT_NUMERIC);
×
249

250
        return new BasicConfigProvider(array_keys($paths), $builders);
×
251
    }
252

253
    /**
254
     * Applies low-level configuration.
255
     */
256
    private function apply_config(AppConfig $config): void
257
    {
258
        $error_handler = $config->error_handler;
×
259

260
        if ($error_handler) {
×
261
            set_error_handler($error_handler);
×
262
        }
263

264
        $exception_handler = $config->exception_handler;
×
265

266
        if ($exception_handler) {
×
267
            set_exception_handler($exception_handler);
×
268
        }
269

270
        if ($config->cache_configs && $this->configs instanceof BasicConfigProvider) {
×
271
            $this->configs->cache = $this->get_storage_for_configs();
×
272
        }
273
    }
274

275
    /**
276
     * Creates a storage engine, using a factory.
277
     *
278
     * @param callable(Application): Storage<string, mixed> $factory
279
     *
280
     * @return Storage<string, mixed>
281
     */
282
    private function create_storage(callable $factory): Storage
283
    {
284
        return $factory($this);
2✔
285
    }
286

287
    /**
288
     * Boot the modules and configure Debug, Prototype, and Events.
289
     *
290
     * Emits {@see BootEvent} after the boot is finished.
291
     *
292
     * The `ICANBOOGIE_READY_TIME_FLOAT` key is added to the `$_SERVER` super global with the
293
     * micro-time at which the boot finished.
294
     */
295
    public function boot(): void
296
    {
297
        $this->assert_can_boot();
1✔
298

299
        $this->status = self::STATUS_BOOTING;
×
300

301
        Binding\Prototype\AutoConfig::configure($this);
×
302

303
        emit(new BootEvent($this));
×
304

305
        $_SERVER['ICANBOOGIE_READY_TIME_FLOAT'] = microtime(true);
×
306

307
        $this->status = self::STATUS_BOOTED;
×
308
    }
309

310
    /**
311
     * Asserts that the application is not booted yet.
312
     *
313
     * @throws InvalidState
314
     */
315
    private function assert_can_boot(): void
316
    {
317
        if ($this->status >= self::STATUS_BOOTING) {
1✔
318
            throw InvalidState::already_booted();
1✔
319
        }
320
    }
321

322
    private Request $request;
323

324
    private function get_request(): Request
325
    {
326
        /** @var Request */
327
        return $this->request ??= Request::from($_SERVER); // @phpstan-ignore argument.type
1✔
328
    }
329

330
    /**
331
     * Run the application.
332
     *
333
     * To avoid error messages triggered by PHP fatal errors to be sent with a 200 (Ok) HTTP code, the HTTP code is
334
     * changed to 500 before the application is run (and booted). When the process runs properly, the response changes
335
     * the HTTP code to the appropriate value.
336
     *
337
     * @param Request|null $request The request to handle. If `null`, a request is created from `$_SERVER`.
338
     */
339
    public function run(?Request $request = null): void
340
    {
341
        $this->initialize_response_header();
×
342
        $this->assert_can_run();
×
343

344
        $this->status = self::STATUS_RUNNING;
×
345

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

348
        emit(new RunEvent($this, $request));
×
349

350
        $response = $this->service_for_class(Responder::class)->respond($request);
×
351
        $response();
×
352

353
        $this->terminate($request, $response);
×
354
    }
355

356
    /**
357
     * Asserts that the application is not running yet.
358
     *
359
     * @throws InvalidState
360
     */
361
    private function assert_can_run(): void
362
    {
363
        if ($this->status < self::STATUS_BOOTED) {
×
364
            throw InvalidState::not_booted();
×
365
        }
366

367
        if ($this->status >= self::STATUS_RUNNING) {
×
368
            throw InvalidState::already_running();
×
369
        }
370
    }
371

372
    /**
373
     * Initializes default response header.
374
     *
375
     * The default response has the {@see ResponseStatus::STATUS_INTERNAL_SERVER_ERROR} status code and the appropriate
376
     * header fields, so it is not cached. That way, if something goes wrong and an error message is displayed, it won't
377
     * be cached by a proxy.
378
     */
379
    private function initialize_response_header(): void
380
    {
381
        http_response_code(ResponseStatus::STATUS_INTERNAL_SERVER_ERROR);
×
382

383
        // @codeCoverageIgnoreStart
384
        if (!headers_sent()) {
385
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
386
            header('Pragma: no-cache');
387
            header('Expires: 0');
388
        }
389
        // @codeCoverageIgnoreEnd
390
    }
391

392
    /**
393
     * Terminate the application.
394
     *
395
     * Emits {@see TerminateEvent}.
396
     */
397
    private function terminate(Request $request, Response $response): void
398
    {
399
        $this->status = self::STATUS_TERMINATING;
×
400

401
        emit(new TerminateEvent($this, $request, $response));
×
402

403
        $this->status = self::STATUS_TERMINATED;
×
404
    }
405

406
    /**
407
     * Emits {@see ClearCacheEvent}
408
     */
409
    public function clear_cache(): void
410
    {
411
        emit(new ClearCacheEvent($this));
1✔
412
    }
413
}
414

415
/*
416
 * Possessions don't touch you in your heart.
417
 * Possessions only tear you apart.
418
 * Possessions cannot kiss you good night.
419
 * Possessions will never hold you tight.
420
 */
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