Skip to content

Commit

Permalink
Merge pull request #30 from WordPress/add/27-settings-screen
Browse files Browse the repository at this point in the history
Add settings screen to toggle modules
  • Loading branch information
felixarntz committed Dec 6, 2021
2 parents 6fbfcc4 + dad2f0d commit 2176a10
Show file tree
Hide file tree
Showing 9 changed files with 542 additions and 1 deletion.
295 changes: 295 additions & 0 deletions admin/load.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
<?php
/**
* Admin integration file
*
* @package performance-lab
*/

/**
* Adds the modules page to the Settings menu.
*
* @since 1.0.0
*/
function perflab_add_modules_page() {
$hook_suffix = add_options_page(
__( 'Performance Modules', 'performance-lab' ),
__( 'Performance', 'performance-lab' ),
'manage_options',
PERFLAB_MODULES_SCREEN,
'perflab_render_modules_page'
);

add_action( "load-{$hook_suffix}", 'perflab_load_modules_page', 10, 0 );

return $hook_suffix;
}
add_action( 'admin_menu', 'perflab_add_modules_page' );

/**
* Initializes settings sections and fields for the modules page.
*
* @global array $wp_settings_sections Registered WordPress settings sections.
*
* @since 1.0.0
*
* @param array|null $modules Associative array of available module data, keyed by module slug. By default, this
* will rely on {@see perflab_get_modules()}.
* @param array|null $focus_areas Associative array of focus area data, keyed by focus area slug. By default, this will
* rely on {@see perflab_get_focus_areas()}.
*/
function perflab_load_modules_page( $modules = null, $focus_areas = null ) {
global $wp_settings_sections;

// Register sections for all focus areas, plus 'Other'.
if ( ! is_array( $focus_areas ) ) {
$focus_areas = perflab_get_focus_areas();
}
$sections = $focus_areas;
$sections['other'] = array( 'name' => __( 'Other', 'performance-lab' ) );
foreach ( $sections as $section_slug => $section_data ) {
add_settings_section(
$section_slug,
$section_data['name'],
null,
PERFLAB_MODULES_SCREEN
);
}

// Register fields for all modules.
if ( ! is_array( $modules ) ) {
$modules = perflab_get_modules();
}
$settings = perflab_get_module_settings();
foreach ( $modules as $module_slug => $module_data ) {
$module_settings = isset( $settings[ $module_slug ] ) ? $settings[ $module_slug ] : array();
$module_section = isset( $sections[ $module_data['focus'] ] ) ? $module_data['focus'] : 'other';

// Mark this module's section as added.
$sections[ $module_section ]['added'] = true;

add_settings_field(
$module_slug,
$module_data['name'],
function() use ( $module_slug, $module_data, $module_settings ) {
perflab_render_modules_page_field( $module_slug, $module_data, $module_settings );
},
PERFLAB_MODULES_SCREEN,
$module_section
);
}

// Remove all sections for which there are no modules.
foreach ( $sections as $section_slug => $section_data ) {
if ( empty( $section_data['added'] ) ) {
unset( $wp_settings_sections[ PERFLAB_MODULES_SCREEN ][ $section_slug ] );
}
}
}

/**
* Renders the modules page.
*
* @since 1.0.0
*/
function perflab_render_modules_page() {
?>
<div class="wrap">
<h1>
<?php esc_html_e( 'Performance Modules', 'performance-lab' ); ?>
</h1>

<form action="options.php" method="post" novalidate="novalidate">
<?php settings_fields( PERFLAB_MODULES_SCREEN ); ?>
<?php do_settings_sections( PERFLAB_MODULES_SCREEN ); ?>
<?php submit_button(); ?>
</form>
</div>
<?php
}

