diff --git a/docs/registering-classes.md b/docs/registering-classes.md new file mode 100644 index 00000000..71b256bd --- /dev/null +++ b/docs/registering-classes.md @@ -0,0 +1,204 @@ +# Registering Classes in the MU-Plugin + +The MU-Plugin and the theme utilize a system to uniformly, auto-register classes that lie within their namespaces. Whilst there are a few constraints, it eases the requirements for engineers to add their classes to multiple locations each time they add one to the system. + +To do this, it uses the [haydenpierce/class-finder](https://packagist.org/packages/haydenpierce/class-finder) package, which reads the `composer.json` file to help locate files that belong in certain namespaces. + +## How do I define a class to be auto-registered? + +All you need to do to get a class to auto-register is extend the `TenUpPlugin\Module::class` or `TenUpTheme\Module::class` classes. That will require you to implement a `can_register()` and a `register()` method. + +### `can_register()` + +The `can_register()` method is used to decide whether a class should be registered or not. Some examples of valid `can_register()` methods are: + +#### Always Register + +```php +public function can_register() { + return true; +} +``` + +#### Register if in the admin area + +```php +public function can_register() { + return is_admin(); +} +``` + +#### Register if a specific plugin is active + +```php +public function can_register() { + return plugin_active( 'plugin-directory/plugin-name.php' ); +} +``` + +#### Register if running via WP-CLI + +```php +public function can_register() { + return defined( 'WP_CLI' ) && WP_CLI; +} +``` + +As you can hopfully see, it's easy enough to do everything we could previously with this approach. + +### `register()` + +The `register()` method is where you hook in to do your actual logic, E.G. adding `add_action()` or `add_filter()` calls. Your `register()` methods should look something like: + +```php +public function register() { + add_action( 'init', [ $this, 'register_post_types' ] ); +} +``` + +One thing worth noting is that the `register()` method will be called at the priority of `8`. This means that you can use the priorty default of `10` or hook in earlier at `9` if you need to. + +### Putting it all together + +Below is a sample of a class that would be auto-registered when in the admin area, used to register some settings via FieldManager: + +```php +namespace TenUpPlugin\Admin; + +/** + * Provide a Site Settings screen. + */ +class SiteSettings extends \TenUpPlugin\Module { + + /** + * Fieldmanager Setting ID + * + * @var string + */ + public $name = 'site_settings'; + + /** + * Only register if on an admin page and if fieldmanager plugin is active. + * + * @return bool + */ + public function can_register() { + return is_admin() && function_exists( 'fm_register_submenu_page' ); + } + + /** + * Register our hooks. + * + * @return void + */ + public function register() { + add_action( 'init', [ $this, 'register_site_settings' ] ); + } + + /** + * Creates a new Site Settings Screen. + * + * @return void + */ + public function register_site_settings() { + // Register the submenu page. + fm_register_submenu_page( + $this->name, + 'options-general.php', + __( 'Site Settings', 'tenup-plugin' ) + ); + + // Load the fields. + add_action( + 'fm_submenu_' . $this->name, + [ $this, 'load' ] + ); + } + + /** + * Configures the site settings. + * + * @return void + */ + public function load() { + $config = [ + 'name' => $this->name, + 'children' => [ + // FM field config. + ], + ]; + + $fm = new \Fieldmanager_Group( $config ); + $fm->activate_submenu_page(); + } +} + +``` + +## How do I get an instance of my registered class? + +The old way of doing this would be to use the `get_plugin_support()` function. As we no longer define and register our classes in the same way, this doesn't work. + +The best way now, is to use the `get_module()` function that ships with the plugin and the theme. + +```php +$site_settings = \TenUpPlugin\get_module( '\TenUpPlugin\Admin\SiteSettings' ); +$a_theme_class = \TenUpTheme\get_module( '\TenUpTheme\Some\Theme\Class' ); +``` + +If it can't find the class, it will return `false`. + +One major difference between the old way and the new way is that when calling the `get_module()` function, you pass in the class name as a string containing the class name with its full namespace. + +## I need to control the order that my classes load + +By default classes will be loaded in the order they're found. It'd often required to load certain classes before another, for example, loading Taxonomies before Post Types. + +To get around this, there is a `$load_order` property available on the `Module` abstract classes. + +`$load_order` accepts an integer and lets us choose which classes will load first. It has no correlation to the `init` priority to a class, but works in the same way, the lower numbers will load first. + +To see it in action, see below. + +```php +namespace TenUpPlugin\Admin; + +/** + * Taxonomy Factory + */ +class TaxonomyFactory extends \TenUpPlugin\Module { + + public $load_order = 9; + + // Rest of class removed for brevity. +} +``` + +```php +namespace TenUpPlugin\Admin; + +/** + * Post Type Factory + */ +class PostTypeFactory extends \TenUpPlugin\Module { + + // Rest of class removed for brevity. +} +``` + +We've defined two classes, one using the default load order (`10`) and another with a custom load order (`9`). + +Because of this, the `TaxonomyFactory` class will always be loaded before the `PostTypeFactory` class. + + +## Known Issues + +### Could not locate `composer.json` + +During deployment, we must deploy the `composer.json` file. This is how the class finder works, so if it doesn't exist you'll get an exception that states: + +``` +Could not locate composer.json. You can get around this by setting ClassFinder::$appRoot manually. +``` + +More information on this issue is available [here](https://gitlab.com/hpierce1102/ClassFinder/-/blob/master/docs/exceptions/missingComposerConfig.md). diff --git a/mu-plugins/10up-plugin/composer.json b/mu-plugins/10up-plugin/composer.json index 5a95bf1c..4795b46e 100644 --- a/mu-plugins/10up-plugin/composer.json +++ b/mu-plugins/10up-plugin/composer.json @@ -8,11 +8,15 @@ } ], "require": { - "php": ">=7.0" + "php": ">=7.0", + "haydenpierce/class-finder": "^0.4.3" }, "autoload": { "psr-4": { "TenUpPlugin\\": "includes/classes/" - } + }, + "files": [ + "includes/helpers/helpers.php" + ] } } diff --git a/mu-plugins/10up-plugin/composer.lock b/mu-plugins/10up-plugin/composer.lock new file mode 100644 index 00000000..1e3df8f3 --- /dev/null +++ b/mu-plugins/10up-plugin/composer.lock @@ -0,0 +1,66 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c54b04ed0a79b4d1316a4107cedc984d", + "packages": [ + { + "name": "haydenpierce/class-finder", + "version": "0.4.3", + "source": { + "type": "git", + "url": "https://gitlab.com/hpierce1102/ClassFinder.git", + "reference": "d6c68f386c764674c59ee336d1dfca35aada5f90" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/hpierce1102%2FClassFinder/repository/archive.zip?sha=d6c68f386c764674c59ee336d1dfca35aada5f90", + "reference": "d6c68f386c764674c59ee336d1dfca35aada5f90", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HaydenPierce\\ClassFinder\\": "src/", + "HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hayden Pierce", + "email": "hayden@haydenpierce.com" + } + ], + "description": "A library that can provide of a list of classes in a given namespace", + "support": { + "issues": "https://gitlab.com/api/v4/projects/7670051/issues" + }, + "time": "2021-01-05T20:50:49+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.0" + }, + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/mu-plugins/10up-plugin/includes/classes/Module.php b/mu-plugins/10up-plugin/includes/classes/Module.php new file mode 100644 index 00000000..54736c57 --- /dev/null +++ b/mu-plugins/10up-plugin/includes/classes/Module.php @@ -0,0 +1,40 @@ +get_classes() as $class ) { + // Create a slug for the class name. + $slug = $this->slugify_class_name( $class ); + + // If the class has already been initialized, skip it. + if ( isset( $this->classes[ $slug ] ) ) { + continue; + } + + // Create a new reflection of the class. + $reflection_class = new ReflectionClass( $class ); + + // Using reflection, check if the class can be initialized. + // If not, skip. + if ( ! $reflection_class->isInstantiable() ) { + continue; + } + + // Make sure the class is a subclass of Module, so we can initialize it. + if ( ! $reflection_class->isSubclassOf( '\TenUpPlugin\Module' ) ) { + continue; + } + + // Initialize the class. + $instantiated_class = new $class(); + + // Assign the classes into the order they should be initialized. + $load_class_order[ intval( $instantiated_class->load_order ) ][] = [ + 'slug' => $slug, + 'class' => $instantiated_class, + ]; + } + + // Sort the initialized classes by load order. + ksort( $load_class_order ); + + // Loop through the classes and initialize them. + foreach ( $load_class_order as $class_objects ) { + foreach ( $class_objects as $class_object ) { + $class = $class_object['class']; + $slug = $class_object['slug']; + + // If the class can be registered, register it. + if ( $class->can_register() ) { + // Call its register method. + $class->register(); + // Store the class in the list of initialized classes. + $this->classes[ $slug ] = $class; + } + } + } + } + + /** + * Slugify a class name. + * + * @param string $class_name The class name. + * + * @return string + */ + protected function slugify_class_name( $class_name ) { + return sanitize_title( str_replace( '\\', '-', $class_name ) ); + } + + /** + * Get a class by its full class name, including namespace. + * + * @param string $class_name The class name & namespace. + * + * @return false|\TenUpPlugin\Module + */ + public function get_class( $class_name ) { + $class_name = $this->slugify_class_name( $class_name ); + + if ( isset( $this->classes[ $class_name ] ) ) { + return $this->classes[ $class_name ]; + } + + return false; + } + + /** + * Get all the initialized classes. + * + * @return array + */ + public function get_all_classes() { + return $this->classes; + } + +} diff --git a/mu-plugins/10up-plugin/includes/core.php b/mu-plugins/10up-plugin/includes/core.php index 3c2529d1..25e1e8ff 100755 --- a/mu-plugins/10up-plugin/includes/core.php +++ b/mu-plugins/10up-plugin/includes/core.php @@ -7,6 +7,7 @@ namespace TenUpPlugin\Core; +use TenUpPlugin\ModuleInitialization; use \WP_Error; use TenUpPlugin\Utility; @@ -22,7 +23,7 @@ function setup() { }; add_action( 'init', $n( 'i18n' ) ); - add_action( 'init', $n( 'init' ) ); + add_action( 'init', $n( 'init' ), apply_filters( 'tenup_plugin_init_priority', 8 ) ); add_action( 'wp_enqueue_scripts', $n( 'scripts' ) ); add_action( 'wp_enqueue_scripts', $n( 'styles' ) ); add_action( 'admin_enqueue_scripts', $n( 'admin_scripts' ) ); @@ -53,6 +54,24 @@ function i18n() { * @return void */ function init() { + do_action( 'tenup_plugin_before_init' ); + + // If the composer.json isn't found, trigger a warning. + if ( ! file_exists( TENUP_PLUGIN_PATH . 'composer.json' ) ) { + add_action( + 'admin_notices', + function() { + $class = 'notice notice-error'; + /* translators: %s: the path to the plugin */ + $message = sprintf( __( 'The composer.json file was not found within %s. No classes will be loaded.', 'tenup-plugin' ), TENUP_PLUGIN_PATH ); + + printf( '

