Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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(
Copy link
Member

@ThierryA ThierryA Dec 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we shouldn't dynamically get the focuses from the modules header. On one hand it is more dynamic on the other it can easily result too undesired results due to typo or else. Thoughts?

Also, I do think that we may need an additional focus area for the "Infrastructure/API" modules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having these defined in a central location helps providing guidance on what to specify in the module header, and it also allows us to provide any extra information about a focus area (e.g. if in the future we wanted to add descriptions for each focus to the settings screen).

Adding a new focus area would be straightforward, it would only require a PR to expand this array (and it already requires a workflow anyway, as it will need a new GitHub label to be created as well).

Regarding something for "Infrastructure/API", I think that goes into the area of dependencies, or modules that go beyond a single focus. Maybe better to open a separate issue to discuss that, since it's a larger topic? Also it might be hard to evaluate this early as we aren't yet in a situation where it would be needed.

'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
Loading