/**
* Renders fields for a given module on the modules page.
*
* @since 1.0.0
*
* @param string $module_slug Slug of the module.
* @param array $module_data Associative array of the module's parsed data.
* @param array $module_settings Associative array of the module's current settings.
*/
function perflab_render_modules_page_field( $module_slug, $module_data, $module_settings ) {
$base_id = sprintf( 'module_%s', $module_slug );
$base_name = sprintf( '%1$s[%2$s]', PERFLAB_MODULES_SETTING, $module_slug );
$enabled = isset( $module_settings['enabled'] ) && $module_settings['enabled'];
?>
<fieldset>
<legend class="screen-reader-text">
<?php echo esc_html( $module_data['name'] ); ?>
</legend>
<label for="<?php echo esc_attr( "{$base_id}_enabled" ); ?>">
<input type="checkbox" id="<?php echo esc_attr( "{$base_id}_enabled" ); ?>" name="<?php echo esc_attr( "{$base_name}[enabled]" ); ?>" aria-describedby="<?php echo esc_attr( "{$base_id}_description" ); ?>" value="1"<?php checked( $enabled ); ?>>
<?php
if ( $module_data['experimental'] ) {
printf(
/* translators: %s: module name */
__( 'Enable %s <strong>(experimental)</strong>?', 'performance-lab' ),
esc_html( $module_data['name'] )
);
} else {
printf(
/* translators: %s: module name */
__( 'Enable %s?', 'performance-lab' ),
esc_html( $module_data['name'] )
);
}
?>
</label>
<p id="<?php echo esc_attr( "{$base_id}_description" ); ?>" class="description">
<?php echo esc_html( $module_data['description'] ); ?>
</p>
</fieldset>
<?php
}

/**
* Gets all available focus areas.
*
* @since 1.0.0
*
* @return array Associative array of focus area data, keyed by focus area slug. Fields for every focus area include
* 'name'.
*/
function perflab_get_focus_areas() {
return array(
'images' => array(
'name' => __( 'Images', 'performance-lab' ),
),
'javascript' => array(
'name' => __( 'JavaScript', 'performance-lab' ),
),
'site-health' => array(
'name' => __( 'Site Health', 'performance-lab' ),
),
'measurement' => array(
'name' => __( 'Measurement', 'performance-lab' ),
),
'object-caching' => array(
'name' => __( 'Object caching', 'performance-lab' ),
),
);
}

/**
* Gets all available modules.
*
* This function iterates through the modules directory and therefore should only be called on the modules page.
* It searches all modules, similar to how plugins are searched in the WordPress core function `get_plugins()`.
*
* @since 1.0.0
*
* @param string $modules_root Modules root directory to look for modules in. Default is the `/modules` directory
* in the plugin's root.
* @return array Associative array of parsed module data, keyed by module slug. Fields for every module include
* 'name', 'description', 'focus', and 'experimental'.
*/
function perflab_get_modules( $modules_root = null ) {
if ( null === $modules_root ) {
$modules_root = dirname( __DIR__ ) . '/modules';
}

$modules = array();
$module_files = array();
$modules_dir = @opendir( $modules_root );

if ( $modules_dir ) {
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( ( $file = readdir( $modules_dir ) ) !== false ) {
if ( '.' === substr( $file, 0, 1 ) ) {
continue;
}

// Unlike plugins, modules must be in a directory.
if ( ! is_dir( $modules_root . '/' . $file ) ) {
continue;
}

$module_dir = @opendir( $modules_root . '/' . $file );
if ( $module_dir ) {
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( ( $subfile = readdir( $module_dir ) ) !== false ) {
if ( '.' === substr( $subfile, 0, 1 ) ) {
continue;
}

// Unlike plugins, module main files must be called `load.php`.
if ( 'load.php' !== $subfile ) {
continue;
}

$module_files[] = "$file/$subfile";
}

closedir( $module_dir );
}
}

closedir( $modules_dir );
}

foreach ( $module_files as $module_file ) {
if ( ! is_readable( "$modules_root/$module_file" ) ) {
continue;
}

$module_data = perflab_get_module_data( "$modules_root/$module_file" );
if ( ! $module_data ) {
continue;
}

$modules[ dirname( $module_file ) ] = $module_data;
}

uasort(
$modules,
function( $a, $b ) {
return strnatcasecmp( $a['name'], $b['name'] );
}
);

return $modules;
}

