-
Notifications
You must be signed in to change notification settings - Fork 138
Add settings screen to toggle modules #30
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d1a48f0
Implement settings screen to toggle modules (WIP, for now with demo m…
felixarntz 8036088
Merge branch 'add/phpunit-infrastructure' into add/27-settings-screen
felixarntz c0c7c04
Merge branch 'add/phpunit-infrastructure' into add/27-settings-screen
felixarntz 7ad8ca7
Add more admin/load.php test coverage and enhance documentation with …
felixarntz 08960ac
Implement functions to parse modules with their data from modules dir…
felixarntz 25288b1
Merge branch 'trunk' into add/27-settings-screen
felixarntz d1e2a82
Change menu title for settings screen to simply Performance.
felixarntz ebf8624
Only load admin integration when in admin.
felixarntz d4764ff
Ensure all module code is loaded for PHPUnit tests.
felixarntz dad2f0d
Rename testdata/modules to testdata/demo-modules to not conflict with…
felixarntz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.