%2$s

', esc_attr( $class ), esc_html( $message ) ); + } + ); + return; + } + + ModuleInitialization::instance()->init_classes(); do_action( 'tenup_plugin_init' ); } diff --git a/mu-plugins/10up-plugin/includes/helpers/helpers.php b/mu-plugins/10up-plugin/includes/helpers/helpers.php new file mode 100644 index 00000000..8f290756 --- /dev/null +++ b/mu-plugins/10up-plugin/includes/helpers/helpers.php @@ -0,0 +1,19 @@ +get_class( $class_name ); +} diff --git a/themes/10up-theme/composer.json b/themes/10up-theme/composer.json index 2d3e063a..e788c8cc 100644 --- a/themes/10up-theme/composer.json +++ b/themes/10up-theme/composer.json @@ -8,7 +8,8 @@ } ], "require": { - "php": ">=7.0" + "php": ">=7.0", + "haydenpierce/class-finder": "^0.4.3" }, "autoload": { "psr-4": { diff --git a/themes/10up-theme/composer.lock b/themes/10up-theme/composer.lock index 96c3abe2..053ecc21 100644 --- a/themes/10up-theme/composer.lock +++ b/themes/10up-theme/composer.lock @@ -1,11 +1,57 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "adbeae4b8a041b45faf8cc21ddd60ade", - "packages": [], + "content-hash": "3df95c79aa556103ac2c289f8d00d5a7", + "packages": [ + { + "name": "haydenpierce/class-finder", + "version": "0.4.3", + "source": { + "type": "git", + "url": "https://gitlab.com/hpierce1102/ClassFinder.git", + "reference": "d6c68f386c764674c59ee336d1dfca35aada5f90" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/hpierce1102%2FClassFinder/repository/archive.zip?sha=d6c68f386c764674c59ee336d1dfca35aada5f90", + "reference": "d6c68f386c764674c59ee336d1dfca35aada5f90", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HaydenPierce\\ClassFinder\\": "src/", + "HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hayden Pierce", + "email": "hayden@haydenpierce.com" + } + ], + "description": "A library that can provide of a list of classes in a given namespace", + "support": { + "issues": "https://gitlab.com/api/v4/projects/7670051/issues" + }, + "time": "2021-01-05T20:50:49+00:00" + } + ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", @@ -15,5 +61,6 @@ "platform": { "php": ">=7.0" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.1.0" } diff --git a/themes/10up-theme/functions.php b/themes/10up-theme/functions.php index 206ddf44..f762586c 100755 --- a/themes/10up-theme/functions.php +++ b/themes/10up-theme/functions.php @@ -29,6 +29,7 @@ require_once TENUP_THEME_INC . 'template-tags.php'; require_once TENUP_THEME_INC . 'utility.php'; require_once TENUP_THEME_INC . 'blocks.php'; +require_once TENUP_THEME_INC . 'helpers.php'; // Run the setup functions. TenUpTheme\Core\setup(); diff --git a/themes/10up-theme/includes/classes/Module.php b/themes/10up-theme/includes/classes/Module.php new file mode 100644 index 00000000..65bc7bc9 --- /dev/null +++ b/themes/10up-theme/includes/classes/Module.php @@ -0,0 +1,40 @@ +get_classes() as $class ) { + // Create a slug for the class name. + $slug = $this->slugify_class_name( $class ); + + // If the class has already been initialized, skip it. + if ( isset( $this->classes[ $slug ] ) ) { + continue; + } + + // Create a new reflection of the class. + $reflection_class = new ReflectionClass( $class ); + + // Using reflection, check if the class can be initialized. + // If not, skip. + if ( ! $reflection_class->isInstantiable() ) { + continue; + } + + // Make sure the class is a subclass of Module, so we can initialize it. + if ( ! $reflection_class->isSubclassOf( '\TenUpTheme\Module' ) ) { + continue; + } + + // Initialize the class. + $instantiated_class = new $class(); + + // Assign the classes into the order they should be initialized. + $load_class_order[ intval( $instantiated_class->load_order ) ][] = [ + 'slug' => $slug, + 'class' => $instantiated_class, + ]; + } + + // Sort the initialized classes by load order. + ksort( $load_class_order ); + + // Loop through the classes and initialize them. + foreach ( $load_class_order as $class_objects ) { + foreach ( $class_objects as $class_object ) { + $class = $class_object['class']; + $slug = $class_object['slug']; + + // If the class can be registered, register it. + if ( $class->can_register() ) { + // Call its register method. + $class->register(); + // Store the class in the list of initialized classes. + $this->classes[ $slug ] = $class; + } + } + } + } + + /** + * Slugify a class name. + * + * @param string $class_name The class name. + * + * @return string + */ + protected function slugify_class_name( $class_name ) { + return sanitize_title( str_replace( '\\', '-', $class_name ) ); + } + + /** + * Get a class by its full class name, including namespace. + * + * @param string $class_name The class name & namespace. + * + * @return false|\TenUpTheme\Module + */ + public function get_class( $class_name ) { + $class_name = $this->slugify_class_name( $class_name ); + + if ( isset( $this->classes[ $class_name ] ) ) { + return $this->classes[ $class_name ]; + } + + return false; + } + + /** + * Get all the initialized classes. + * + * @return array + */ + public function get_all_classes() { + return $this->classes; + } + +} diff --git a/themes/10up-theme/includes/core.php b/themes/10up-theme/includes/core.php index 67a33615..b6f39c1e 100755 --- a/themes/10up-theme/includes/core.php +++ b/themes/10up-theme/includes/core.php @@ -7,6 +7,7 @@ namespace TenUpTheme\Core; +use TenUpTheme\ModuleInitialization; use TenUpTheme\Utility; /** @@ -19,6 +20,7 @@ function setup() { return __NAMESPACE__ . "\\$function"; }; + add_action( 'init', $n( 'init' ), apply_filters( 'tenup_theme_init_priority', 8 ) ); add_action( 'after_setup_theme', $n( 'i18n' ) ); add_action( 'after_setup_theme', $n( 'theme_setup' ) ); add_action( 'wp_enqueue_scripts', $n( 'scripts' ) ); @@ -33,6 +35,33 @@ function setup() { add_filter( 'script_loader_tag', $n( 'script_loader_tag' ), 10, 2 ); } +/** + * Initializes the theme classes and fires an action plugins can hook into. + * + * @return void + */ +function init() { + do_action( 'tenup_theme_before_init' ); + + // If the composer.json isn't found, trigger a warning. + if ( ! file_exists( TENUP_THEME_PATH . 'composer.json' ) ) { + add_action( + 'admin_notices', + function() { + $class = 'notice notice-error'; + /* translators: %s: the path to the plugin */ + $message = sprintf( __( 'The composer.json file was not found within %s. No classes will be loaded.', 'tenup-theme' ), TENUP_THEME_PATH ); + + printf( '

%2$s

', esc_attr( $class ), esc_html( $message ) ); + } + ); + return; + } + + ModuleInitialization::instance()->init_classes(); + do_action( 'tenup_theme_init' ); +} + /** * Makes Theme available for translation. * diff --git a/themes/10up-theme/includes/helpers.php b/themes/10up-theme/includes/helpers.php new file mode 100644 index 00000000..48f81236 --- /dev/null +++ b/themes/10up-theme/includes/helpers.php @@ -0,0 +1,19 @@ +get_class( $class_name ); +}