App   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 117
c 3
b 0
f 0
dl 0
loc 432
rs 9.76
wmc 33

19 Methods

Rating   Name   Duplication   Size   Complexity  
A has_module_manager() 0 2 1
A container_config() 0 6 2
A set_view_path() 0 3 1
A __construct() 0 5 1
A is_booted() 0 2 1
A set_app_config() 0 23 2
A set_container() 0 7 2
A set_module_manager() 0 7 2
A registration_classes() 0 9 3
A set_loader() 0 7 2
A module() 0 13 3
A __debugInfo() 0 8 1
A finalise() 0 80 1
A has_app_config() 0 2 1
A boot() 0 12 2
A get_container() 0 6 2
A make() 0 5 2
A config() 0 5 2
A view() 0 8 2
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * Primary App container.
6
 *
7
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
8
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
9
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
10
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
11
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
12
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
13
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
14
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
15
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
16
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
17
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18
 *
19
 * @author Glynn Quelch <[email protected]>
20
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
21
 * @package PinkCrab\Perique
22
 * @since 0.4.0
23
 */
24
25
namespace PinkCrab\Perique\Application;
26
27
use PinkCrab\Loader\Hook_Loader;
28
use PinkCrab\Perique\Application\Hooks;
29
use PinkCrab\Perique\Interfaces\Module;
30
use PinkCrab\Perique\Services\View\View;
31
use PinkCrab\Perique\Utils\Object_Helper;
32
use PinkCrab\Perique\Application\App_Config;
33
use PinkCrab\Perique\Interfaces\DI_Container;
34
use PinkCrab\Perique\Application\App_Validation;
35
use PinkCrab\Perique\Interfaces\Inject_App_Config;
36
use PinkCrab\Perique\Utils\App_Config_Path_Helper;
37
use PinkCrab\Perique\Interfaces\Inject_Hook_Loader;
38
use PinkCrab\Perique\Interfaces\Inject_DI_Container;
39
use PinkCrab\Perique\Interfaces\Registration_Middleware;
40
use PinkCrab\Perique\Services\Registration\Module_Manager;
41
use PinkCrab\Perique\Exceptions\App_Initialization_Exception;
42
43
final class App {
44
45
46
	/**
47
	 * Defines if the app has already been booted.
48
	 *
49
	 * @var bool
50
	 */
51
	private static bool $booted = false;
52
53
	/**
54
	 * Dependency Injection Container
55
	 *
56
	 * @var DI_Container|null
57
	 */
58
	private static ?DI_Container $container = null;
59
60
	/**
61
	 * The Apps Config
62
	 *
63
	 * @var App_Config|null
64
	 */
65
	private static ?App_Config $app_config = null;
66
67
	/**
68
	 * Handles all modules.
69
	 *
70
	 * @var Module_Manager|null
71
	 */
72
	private ?Module_Manager $module_manager = null;
73
74
	/**
75
	 * Hook Loader
76
	 *
77
	 * @var Hook_Loader|null
78
	 */
79
	private ?Hook_Loader $loader = null;
80
81
	/**
82
	 * App Base path.
83
	 *
84
	 * @var string
85
	 */
86
	private string $base_path;
87
88
	/**
89
	 * Apps view path.
90
	 *
91
	 * @var string
92
	 */
93
	private string $view_path;
94
95
	/**
96
	 * Checks if the app has already been booted.
97
	 *
98
	 * @return boolean
99
	 */
100
	public static function is_booted(): bool {
101
		return self::$booted;
102
	}
103
104
	/**
105
	 * Creates an instance of the app.
106
	 *
107
	 * @param string $base_path
108
	 */
109
	public function __construct( string $base_path ) {
110
		$this->base_path = $base_path;
111
112
		// Assume the view path.
113
		$this->view_path = rtrim( $this->base_path, '/\\' ) . \DIRECTORY_SEPARATOR . 'views';
114
	}
115
116
	/**
117
	 * Set the view path.
118
	 *
119
	 * @param string $view_path
120
	 * @return self
121
	 */
122
	public function set_view_path( string $view_path ): self {
123
		$this->view_path = $view_path;
124
		return $this;
125
	}
126
127
	/**
128
	 * Sets the DI Container.
129
	 *
130
	 * @param \PinkCrab\Perique\Interfaces\DI_Container $container
131
	 * @return self
132
	 * @throws App_Initialization_Exception Code 2
133
	 */
134
	public function set_container( DI_Container $container ): self {
135
		if ( self::$container !== null ) {
136
			throw App_Initialization_Exception::di_container_exists();
137
		}
138
139
		self::$container = $container;
140
		return $this;
141
	}
142
143
	/**
144
	 * Checks if the Module_Manager has been set.
145
	 *
146
	 * @return boolean
147
	 */
148
	public function has_module_manager(): bool {
149
		return $this->module_manager instanceof Module_Manager;
150
	}
151
152
153
	/**
154
	 * Define the app config.
155
	 *
156
	 * @param array<string, mixed> $settings
157
	 * @return self
158
	 * @throws App_Initialization_Exception Code 5
159
	 */
160
	public function set_app_config( array $settings ): self {
161
		if ( self::$app_config !== null ) {
162
			throw App_Initialization_Exception::app_config_exists();
163
		}
164
165
		// Run through the filter to allow for config changes.
166
		$settings = apply_filters( Hooks::APP_INIT_CONFIG_VALUES, $settings );
167
168
		// Ensure the base path and url are defined from app.
169
		$settings['path']           = $settings['path'] ?? array();
170
		$settings['path']['plugin'] = $this->base_path;
171
		$settings['path']['view']   = $this->view_path ?? App_Config_Path_Helper::assume_view_path( $this->base_path );
172
173
		// Get the url from the base path.
174
		$settings['url']           = $settings['url'] ?? array();
175
		$settings['url']['plugin'] = App_Config_Path_Helper::assume_base_url( $this->base_path );
176
		$settings['url']['view']   = App_Config_Path_Helper::assume_view_url(
177
			$this->base_path,
178
			$this->view_path ?? App_Config_Path_Helper::assume_view_path( $this->base_path )
179
		);
180
181
		self::$app_config = new App_Config( $settings );
182
		return $this;
183
	}
184
185
	/**
186
	 * Set the module manager.
187
	 *
188
	 * @param Module_Manager $module_manager
189
	 * @return self
190
	 * @throws App_Initialization_Exception Code 10
191
	 */
192
	public function set_module_manager( Module_Manager $module_manager ): self {
193
		if ( $this->module_manager !== null ) {
194
			throw App_Initialization_Exception::module_manager_exists();
195
		}
196
197
		$this->module_manager = $module_manager;
198
		return $this;
199
	}
200
201
	/**
202
	 * Sets the loader to the app
203
	 *
204
	 * @param \PinkCrab\Loader\Hook_Loader $loader
205
	 * @return self
206
	 */
207
	public function set_loader( Hook_Loader $loader ): self {
208
		if ( $this->loader !== null ) {
209
			throw App_Initialization_Exception::loader_exists();
210
		}
211
		$this->loader = $loader;
212
213
		return $this;
214
	}
215
216
	/**
217
	 * Interface with the container using a callable.
218
	 *
219
	 * @param callable(DI_Container):void $callback
220
	 * @return self
221
	 * @throws App_Initialization_Exception Code 1
222
	 */
223
	public function container_config( callable $callback ): self {
224
		if ( self::$container === null ) {
225
			throw App_Initialization_Exception::requires_di_container();
226
		}
227
		$callback( self::$container );
228
		return $this;
229
	}
230
231
	/**
232
	 * Sets the class list.
233
	 *
234
	 * @param array<class-string> $class_list
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string>.
Loading history...
235
	 * @return self
236
	 * @throws App_Initialization_Exception Code 3
237
	 */
238
	public function registration_classes( array $class_list ): self {
239
		if ( $this->module_manager === null ) {
240
			throw App_Initialization_Exception::requires_module_manager();
241
		}
242
243
		foreach ( $class_list as $class ) {
244
			$this->module_manager->register_class( $class );
245
		}
246
		return $this;
247
	}
248
249
	/**
250
	 * Adds a module to the app.
251
	 *
252
	 * @template Module_Instance of Module
253
	 * @param class-string<Module_Instance>                     $module
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Module_Instance> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Module_Instance>.
Loading history...
254
	 * @param callable(Module, ?Registration_Middleware):Module $callback
255
	 * @return self
256
	 * @throws App_Initialization_Exception Code 1 If DI container not registered
257
	 * @throws App_Initialization_Exception Code 3 If module manager not defined.
258
	 */
259
	public function module( string $module, ?callable $callback = null ): self {
260
		// Check if module manager exists.
261
		if ( $this->module_manager === null ) {
262
			throw App_Initialization_Exception::requires_module_manager();
263
		}
264
265
		if ( self::$container === null ) {
266
			throw App_Initialization_Exception::requires_di_container();
267
		}
268
269
		$this->module_manager->push_module( $module, $callback );
270
271
		return $this;
272
	}
273
274
275
	/**
276
	 * Boots the populated app.
277
	 *
278
	 * @return self
279
	 */
280
	public function boot(): self {
281
282
		// Validate.
283
		$validate = new App_Validation( $this );
284
		if ( $validate->validate() === false ) {
285
			throw App_Initialization_Exception::failed_boot_validation( array_map( 'esc_attr', $validate->errors ) );
286
		}
287
288
		// Run the final process, where all are loaded in via
289
		$this->finalise();
290
		self::$booted = true;
291
		return $this;
292
	}
293
294
	/**
295
	 * Finialises all settings and boots the app on init hook call (priority 1)
296
	 *
297
	 * @return self
298
	 * @throws App_Initialization_Exception (code 9)
299
	 */
300
	private function finalise(): self {
301
302
		// As we have passed validation
303
		/**
304
		 * @var DI_Container self::$container
305
		 */
306
307
		// Bind self to container.
308
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
0 ignored issues
show
Bug introduced by
The method addRule() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

308
		self::$container->/** @scrutinizer ignore-call */ 
309
                    addRule( // @phpstan-ignore-line, already verified if not null

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
309
			'*',
310
			array(
311
				'substitutions' => array(
312
					self::class         => $this,
313
					DI_Container::class => self::$container,
314
					\wpdb::class        => $GLOBALS['wpdb'],
315
				),
316
			)
317
		);
318
319
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
320
			App_Config::class,
321
			array(
322
				'constructParams' => array(
323
					// @phpstan-ignore-next-line, already verified if not null
324
					self::$app_config->export_settings(),
0 ignored issues
show
Bug introduced by
The method export_settings() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

324
					self::$app_config->/** @scrutinizer ignore-call */ 
325
                        export_settings(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
325
				),
326
			)
327
		);
328
329
		// Allow the passing of Hook Loader via interface and method injection.
330
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
331
			Inject_Hook_Loader::class,
332
			array(
333
				'call' => array(
334
					array( 'set_hook_loader', array( $this->loader ) ),
335
				),
336
			)
337
		);
338
339
		//Allow the passing of App Config via interface and method injection.
340
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
341
			Inject_App_Config::class,
342
			array(
343
				'call' => array(
344
					array( 'set_app_config', array( self::$app_config ) ),
345
				),
346
			)
347
		);
