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

[Pattern Directory]: Allow pattern registration from directory with theme.json #38323

Merged
merged 8 commits into from
Feb 10, 2022
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* WP_Theme_JSON_Gutenberg class
* WP_Theme_JSON_5_9 class
*
* @package gutenberg
*/
Expand All @@ -14,7 +14,7 @@
*
* @access private
*/
class WP_Theme_JSON_Gutenberg {
class WP_Theme_JSON_5_9 {

/**
* Container of data in theme.json format.
Expand Down
41 changes: 41 additions & 0 deletions lib/compat/wordpress-6.0/block-patterns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Block patterns registration from `theme.json` and Pattern Directory.
*
* @package gutenberg
*/

/**
* Registers patterns from Pattern Directory provided by a theme's
* `theme.json` file.
*/
function gutenberg_register_remote_theme_patterns() {
$should_load_remote = apply_filters( 'should_load_remote_block_patterns', true );
$theme_has_support = WP_Theme_JSON_Resolver_Gutenberg::theme_has_support();
if ( ! get_theme_support( 'core-block-patterns' ) || ! $should_load_remote || ! $theme_has_support ) {
return;
}

$pattern_settings = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_patterns();
if ( empty( $pattern_settings ) ) {
return;
}
$request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' );
$request['slug'] = implode( ',', $pattern_settings );
$response = rest_do_request( $request );
if ( $response->is_error() ) {
return;
}
$patterns = $response->get_data();
$patterns_registry = WP_Block_Patterns_Registry::get_instance();
foreach ( $patterns as $pattern ) {
$pattern_name = sanitize_title( $pattern['title'] );
// Some patterns might be already registered as core patterns with the `core` prefix.
$is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" );
if ( ! $is_registered ) {
register_block_pattern( $pattern_name, (array) $pattern );
}
}
}

add_action( 'init', 'gutenberg_register_remote_theme_patterns' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
/**
* REST API: Gutenberg_REST_Global_Styles_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Controller which provides REST endpoint for block patterns.
*/
class Gutenberg_REST_Pattern_Directory_Controller extends WP_REST_Pattern_Directory_Controller {
/**
* Search and retrieve block patterns metadata
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
/**
* Include an unmodified `$wp_version`, so the API can craft a response that's tailored to
* it. Some plugins modify the version in a misguided attempt to improve security by
* obscuring the version, which can cause invalid requests.
*/
require ABSPATH . WPINC . '/version.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';

$gutenberg_data = get_plugin_data( dirname( dirname( dirname( __DIR__ ) ) ) . '/gutenberg.php', false );

$query_args = array(
'locale' => get_user_locale(),
'wp-version' => $wp_version, // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- it's defined in `version.php` above.
'gutenberg-version' => $gutenberg_data['Version'],
);

$category_id = $request['category'];
$keyword_id = $request['keyword'];
$search_term = $request['search'];
$slug = $request['slug'];

if ( $category_id ) {
$query_args['pattern-categories'] = $category_id;
}

if ( $keyword_id ) {
$query_args['pattern-keywords'] = $keyword_id;
}

if ( $search_term ) {
$query_args['search'] = $search_term;
}

if ( $slug ) {
$query_args['slug'] = $slug;
}

/**
* Include a hash of the query args, so that different requests are stored in
* separate caches.
*
* MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
* under the character limit for `_site_transient_timeout_{...}` keys.
*
* @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
*/
$transient_key = 'wp_remote_block_patterns_' . md5( implode( '-', $query_args ) );

/**
* Use network-wide transient to improve performance. The locale is the only site
* configuration that affects the response, and it's included in the transient key.
*/
$raw_patterns = get_site_transient( $transient_key );

if ( ! $raw_patterns ) {
$api_url = add_query_arg(
array_map( 'rawurlencode', $query_args ),
'http://api.wordpress.org/patterns/1.0/'
);

if ( wp_http_supports( array( 'ssl' ) ) ) {
$api_url = set_url_scheme( $api_url, 'https' );
}

/**
* Default to a short TTL, to mitigate cache stampedes on high-traffic sites.
* This assumes that most errors will be short-lived, e.g., packet loss that causes the
* first request to fail, but a follow-up one will succeed. The value should be high
* enough to avoid stampedes, but low enough to not interfere with users manually
* re-trying a failed request.
*/
$cache_ttl = 5;
$wporg_response = wp_remote_get( $api_url );
$raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) );

if ( is_wp_error( $wporg_response ) ) {
$raw_patterns = $wporg_response;

} elseif ( ! is_array( $raw_patterns ) ) {
// HTTP request succeeded, but response data is invalid.
$raw_patterns = new WP_Error(
'pattern_api_failed',
sprintf(
/* translators: %s: Support forums URL. */
__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.', 'gutenberg' ),
__( 'https://wordpress.org/support/forums/', 'gutenberg' )
),
array(
'response' => wp_remote_retrieve_body( $wporg_response ),
)
);

} else {
// Response has valid data.
$cache_ttl = HOUR_IN_SECONDS;
}

set_site_transient( $transient_key, $raw_patterns, $cache_ttl );
}

if ( is_wp_error( $raw_patterns ) ) {
$raw_patterns->add_data( array( 'status' => 500 ) );

return $raw_patterns;
}

$response = array();

if ( $raw_patterns ) {
foreach ( $raw_patterns as $pattern ) {
$response[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $pattern, $request )
);
}
}

return new WP_REST_Response( $response );
}
}
45 changes: 45 additions & 0 deletions lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* WP_Theme_JSON_Gutenberg class
*
* @package gutenberg
*/