/**
* Parses the module main file to get the module's metadata.
*
* This is similar to how plugin data is parsed in the WordPress core function `get_plugin_data()`.
*
* @since 1.0.0
*
* @param string $module_file Absolute path to the main module file.
* @return array|bool Associative array of parsed module data, or false on failure. Fields for every module include
* 'name', 'description', 'focus', and 'experimental'.
*/
function perflab_get_module_data( $module_file ) {
$default_headers = array(
'name' => 'Module Name',
'description' => 'Description',
'focus' => 'Focus',
'experimental' => 'Experimental',
);

$module_data = get_file_data( $module_file, $default_headers, 'perflab_module' );

// Module name and description are the minimum requirements.
if ( ! $module_data['name'] || ! $module_data['description'] ) {
return false;
}

// Experimental should be a boolean.
if ( 'yes' === strtolower( trim( $module_data['experimental'] ) ) ) {
$module_data['experimental'] = true;
} else {
$module_data['experimental'] = false;
}

return $module_data;
}
3 changes: 2 additions & 1 deletion docs/Writing-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ Every module surfaces on the admin settings page of the performance plugin, wher
* The `load.php` entry point file must contain a module header with the following fields:
* `Module Name`: Name of the module (comparable to `Plugin Name` for plugins). It will be displayed on the performance plugin's settings page.
* `Description`: Brief description of the module (comparable to `Description` for plugins). It will be displayed next to the module name on the performance plugin's settings page.
* `Focus`: Identifier of a single focus area (e.g. `images`, or `javascript`, `site-health`, `measurement`, or `object-caching`). This should correspond to a section on the performance plugin's settings page.
* `Focus`: Identifier of a single focus area (e.g. `images`). This should correspond to a section on the performance plugin's settings page. [See the `perflab_get_focus_areas()` function for the currently available focus areas.](../admin/load.php#L161)
* `Experimental`: Either `Yes` or `No`. If `Yes`, the module will be marked as explicitly experimental on the performance plugin's settings page. While all modules are somewhat experimental (similar to feature plugins), for some that may apply more than for others. For example, certain modules we would encourage limited testing in production for, where we've already established a certain level of reliability/quality, in other cases modules shouldn't be used in production at all.
* The module must neither rely on any PHP code from outside its directory nor on any external PHP code. If relying on an external PHP dependency is essential for a module, the approach should be evaluated and discussed with the wider team.
* The module must use the `performance-lab` text domain for all of its localizable strings.
* All global code structures in the module PHP codebase must be prefixed (e.g. with a string based on the module slug) to avoid conflicts with other modules or plugins.
* All test code for a module (e.g. PHPUnit tests) must be located in a directory `/tests/modules/{module-slug}` where `{module-slug}` is the module's slug (i.e. the same folder name used above).
* If tests require some test-specific structures (e.g. dummy data or mock classes), those should be implemented in a directory `/tests/testdata/modules/{module-slug}`.
* The module must adhere to the WordPress coding and documentation standards.

## Module recommendations
Expand Down
5 changes: 5 additions & 0 deletions load.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,8 @@ function perflab_load_active_modules() {
}

perflab_load_active_modules();

// Only load admin integration when in admin.
if ( is_admin() ) {
require_once plugin_dir_path( __FILE__ ) . 'admin/load.php';
}
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<arg value="ps"/>
<arg name="extensions" value="php"/>

<file>./admin</file>
<file>./load.php</file>
<file>./modules</file>
<file>./tests</file>
Expand Down

0 comments on commit 2176a10

Please sign in to comment.