Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add module loader #8

Merged
merged 9 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ It will give you all of the tools you needs to get up and running quickly.
3. In the Settings->Branches section of the new repository, set the branch protection rules to require a pull request to trunk, an approved review, and status checks to pass before merging.
4. In Settings->Collaborators and teams section of the new repository, add the Newspack team as a collaborators with write access. You may need to add them individually and you can get their usernames from the Newspack team.
5. Checkout the repo locally and run `npm run setup` to install everything.
6. In it doesn't already exist, create and `staging` branch and push it to the remote repository.
7. Start developing!
6. Do a search and replace for `PublisherName` with a camel case slug of your publication.
7. In it doesn't already exist, create and `staging` branch and push it to the remote repository.
8. Start developing!

### Secrets

Expand Down
8 changes: 8 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
"scripts": {
"lint": "./vendor/bin/phpcs --standard=phpcs.xml"
},
"autoload": {
"classmap": [
"./inc"
]
},
"config": {
"platform": {
"php": "8.0"
},
"allow-plugins": {
"squizlabs/php_codesniffer": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
Expand Down
157 changes: 157 additions & 0 deletions inc/class-module-loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php
/**
* Module Loader
*
* @package PublisherName
*/

namespace PublisherName;

defined( 'ABSPATH' ) || exit;

/**
* Module Loader
*/
class Module_Loader {

const MODULES_OPTION_NAME = __NAMESPACE__ . '-modules';

/**
* Initialize the class
*
* @return void
*/
public static function init() {

add_action( 'admin_menu', [ __CLASS__, 'add_admin_menu' ] );
add_filter( 'allowed_options', [ __CLASS__, 'add_allowed_options' ] );
add_action( 'plugins_loaded', [ __CLASS__, 'load_modules' ] );
}

/**
* Add the admin menu.
*
* @return void
*/
public static function add_admin_menu() {
add_options_page(
'Custom Modules',
'Custom Modules',
'manage_options',
'custom-modules',
[ __CLASS__, 'render_admin_page' ]
);
}

/**
* Add the custom modules to the allowed options.
*
* @param array $options The allowed options.
* @return array
*/
public static function add_allowed_options( $options ) {
$options['custom-modules'] = [ self::MODULES_OPTION_NAME ];
return $options;
}

/**
* Render the admin page.
*
* @return void
*/
public static function render_admin_page() {
$current_active_modules = get_option( self::MODULES_OPTION_NAME );
if ( ! is_array( $current_active_modules ) ) {
$current_active_modules = [];
}
$available_modules = self::get_available_modules();
?>
<div class="wrap">
<h1>Custom Modules</h1>
<p>Enable or disable Custom modules</p>
<form method="post" action="options.php">
<?php wp_nonce_field( 'custom-modules-options' ); ?>
<input type="hidden" name="option_page" value="custom-modules">
<input type="hidden" name="action" value="update">
<table class="form-table" role="presentation">
<tbody>
<?php foreach ( $available_modules as $module ) : ?>
<?php $checked = in_array( $module['path'], $current_active_modules, true ) ? 'checked' : ''; ?>
<tr>
<th scope="row"><?php echo esc_html( $module['name'] ); ?></th>
<td>
<fieldset>
<legend class="screen-reader-text">
<span><?php echo esc_html( $module['name'] ); ?></span>
</legend>
<label for="<?php echo esc_attr( $module['slug'] ); ?>">
<input name="<?php echo esc_attr( self::MODULES_OPTION_NAME ); ?>[]" type="checkbox" value="<?php echo esc_attr( $module['path'] ); ?>" <?php echo esc_attr( $checked ); ?>>
Enabled
</label>
<p class="description">
<?php echo esc_html( $module['description'] ); ?>
</p>
</fieldset>
</td>
</tr>
<?php endforeach; ?>
<tbody>

</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}

/**
* Get the available modules in the file system.
*
* @return array
*/
private static function get_available_modules() {
$modules_dir = __DIR__ . '/modules';
$dirs = scandir( $modules_dir );
$modules = [];
foreach ( $dirs as $dir ) {
if ( '.' === $dir || '..' === $dir ) {
continue;
}
$module_file = $modules_dir . '/' . $dir . '/module.php';
if ( file_exists( $module_file ) ) {
$modules[ $dir ] = [
'slug' => $dir,
'path' => $module_file,
'name' => $dir,
'description' => '',
];
}

$info_file = $modules_dir . '/' . $dir . '/info.json';
if ( file_exists( $info_file ) ) {
$info = json_decode( file_get_contents( $info_file ), true ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
if ( ! empty( $info['name'] ) ) {
$modules[ $dir ]['name'] = $info['name'];
}
if ( ! empty( $info['description'] ) ) {
$modules[ $dir ]['description'] = $info['description'];
}
}
}
return $modules;
}

/**
* Load the active modules.
*
* @return void
*/
public static function load_modules() {
$active_modules = get_option( self::MODULES_OPTION_NAME, [] );
foreach ( $active_modules as $module ) {
if ( file_exists( $module ) ) {
require_once $module;
}
}
}
}
39 changes: 39 additions & 0 deletions inc/modules/sample/class-admin-panel-notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* Admin Panel Notice.
*
* @package PublisherName
*/

namespace PublisherName\SampleModule;

defined( 'ABSPATH' ) || exit;

/**
* Class to add a notice to the admin panel
*/
class Admin_Panel_Notice {

/**
* Initialize the class
*
* @return void
*/
public static function init() {
add_action( 'admin_bar_menu', [ __CLASS__, 'add_notice' ], 999 );
}

/**
* Adds a notice to the admin bar
*
* @param object $wp_admin_bar The Admin bar object.
* @return void
*/
public static function add_notice( $wp_admin_bar ) {
$args = array(
'id' => 'sample_info',
'title' => 'Custom Module is enabled',
);
$wp_admin_bar->add_node( $args );
}
}
4 changes: 4 additions & 0 deletions inc/modules/sample/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Sample Module",
"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."
}
18 changes: 18 additions & 0 deletions inc/modules/sample/module.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/**
* This file will be automatically loaded when the module is active.
*
* @package PublisherName
*/

namespace PublisherName\SampleModule;

/**
* Here you can have code doing stuff. Just remember to always perform actions in a hook callback and never in the global scope.
*/

/**
* You don't need to use classes if you don't want to, but this is an
* example of how classes files are automatically loaded if you follow the naming convention.
*/
Admin_Panel_Notice::init();
4 changes: 4 additions & 0 deletions plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

require_once 'vendor/autoload.php';

Module_Loader::init();