348
349
		// Allow the passing of DI Container via interface and method injection.
350
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
351
			Inject_DI_Container::class,
352
			array(
353
				'call' => array(
354
					array( 'set_di_container', array( self::$container ) ),
355
				),
356
			)
357
		);
358
359
		// Build all modules and middleware.
360
		$this->module_manager->register_modules(); // @phpstan-ignore-line, already verified if not null
0 ignored issues
show
Bug introduced by
The method register_modules() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

360
		$this->module_manager->/** @scrutinizer ignore-call */ 
361
                         register_modules(); // @phpstan-ignore-line, already verified if not null

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
361
362
		/**
363
 * @hook{string, App_Config, Loader, DI_Container}
364
*/
365
		do_action( Hooks::APP_INIT_PRE_BOOT, self::$app_config, $this->loader, self::$container ); // phpcs:disable WordPress.NamingConventions.ValidHookName.*
366
367
		// Initialise on init
368
		add_action(
369
			'init',
370
			function () {
371
				do_action( Hooks::APP_INIT_PRE_REGISTRATION, self::$app_config, $this->loader, self::$container );
372
				$this->module_manager->process_middleware(); // @phpstan-ignore-line, already verified if not null
373
				do_action( Hooks::APP_INIT_POST_REGISTRATION, self::$app_config, $this->loader, self::$container );
374
				$this->loader->register_hooks(); // @phpstan-ignore-line, if loader is not defined, exception will be thrown above
375
			},
376
			1
377
		);
