Skip to content

Commit da1199f

Browse files
authored
Merge pull request #8 from Automattic/add-module-loader
feat: add module loader
2 parents 44e1952 + 865e4d0 commit da1199f

9 files changed

+264
-9
lines changed

composer.json

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "publisher-name/project-name",
2+
"name": "publisher-name/custom-plugin",
33
"require-dev": {
44
"dealerdirect/phpcodesniffer-composer-installer": "*",
55
"squizlabs/php_codesniffer": "^3.6",
@@ -10,11 +10,20 @@
1010
"scripts": {
1111
"lint": "./vendor/bin/phpcs --standard=phpcs.xml"
1212
},
13+
"autoload": {
14+
"psr-4": {
15+
"PublisherName\\": "inc/"
16+
},
17+
"files": []
18+
},
1319
"config": {
14-
"allow-plugins": {
15-
"squizlabs/php_codesniffer": true,
16-
"dealerdirect/phpcodesniffer-composer-installer": true,
17-
"wp-coding-standards/wpcs": true
18-
}
19-
}
20+
"platform": {
21+
"php": "8.0"
22+
},
23+
"allow-plugins": {
24+
"squizlabs/php_codesniffer": true,
25+
"dealerdirect/phpcodesniffer-composer-installer": true,
26+
"wp-coding-standards/wpcs": true
27+
}
28+
}
2029
}

inc/.gitkeep

Whitespace-only changes.