/**
* Class that encapsulates the processing of structures that adhere to the theme.json spec.
*
* This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
* This is a low-level API that may need to do breaking changes. Please,
* use get_global_settings, get_global_styles, and get_global_stylesheet instead.
*
* @access private
*/
class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 {

/**
* The top-level keys a theme.json can have.
*
* @var string[]
*/
const VALID_TOP_LEVEL_KEYS = array(
'customTemplates',
'patterns',
'settings',
'styles',
'templateParts',
'version',
);

/**
* Returns the current theme's wanted patterns(slugs) to be
* registered from Pattern Directory.
*
* @return array
*/
public function get_patterns() {
if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
return $this->theme_json['patterns'];
}
return array();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of copying the entire file to lib/compat/wordpress-6.0, should we instead keep the original file under lib/compact/wordpress-5.9 but rename the class to match core classname and only declare it if we're on 5.8 (the class doesn't exist) and in lib/compat/wordpress-6.0 extend that class (like you did above for the rest controller and I believe I did that on another controller).

The advantage of this is that we know exactly what's on 5.9 and what was added on 6.0

Copy link
Contributor Author

@ntsekouras ntsekouras Feb 3, 2022

Choose a reason for hiding this comment

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

It seems in many cases including this one, this is quite hard due to private functions and properties. In this case for making it work we need to override the VALID_TOP_LEVEL_KEYS const in child class, but this const is referenced inside private functions in the parent class, so it uses the existing old value from the parent.

I then tried to move more functions to the new child class but we have so many private functions/properties there used, that we would need to move so much code there, changing the names(prefix gutenberg_) in order to be called by the child class without being private, but the content would be the same.

I think we cannot avoid this easily - at least from what I could see, and maybe we should copy the whole class as is here in 6.0 and in followups check what we should change to protected instead of private. That would help as from that point on, to handle this more gracefully.

What do you think? Am I missing something php related?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the problem here is the change in VALID_TOP_LEVEL_KEYS right? Would be good if we could switch at some point to "schema" based validation and avoid the adhoc code. That said, it's subject for another time.