378
379
		return $this;
380
	}
381
382
	// Magic Helpers.
383
384
	/**
385
	 * Creates an instance of class using the DI Container.
386
	 *
387
	 * @param string               $class_string
388
	 * @param array<string, mixed> $args
389
	 *
390
	 * @return object|null
391
	 * @throws App_Initialization_Exception Code 4
392
	 */
393
	public static function make( string $class_string, array $args = array() ): ?object {
394
		if ( self::$booted === false ) {
395
			throw App_Initialization_Exception::app_not_initialized( DI_Container::class );
396
		}
397
		return self::$container->create( $class_string, $args ); // @phpstan-ignore-line, already verified if not null
398
	}
399
400
	/**
401
	 * Gets a value from the internal App_Config
402
	 *
403
	 * @param string $key      The config key to call
404
	 * @param string ...$child Additional params passed.
405
	 * @return mixed
406
	 * @throws App_Initialization_Exception Code 4
407
	 */
408
	public static function config( string $key, string ...$child ) {
409
		if ( self::$booted === false ) {
410
			throw App_Initialization_Exception::app_not_initialized( App_Config::class );
411
		}
412
		return self::$app_config->{$key}( ...$child );
413
	}
414
415
	/**
416
	 * Returns the View helper, populated with current Renderable engine.
417
	 *
418
	 * @return View|null
419
	 * @throws App_Initialization_Exception Code 4
420
	 */
