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

[WIP] Add plugin template registration API #61577

Draft
wants to merge 14 commits into
base: trunk
Choose a base branch
from
Draft
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
10 changes: 10 additions & 0 deletions lib/block-templates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/**
* Block template functions.
*
* @package gutenberg
*/

function gutenberg_register_template( $template_name, $args = array() ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Todo: docblock, including the full arguments array once it's finalized.

I suggest referring the registry args docblock to this function so y'all only need to maintain a single canonical list.

return WP_Block_Templates_Registry::get_instance()->register( $template_name, $args );
}
272 changes: 272 additions & 0 deletions lib/class-wp-block-templates-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
<?php
/**
* Block template functions.
*
* @package Gutenberg
* @since 6.7.0
*/

if ( ! class_exists( 'WP_Block_Templates_Registry' ) ) {
/**
* Core class used for interacting with block templates.
*
* @since 6.7.0
*/
final class WP_Block_Templates_Registry {
/**
* Registered block templates, as `$name => $instance` pairs.
*
* @since 6.7.0
* @var WP_Block_Template[] $registered_block_templates Registered block templates.
*/
private $registered_block_templates = array();

/**
* Container for the main instance of the class.
*
* @since 6.7.0
* @var WP_Block_Templates_Registry|null
*/
private static $instance = null;

/**
* Registers a block template.
*
* @since 6.7.0
*
* @param string|WP_Block_Template $template_name Block template name including namespace, or alternatively
* a complete WP_Block_Template instance. In case a WP_Block_Template
* is provided, the $args parameter will be ignored.
* @param array $args Optional. Array of block template arguments.
* @return WP_Block_Template|false The registered block template on success, or false on failure.
*/
public function register( $template_name, $args = array() ) {

$template = null;
if ( $template_name instanceof WP_Block_Template ) {
$template = $template_name;
$template_name = $template->name;
}

if ( ! is_string( $template_name ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Block template names must be a string.', 'gutenberg' ),
'6.7.0'
);
return new WP_Error( 'template_name_no_string', __( 'Block template names must be a string.', 'gutenberg' ) );
}

if ( preg_match( '/[A-Z]+/', $template_name ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Block template names must not contain uppercase characters.', 'gutenberg' ),
'6.7.0'
);
return new WP_Error( 'template_name_no_uppercase', __( 'Block template names must not contain uppercase characters.', 'gutenberg' ) );
}

$name_matcher = '/^[a-z0-9-]+\/\/[a-z0-9-]+$/';
if ( ! preg_match( $name_matcher, $template_name ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Block template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ),
'6.7.0'
);
return new WP_Error( 'template_no_prefix', __( 'Block template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ) );
}

if ( $this->is_registered( $template_name ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Template name. */
sprintf( __( 'Template "%s" is already registered.', 'gutenberg' ), $template_name ),
'6.7.0'
);
/* translators: %s: Template name. */
return new WP_Error( 'template_already_registered', __( 'Template "%s" is already registered.', 'gutenberg' ) );
}

if ( ! $template ) {
$theme_name = get_stylesheet();
$slug = isset( $args['slug'] ) ? $args['slug'] : explode( '//', $template_name )[1];

$template = new WP_Block_Template();
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming things: Is it specifically a block template or could it be used as a generic template in both block and classic themes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would be the use case to use this for classic themes? This PR hooks into block-theme specific filters and endpoints, so in my mind it should only be intended to be used by block templates.

$template->id = $theme_name . '//' . $slug;
$template->theme = $theme_name;
$template->plugin = isset( $args['plugin'] ) ? $args['plugin'] : '';
$template->author = null;
$template->content = isset( $args['content'] ) ? $args['content'] : '';
$template->source = 'plugin';
Copy link
Contributor

Choose a reason for hiding this comment

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

source and origin AFAIK are used in some calculations for some flows like deleting, checking if is_custom etc.. Should we default to plugin here? I'm a bit wary with source especially because if we only supported custom and theme before, there could be many scattered places in code with an if else block and logic..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I found this in some places so far, like is-template-revertable.js and is-template-removable.js, where I needed to update the checks to account for plugin templates.

Your proposal it's not 100% clear to me. Would you keep source and origin as theme for plugin-registered templates and use the plugin property to identify the templates which come from plugins?

Copy link
Contributor

Choose a reason for hiding this comment

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

Your proposal it's not 100% clear to me. Would you keep source and origin as theme for plugin-registered templates and use the plugin property to identify the templates which come from plugins?

I'm a little unclear on the purposes and differences of these too. I'm not sure why theme, plugin, source and origin are all needed.

Presuming that it's to indicate whether a template is registered via a theme or plugin, I am not sure if it's needed. Shouldn't the prefix/slug be enough of an indication. I think requiring developers to indicate whether it's from a theme or plugin will lead to confusion and mistakes during the registration of template parts as a result of copy/paste errors or the use of a generator.

Copy link
Contributor

Choose a reason for hiding this comment

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

I remember working on this in the past and might be able to shed some light. I remember I found that the templates API had very little support for plugins and had been made originally only for theme provided templates (that's why there are fields like theme rather than something more generic), but at the same time there were already plugins providing templates via one of the available filters.

I remember source was being set to a value of 'plugin' by plugins to match how themes used it, but wasn't suitable for anything because as soon as a template is customized it's value changes to custom. So I added origin as a way to show the original source value even after a template has been customized. I don't think it needs to be set to anything when registering templates, as core I think core copies the source to the origin. You might need to confirm that.

I think themes were also adding theme names/ids/slugs to the theme field, as there was nothing else available. The design team wanted to show the theme title in one of the columns, so where that requirement was from.

I believe I also added author support for custom templates added by users, as that was also missing. That's all I recall, it was a while ago!

I found the PR here - Add author support to templates and template parts

$template->slug = $slug;
$template->type = 'wp_template';
$template->title = isset( $args['title'] ) ? $args['title'] : '';
$template->description = isset( $args['description'] ) ? $args['description'] : '';
peterwilsoncc marked this conversation as resolved.
Show resolved Hide resolved
$template->status = 'publish';
$template->has_theme_file = true;
$template->origin = 'plugin';
$template->is_custom = true;
$template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : '';
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to have something similar to register_taxonomy_for_object_type() to allow devs to extend this list after registration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds like a good idea. Do you think it should be part of this PR? I'm a bit wary of increasing its scope, and I think that's something that can be worked on afterwards. What do you think?

}

$this->registered_block_templates[ $template_name ] = $template;

return $template;
}

/**
* Retrieves all registered block templates.
*
* @since 6.7.0
*
* @return WP_Block_Template[]|false Associative array of `$block_template_name => $block_template` pairs.
*/
public function get_all_registered() {
return $this->registered_block_templates;
}

/**
* Retrieves a registered template by its and name.
*
* @since 6.7.0
*
* @param string $template_name Block template name including namespace.
* @return WP_Block_Template|null|false The registered block template, or null if it is not registered.
*/
public function get_registered( $template_name ) {
if ( ! $this->is_registered( $template_name ) ) {
return null;
}

return $this->registered_block_templates[ $template_name ];
}

/**
* Retrieves a registered template by its slug.
*
* @since 6.7.0
*
* @param string $template_slug Slug of the template.
* @return WP_Block_Template|null The registered block template, or null if it is not registered.
*/
public function get_by_slug( $template_slug ) {
$all_templates = $this->get_all_registered();

if ( ! $all_templates ) {
return null;
}

foreach ( $all_templates as $template ) {
if ( $template->slug === $template_slug ) {
return $template;
}
}

return null;
}

/**
* Retrieves registered block templates matching a query.
*
* @since 6.7.0
*
* @param array $query {
* Arguments to retrieve templates. Optional, empty by default.
*
* @type string[] $slug__in List of slugs to include.
* @type string[] $slug__not_in List of slugs to skip.
* @type string $post_type Post type to get the templates for.
* }
*/
public function get_by_query( $query = array() ) {
$all_templates = $this->get_all_registered();

if ( ! $all_templates ) {
return array();
}

$query = wp_parse_args(
$query,
array(
'slug__in' => array(),
'slug__not_in' => array(),
'post_type' => '',
)
);
$slugs_to_include = $query['slug__in'];
$slugs_to_skip = $query['slug__not_in'];
$post_type = $query['post_type'];

foreach ( $all_templates as $template_name => $template ) {
if ( ! empty( $slugs_to_include ) && ! in_array( $template->slug, $slugs_to_include, true ) ) {
unset( $all_templates[ $template_name ] );
}

if ( ! empty( $slugs_to_skip ) && in_array( $template->slug, $slugs_to_skip, true ) ) {
unset( $all_templates[ $template_name ] );
}

if ( ! empty( $post_type ) && ! in_array( $post_type, $template->post_types, true ) ) {
unset( $all_templates[ $template_name ] );
}
}

return $all_templates;
}

/**
* Checks if a block template is registered.
*
* @since 6.7.0
*
* @param string $template_name Block template name including namespace.
* @return bool True if the template is registered, false otherwise.
*/
public function is_registered( $template_name ) {
return isset( $this->registered_block_templates[ $template_name ] );
}

/**
* Unregisters a block template.
*
* @since 6.7.0
*
* @param string $name Block template name including namespace.
* @return WP_Block_Template|false The unregistered block template on success, or false on failure.
*/
public function unregister( $template_name ) {
if ( ! $this->is_registered( $template_name ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Template name. */
sprintf( __( 'Template "%s" is not registered.', 'gutenberg' ), $template_name ),
'6.7.0'
);
/* translators: %s: Template name. */
return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.', 'gutenberg' ) );
}

$unregistered_block_template = $this->registered_block_templates[ $template_name ];
unset( $this->registered_block_templates[ $template_name ] );

return $unregistered_block_template;
}

/**
* Utility method to retrieve the main instance of the class.
*
* The instance will be created if it does not exist yet.
*
* @since 6.7.0
*
* @return WP_Block_Templates_Registry The main instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}

return self::$instance;
}
}
}
Loading
Loading