For now, I guess we don't have any other option than copying everything 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case VALID_TOP_LEVEL_KEYS is affected, but in the future we will probably need to make more changes in other private functions. That goes to other classes as well, so that's why I'm suggesting making some things protected or static for 6.0 and then we could make adjustments easier for future releases.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I think each PR should add its own needed changes, if we can land this without copying everything, I'd do that. If we need to make another change later, we'll need to figure how to do it at that point.

Copy link
Member

Choose a reason for hiding this comment

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

What if we add a couple of filters to the JSON parser in WP-Core for 5.9.1?
That would unblock this PR - as well as some others that currently have no way to hook in there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we need to make another change later, we'll need to figure how to do it at that point.

That's the thing though, the way I see it it as I explained above, we cannot avoid copying the class. But if we do not change some private things we will end up copying the class forever(when we have changes of course). Do you find something bad with making some things protected or are there strong reasons for keeping them private?

Copy link
Member

Choose a reason for hiding this comment

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

@youknowriad @ntsekouras I haven't yet dug into an alternative way to extend this. I'll do it later.

I wanted you to be aware that in https://github.com/WordPress/gutenberg/pull/37140/files#r801650187 we'll need to change from private to protected some things in the resolver. I've prepared #38625 that could be part of 5.9.1.

Perhaps we can do similar modifications to WP_Theme_JSON so this doesn't require copying the entire class.

Copy link
Contributor Author

@ntsekouras ntsekouras Feb 8, 2022

Choose a reason for hiding this comment

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

The required changes in core for this to work with a child class that changes VALID_TOP_LEVEL_KEYS and just adds the new get_patterns function would be:

  1. Change $theme_json to protected, because we need to access this from the child class
  2. Update the line that uses VALID_TOP_LEVEL_KEYS from self::VALID_TOP_LEVEL_KEYS to static::VALID_TOP_LEVEL_KEYS, as static:: is inheritance-aware.

Copy link
Member

Choose a reason for hiding this comment

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

Incorporated in #38625 the changes needed for this and pushed at f6a6dd7 and 52ca23f

}
10 changes: 10 additions & 0 deletions lib/compat/wordpress-6.0/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ function gutenberg_register_global_styles_endpoints() {
$editor_settings->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );


/**
* Registers the block pattern directory.
*/
function gutenberg_register_rest_pattern_directory() {
$pattern_directory_controller = new Gutenberg_REST_Pattern_Directory_Controller();
$pattern_directory_controller->register_routes();
}
add_filter( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' );
5 changes: 4 additions & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-5.9/theme-templates.php';
require __DIR__ . '/editor-settings.php';
require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php';
require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-gutenberg.php';
require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-5-9.php';
require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php';
require __DIR__ . '/compat/wordpress-5.9/theme.php';
require __DIR__ . '/compat/wordpress-5.9/admin-menu.php';
Expand All @@ -99,7 +99,10 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.0/post-lock.php';
require __DIR__ . '/compat/wordpress-6.0/blocks.php';
require __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-global-styles-controller.php';
require __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-pattern-directory-controller.php';
require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php';
require __DIR__ . '/compat/wordpress-6.0/rest-api.php';
require __DIR__ . '/compat/wordpress-6.0/block-patterns.php';
require __DIR__ . '/compat/experimental/blocks.php';

require __DIR__ . '/blocks.php';
Expand Down
7 changes: 7 additions & 0 deletions schemas/json/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,13 @@
"required": [ "name" ],
"additionalProperties": false
}
},
"patterns": {
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for doing this!

I thought the reference doc for theme.json was updated upon the schema changes. Apparently, it doesn't. I've now realized that it only contains data coming from the settings & styles keys, but not from customTemplates, templateParts, or this new key.

Copy link
Member

Choose a reason for hiding this comment

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

"description": "An array of pattern slugs to be registered from the Pattern Directory.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [ "version" ],
Expand Down
5 changes: 3 additions & 2 deletions test/emptytheme/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"contentSize": "840px",
"wideSize": "1100px"
}
}
}
},
"patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ]
}