inc/Module_Loader.php

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
/**
3+
* Module Loader
4+
*
5+
* @package PublisherName
6+
*/
7+
8+
namespace PublisherName;
9+
10+
defined( 'ABSPATH' ) || exit;
11+
12+
/**
13+
* Module Loader
14+
*/
15+
class Module_Loader {
16+
17+
const MODULES_OPTION_NAME = __NAMESPACE__ . '-modules';
18+
19+
/**
20+
* Initialize the class
21+
*
22+
* @return void
23+
*/
24+
public static function init() {
25+
26+
add_action( 'admin_menu', [ __CLASS__, 'add_admin_menu' ] );
27+
add_filter( 'allowed_options', [ __CLASS__, 'add_allowed_options' ] );
28+
add_action( 'plugins_loaded', [ __CLASS__, 'load_modules' ] );
29+
}
30+
31+
/**
32+
* Add the admin menu.
33+
*
34+
* @return void
35+
*/
36+
public static function add_admin_menu() {
37+
add_options_page(
38+
'Custom Modules',
39+
'Custom Modules',
40+
'manage_options',
41+
'custom-modules',
42+
[ __CLASS__, 'render_admin_page' ]
43+
);
44+
}
45+
46+
/**
47+
* Add the custom modules to the allowed options.
48+
*
49+
* @param array $options The allowed options.
50+
* @return array
51+
*/
52+
public static function add_allowed_options( $options ) {
53+
$options['custom-modules'] = [ self::MODULES_OPTION_NAME ];
54+
return $options;
55+
}
56+
57+
/**
58+
* Render the admin page.
59+
*
60+
* @return void
61+
*/
62+
public static function render_admin_page() {
63+
$current_active_modules = get_option( self::MODULES_OPTION_NAME );
64+
if ( ! is_array( $current_active_modules ) ) {
65+
$current_active_modules = [];
66+
}
67+
$available_modules = self::get_available_modules();
68+
?>
69+
<div class="wrap">
70+
<h1>Custom Modules</h1>
71+
<p>Enable or disable Custom modules</p>
72+
<form method="post" action="options.php">
73+
<?php wp_nonce_field( 'custom-modules-options' ); ?>
74+
<input type="hidden" name="option_page" value="custom-modules">
75+
<input type="hidden" name="action" value="update">
76+
<table class="form-table" role="presentation">
77+
<tbody>
78+
<?php foreach ( $available_modules as $module ) : ?>
79+
<?php $checked = in_array( $module['path'], $current_active_modules, true ) ? 'checked' : ''; ?>
80+
<tr>
81+
<th scope="row"><?php echo esc_html( $module['name'] ); ?></th>
82+
<td>
83+
<fieldset>
84+
<legend class="screen-reader-text">
85+
<span><?php echo esc_html( $module['name'] ); ?></span>
86+
</legend>
87+
<label for="<?php echo esc_attr( $module['slug'] ); ?>">
88+
<input name="<?php echo esc_attr( self::MODULES_OPTION_NAME ); ?>[]" type="checkbox" value="<?php echo esc_attr( $module['path'] ); ?>" <?php echo esc_attr( $checked ); ?>>
89+
Enabled
90+
</label>
91+
<p class="description">
92+
<?php echo esc_html( $module['description'] ); ?>
93+
</p>
94+
</fieldset>
95+
</td>
96+
</tr>
97+
<?php endforeach; ?>
98+
<tbody>
99+
100+
</table>
101+
<?php submit_button(); ?>
102+
</form>
103+
</div>
104+
<?php
105+
}
106+
107+
/**
108+
* Get the available modules in the file system.
109+
*
110+
* @return array
111+
*/
112+
private static function get_available_modules() {
113+
$modules_dir = __DIR__ . '/Modules';
114+
$dirs = scandir( $modules_dir );
115+
$modules = [];
116+
foreach ( $dirs as $dir ) {
117+
if ( '.' === $dir || '..' === $dir ) {
118+
continue;
119+
}
120+
$module_file = $modules_dir . '/' . $dir . '/module.php';
121+
if ( file_exists( $module_file ) ) {
122+
$modules[ $dir ] = [
123+
'slug' => $dir,
124+
'path' => $module_file,
125+
'name' => $dir,
126+
'description' => '',
127+
];
128+
}
129+
130+
$info_file = $modules_dir . '/' . $dir . '/info.json';
131+
if ( file_exists( $info_file ) ) {
132+
$info = json_decode( file_get_contents( $info_file ), true ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
133+
if ( ! empty( $info['name'] ) ) {
134+
$modules[ $dir ]['name'] = $info['name'];
135+
}
136+
if ( ! empty( $info['description'] ) ) {
137+
$modules[ $dir ]['description'] = $info['description'];
138+
}
139+
}
140+
}
141+
return $modules;
142+
}
143+
144+
/**
145+
* Load the active modules.
146+
*
147+
* @return void
148+
*/
149+
public static function load_modules() {
150+
$active_modules = get_option( self::MODULES_OPTION_NAME, [] );
151+
if ( ! empty( $active_modules ) ) {
152+
foreach ( $active_modules as $module ) {
153+
if ( file_exists( $module ) ) {
154+
require_once $module;
155+
}
156+
}
157+
}
158+
}
159+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
/**
3+
* Admin Panel Notice.
4+
*
5+
* @package PublisherName
6+
*/
7+
8+
namespace PublisherName\Modules\Sample;
9+
10+
defined( 'ABSPATH' ) || exit;
11+
12+
/**
13+
* Class to add a notice to the admin panel
14+
*/
15+
class Admin_Panel_Notice {
16+
17+
/**
18+
* Initialize the class
19+
*
20+
* @return void
21+
*/
22+
public static function init() {
23+
add_action( 'admin_bar_menu', [ __CLASS__, 'add_notice' ], 999 );
24+
}
25+
26+
/**
27+
* Adds a notice to the admin bar
28+
*
29+
* @param object $wp_admin_bar The Admin bar object.
30+
* @return void
31+
*/
32+
public static function add_notice( $wp_admin_bar ) {
33+
$args = array(
34+
'id' => 'sample_info',
35+
'title' => 'Custom Module is enabled',
36+
);
37+
$wp_admin_bar->add_node( $args );
38+
}
39+
}

inc/Modules/Sample/info.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "Sample Module",
3+
"description": "This module only adds a note at the admin top bar. It exists only as an example. Delete it and create your own modules."
4+
}

inc/Modules/Sample/module.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
/**
3+
* This file will be automatically loaded when the module is active.
4+
*
5+
* @package PublisherName
6+
*/
7+
8+
namespace PublisherName\Modules\Sample;
9+
10+
/**
11+
* Here you can have code doing stuff. Just remember to always perform actions in a hook callback and never in the global scope.
12+
*/
13+
14+
/**
15+
* You don't need to use classes if you don't want to, but this is an
16+
* example of how classes files are automatically loaded if you follow the naming convention.
17+
*/
18+
Admin_Panel_Notice::init();

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "starter-plugin",
2+
"name": "publisher-name",
33
"version": "1.0.0",
44
"description": "A starting point for custom plugins on the Newspack platform",
55
"author": "Publisher name",
@@ -32,4 +32,4 @@
3232
"husky": "^9.1.7",
3333
"lint-staged": "^13.2.0"
3434
}
35-
}
35+
}

phpcs.xml

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed" />
3838
<exclude name="Universal.NamingConventions.NoReservedKeywordParameterNames" />
3939
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.Found" />
40+
<!-- Allow for PSR-4 file names-->
41+
<exclude name="WordPress.Files.FileName.NotHyphenatedLowercase" />
42+
<exclude name="WordPress.Files.FileName.InvalidClassFileName" />
4043
</rule>
4144

4245
<rule ref="PHPCompatibilityWP"/>

plugin.php

+23
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,26 @@
1919

2020
// Exit if accessed directly.
2121
defined( 'ABSPATH' ) || exit;
22+
23+
// This will load composer's autoload.
24+
// Keep in mind that you need to run `composer dump-autoload` after adding new classes.
25+
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
26+
require_once __DIR__ . '/vendor/autoload.php';
27+
} else {
28+
// Add admin notice in case the plugin has not been built.
29+
add_action(
30+
'admin_notices',
31+
function() {
32+
?>
33+
<div class="notice notice-error">
34+
<p><?php esc_html_e( 'Publisher Name plugin was not properly built.', 'publisher-name' ); ?></p>
35+
</div>
36+
<?php
37+
}
38+
);
39+
return;
40+
}
41+
42+
// Initialize the plugin.
43+
44+
Module_Loader::init();

0 commit comments

Comments
 (0)