Passed
Pull Request — master (#158)
by Glynn
07:30
created

App::set_loader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 bool
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 bool
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(
286
				$validate->errors
287
			);
288
		}
289
290
		// Run the final process, where all are loaded in via
291
		$this->finalise();
292
		self::$booted = true;
293
		return $this;
294
	}
295
296
	/**
297
	 * Finialises all settings and boots the app on init hook call (priority 1)
298
	 *
299
	 * @return self
300
	 * @throws App_Initialization_Exception (code 9)
301
	 */
302
	private function finalise(): self {
303
304
		// As we have passed validation
305
		/**
306
		 * @var DI_Container self::$container
307
		 */
308
309
		// Bind self to container.
310
		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

310
		self::$container->/** @scrutinizer ignore-call */ 
311
                    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...
311
			'*',
312
			array(
313
				'substitutions' => array(
314
					self::class         => $this,
315
					DI_Container::class => self::$container,
316
					\wpdb::class        => $GLOBALS['wpdb'],
317
				),
318
			)
319
		);
320
321
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
322
			App_Config::class,
323
			array(
324
				'constructParams' => array(
325
					// @phpstan-ignore-next-line, already verified if not null
326
					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

326
					self::$app_config->/** @scrutinizer ignore-call */ 
327
                        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...
327
				),
328
			)
329
		);
330
331
		// Allow the passing of Hook Loader via interface and method injection.
332
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
333
			Inject_Hook_Loader::class,
334
			array(
335
				'call' => array(
336
					array( 'set_hook_loader', array( $this->loader ) ),
337
				),
338
			)
339
		);
340
341
		//Allow the passing of App Config via interface and method injection.
342
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
343
			Inject_App_Config::class,
344
			array(
345
				'call' => array(
346
					array( 'set_app_config', array( self::$app_config ) ),
347
				),
348
			)
349
		);
350
351
		// Allow the passing of DI Container via interface and method injection.
352
		self::$container->addRule( // @phpstan-ignore-line, already verified if not null
353
			Inject_DI_Container::class,
354
			array(
355
				'call' => array(
356
					array( 'set_di_container', array( self::$container ) ),
357
				),
358
			)
359
		);
360
361
		// Build all modules and middleware.
362
		$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

362
		$this->module_manager->/** @scrutinizer ignore-call */ 
363
                         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...
363
364
		/** @hook{string, App_Config, Loader, DI_Container} */
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
388
	 * @param array<string, mixed> $args
389
	 * @return object|null
390
	 * @throws App_Initialization_Exception Code 4
391
	 */
392
	public static function make( string $class, array $args = array() ): ?object {
393
		if ( self::$booted === false ) {
394
			throw App_Initialization_Exception::app_not_initialized( DI_Container::class );
395
		}
396
		return self::$container->create( $class, $args ); // @phpstan-ignore-line, already verified if not null
397
	}
398
399
	/**
400
	 * Gets a value from the internal App_Config
401
	 *
402
	 * @param string $key The config key to call
403
	 * @param string ...$child Additional params passed.
404
	 * @return mixed
405
	 * @throws App_Initialization_Exception Code 4
406
	 */
407
	public static function config( string $key, string ...$child ) {
408
		if ( self::$booted === false ) {
409
			throw App_Initialization_Exception::app_not_initialized( App_Config::class );
410
		}
411
		return self::$app_config->{$key}( ...$child );
412
	}
413
414
	/**
415
	 * Returns the View helper, populated with current Renderable engine.
416
	 *
417
	 * @return View|null
418
	 * @throws App_Initialization_Exception Code 4
419
	 */
420
	public static function view(): ?View {
421
		if ( self::$booted === false ) {
422
			throw App_Initialization_Exception::app_not_initialized( View::class );
423
		}
424
		/** @var ?View */
425
		return self::$container->create( View::class ); // @phpstan-ignore-line, already verified if not null
426
	}
427
428
	/** @return array{
429
	 *  container:?DI_Container,
430
	 *  app_config:?App_Config,
431
	 *  booted:bool,
432
	 *  module_manager:?Module_Manager,
433
	 *  base_path:string,
434
	 *  view_path:?string
435
	 * } */
436
	public function __debugInfo() {
437
		return array(
438
			'container'      => self::$container,
439
			'app_config'     => self::$app_config,
440
			'booted'         => self::$booted,
441
			'module_manager' => $this->module_manager,
442
			'base_path'      => $this->base_path,
443
			'view_path'      => $this->view_path,
444
		);
445
	}
446
447
	/**
448
	 * Checks if app config set.
449
	 *
450
	 * @return bool
451
	 */
452
	public function has_app_config(): bool {
453
		return Object_Helper::is_a( self::$app_config, App_Config::class );
454
	}
455
456
	/**
457
	 * Returns the defined container.
458
	 *
459
	 * @return DI_Container
460
	 * @throws App_Initialization_Exception (Code 1)
461
	 */
462
	public function get_container(): DI_Container {
463
		if ( self::$container === null ) {
464
			// Throw container not set.
465
			throw App_Initialization_Exception::requires_di_container();
466
		}
467
		return self::$container;
468
	}
469
}
470