421
	public static function view(): ?View {
422
		if ( self::$booted === false ) {
423
			throw App_Initialization_Exception::app_not_initialized( View::class );
424
		}
425
		/**
426
 * @var ?View
427
*/
428
		return self::$container->create( View::class ); // @phpstan-ignore-line, already verified if not null
429
	}
430
431
	/**
432
	 * Returns the App_Config
433
	 *
434
	 * @return array{
435
	 *  container:?DI_Container,
436
	 *  app_config:?App_Config,
437
	 *  booted:bool,
438
	 *  module_manager:?Module_Manager,
439
	 *  base_path:string,
440
	 *  view_path:?string
441
	 * }
442
	*/
443
	public function __debugInfo(): array {
444
		return array(
445
			'container'      => self::$container,
446
			'app_config'     => self::$app_config,
447
			'booted'         => self::$booted,
448
			'module_manager' => $this->module_manager,
449
			'base_path'      => $this->base_path,
450
			'view_path'      => $this->view_path,
451
		);
452
	}
453
454
	/**
455
	 * Checks if app config set.
456
	 *
457
	 * @return boolean
458
	 */
459
	public function has_app_config(): bool {
460
		return Object_Helper::is_a( self::$app_config, App_Config::class );
461
	}
462
463
	/**
464
	 * Returns the defined container.
465
	 *
466
	 * @return DI_Container
467
	 * @throws App_Initialization_Exception (Code 1)
468
	 */
469
	public function get_container(): DI_Container {
470
		if ( self::$container === null ) {
471
			// Throw container not set.
472
			throw App_Initialization_Exception::requires_di_container();
473
		}
474
		return self::$container;
475
	}
476
}
477