Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Allow block templates to be customised and saved #5062

Merged
merged 25 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
848303b
Ensure $template is object before accessing properties
opr Nov 3, 2021
5acf2b6
Add Gutenberg utils for processing templates based on a post from the db
opr Nov 3, 2021
4bfbc4a
Force theme to always be WooCommerce
opr Nov 3, 2021
5a5e6fb
Add maybe_return_blocks_template and get_single_block_template funcs
opr Nov 3, 2021
4819f63
Set theme to always be woocommerce when making templates from files
opr Nov 3, 2021
5c4b0ed
Check if template has been customised and saved in the database first
opr Nov 3, 2021
495f4d2
Prevent filesystem templates being used if a custom database one exists
opr Nov 3, 2021
3cc5e67
Fix syntax error from rebase
opr Nov 3, 2021
46d4678
Remove unnecessary code from BlockTemplateUtils
opr Nov 4, 2021
2d53a52
Ensure template item is an object containing correct properties
opr Nov 4, 2021
2828553
Prevent warnings from appearing
opr Nov 4, 2021
00d2cdc
Ensure title is added to the template when saving
opr Nov 4, 2021
12fddc9
Filter templates that don't match the queried slug.
opr Nov 5, 2021
ff8293b
Remove unused code
opr Nov 5, 2021
21e08bc
Check if a saved version of the template exists when trying to render
opr Nov 5, 2021
156cb22
Rename default_block_template_is_available to block_template_is_avail…
opr Nov 5, 2021
e715879
Re-hook pre_get_block_template before returning from maybe_return_blo…
opr Nov 5, 2021
238560c
Make comment easier to read
opr Nov 5, 2021
34c33c6
Look for template in woocommerce theme or real theme taxonomy
opr Nov 5, 2021
8760a01
Remove duplicated title assignment
opr Nov 5, 2021
c83af04
Prevent template being added twice when loading from the db
opr Nov 5, 2021
550d218
Filter templates before returning if slugs are supplied
opr Nov 5, 2021
c06a4e5
Simplify `get_block_templates` function into two functions
opr Nov 5, 2021
a365ae8
Add function to stop theme templates that are added after db ones sho…
opr Nov 5, 2021
02dec60
Fix typographical errors
opr Nov 5, 2021
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
256 changes: 234 additions & 22 deletions src/BlockTemplatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,87 @@ public function __construct() {
protected function init() {
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'pre_get_block_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );
}

/**
* This function checks if there's a blocks template (ultimately it resolves either a saved blocks template from the
* database or a template file in `woo-gutenberg-products/block/templates/block-templates/`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* database or a template file in `woo-gutenberg-products/block/templates/block-templates/`)
* database or a template file in `woo-gutenberg-products-block/templates/block-templates/`)

* to return to pre_get_posts short-circuiting the query in Gutenberg.
*
* @param \WP_Block_Template|null $template Return a block template object to short-circuit the default query,
* or null to allow WP to run its normal queries.
* @param string $id Template unique identifier (example: theme_slug//template_slug).
* @param array $template_type wp_template or wp_template_part.
*
* @return mixed|\WP_Block_Template|\WP_Error
*/
public function maybe_return_blocks_template( $template, $id, $template_type ) {
$template_name_parts = explode( '//', $id );
if ( count( $template_name_parts ) < 2 ) {
return $template;
}
list( , $slug ) = $template_name_parts;

// Remove the filter at this point because if we don't then this function will infinite loop.
remove_filter( 'pre_get_block_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );

// Check if the theme has a saved version of this template before falling back to the woo one. Please note how
// the slug has not been modified at this point, we're still using the default one passed to this hook.
$maybe_template = gutenberg_get_block_template( $id, $template_type );
if ( null !== $maybe_template ) {
add_filter( 'pre_get_block_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );
return $maybe_template;
Aljullu marked this conversation as resolved.
Show resolved Hide resolved
}

// Theme-based template didn't exist, try switching the theme to woocommerce and try again. This function has
// been unhooked so won't run again.
add_filter( 'get_block_template', array( $this, 'get_single_block_template' ), 10, 3 );
$maybe_template = gutenberg_get_block_template( 'woocommerce//' . $slug, $template_type );

// Re-hook this function, it was only unhooked to stop recursion.
add_filter( 'pre_get_block_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );
remove_filter( 'get_block_template', array( $this, 'get_single_block_template' ), 10, 3 );
if ( null !== $maybe_template ) {
return $maybe_template;
}

// At this point we haven't had any luck finding a template. Give up and let Gutenberg take control again.
return $template;
}

/**
* Runs on the get_block_template hook. If a template is already found and passed to this function, then return it
* and don't run.
* If a template is *not* passed, try to look for one that matches the ID in the database, if that's not found defer
* to Blocks templates files. Priority goes: DB-Theme, DB-Blocks, Filesystem-Theme, Filesystem-Blocks.
*
* @param \WP_Block_Template $template The found block template.
* @param string $id Template unique identifier (example: theme_slug//template_slug).
* @param array $template_type wp_template or wp_template_part.
*
* @return mixed|null
*/
public function get_single_block_template( $template, $id, $template_type ) {

// The template was already found before the filter runs, just return it immediately.
if ( null !== $template ) {
return $template;
}

$template_name_parts = explode( '//', $id );
if ( count( $template_name_parts ) < 2 ) {
return $template;
}
list( , $slug ) = $template_name_parts;

// If this blocks template doesn't exist then we should just skip the function and let Gutenberg handle it.
if ( ! $this->block_template_is_available( $slug ) ) {
return $template;
}

$available_templates = $this->get_block_templates( array( $slug ) );
return ( is_array( $available_templates ) && count( $available_templates ) > 0 ) ? (object) $available_templates[0] : $template;
}

/**
Expand All @@ -54,13 +135,32 @@ public function add_block_templates( $query_result, $query, $template_type ) {
}

$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
$template_files = $this->get_block_templates();
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
$template_files = $this->get_block_templates( $slugs );

// @todo: Add apply_filters to _gutenberg_get_template_files() in Gutenberg to prevent duplication of logic.
foreach ( $template_files as $template_file ) {
$template = BlockTemplateUtils::gutenberg_build_template_result_from_file( $template_file, 'wp_template' );

if ( $post_type && ! $template->is_custom ) {
// Avoid adding the same template if it's already in the array of $query_result.
if (
array_filter(
$query_result,
function( $query_result_template ) use ( $template_file ) {
return $query_result_template->slug === $template_file->slug &&
$query_result_template->theme === $template_file->theme;
}
)
) {
continue;
}

// It would be custom if the template was modified in the editor, so if it's not custom we can load it from
// the filesystem.
if ( 'custom' !== $template_file->source ) {
$template = BlockTemplateUtils::gutenberg_build_template_result_from_file( $template_file, 'wp_template' );
} else {
$template_file->title = BlockTemplateUtils::convert_slug_to_title( $template_file->slug );
$query_result[] = $template_file;
continue;
}

Expand All @@ -72,55 +172,167 @@ public function add_block_templates( $query_result, $query, $template_type ) {
}

$is_not_custom = false === array_search(
wp_get_theme()->get_stylesheet() . '//' . $template_file['slug'],
wp_get_theme()->get_stylesheet() . '//' . $template_file->slug,
array_column( $query_result, 'id' ),
true
);
$fits_slug_query =
! isset( $query['slug__in'] ) || in_array( $template_file['slug'], $query['slug__in'], true );
! isset( $query['slug__in'] ) || in_array( $template_file->slug, $query['slug__in'], true );
$fits_area_query =
! isset( $query['area'] ) || $template_file['area'] === $query['area'];
! isset( $query['area'] ) || $template_file->area === $query['area'];
$should_include = $is_not_custom && $fits_slug_query && $fits_area_query;
if ( $should_include ) {
$query_result[] = $template;
}
}

$query_result = $this->remove_theme_templates_with_custom_alternative( $query_result );
return $query_result;
}

/**
* Get and build the block template objects from the block template files.
* Removes templates that were added to a theme's block-templates director, but already had a customised version saved in the database.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Removes templates that were added to a theme's block-templates director, but already had a customised version saved in the database.
* Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.

*
* @return array
* @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
*
* @return array List of templates with duplicates removed. The customised alternative is preferred over the theme default.
*/
public function get_block_templates() {
public function remove_theme_templates_with_custom_alternative( $templates ) {

// Get the slugs of all templates that have been customised and saved in the database.
$customised_template_slugs = array_map(
function( $template ) {
return $template->slug;
},
array_values(
array_filter(
$templates,
function( $template ) {
// This template has been customised and saved as a post.
return 'custom' === $template->source;
}
)
)
);

// Remove theme (i.e. filesystem) templates that have the same slug as a customised one. We don't need to check
// for `woocommerce` in $template->source here because woocommerce templates won't have been added to $templates
// if a saved version was found in the db. This only affects saved templates that were saved BEFORE a theme
// template with the same slug was added.
return array_values(
array_filter(
$templates,
function( $template ) use ( $customised_template_slugs ) {
// This template has been customised and saved as a post, so return it.
return ! ( 'theme' === $template->source && in_array( $template->slug, $customised_template_slugs, true ) );
}
)
);
}

/**
* Gets the templates saved in the database.
*
* @param array $slugs An array of slugs to retrieve templates for.
*
* @return int[]|\WP_Post[] An array of found templates.
*/
public function get_block_templates_from_db( $slugs = array() ) {
$check_query_args = array(
'post_type' => 'wp_template',
'posts_per_page' => -1,
'no_found_rows' => true,
'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
array(
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => array( 'woocommerce', get_stylesheet() ),
),
),
);
if ( is_array( $slugs ) && count( $slugs ) > 0 ) {
$check_query_args['post_name__in'] = $slugs;
}
$check_query = new \WP_Query( $check_query_args );
$saved_woo_templates = $check_query->posts;

return array_map(
function( $saved_woo_template ) {
return BlockTemplateUtils::gutenberg_build_template_result_from_post( $saved_woo_template );
},
$saved_woo_templates
);
}

/**
* Gets the templates from the WooCommerce blocks directory, skipping those for which a template already exists
* in the theme directory.
*
* @param string[] $slugs An array of slugs to filter templates by. Templates whose slug does not match will not be returned.
* @param array $already_found_templates Templates that have already been found, these will customised templates that are loaded from the database.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: There seems to be a typo in the comment these will customised.

*
* @return array Templates from the WooCommerce blocks plugin directory.
*/
public function get_block_templates_from_woocommerce( $slugs, $already_found_templates ) {
$template_files = BlockTemplateUtils::gutenberg_get_template_paths( $this->templates_directory );
$templates = array();

foreach ( $template_files as $template_file ) {
Aljullu marked this conversation as resolved.
Show resolved Hide resolved
$template_slug = substr(
$template_file,
strpos( $template_file, self::TEMPLATES_DIR_NAME . DIRECTORY_SEPARATOR ) + 1 + strlen( self::TEMPLATES_DIR_NAME ),
-5
);

// If the theme already has a template then there is no need to load ours in.
if ( $this->theme_has_template( $template_slug ) ) {
// This template does not have a slug we're looking for. Skip it.
if ( is_array( $slugs ) && count( $slugs ) > 0 && ! in_array( $template_slug, $slugs, true ) ) {
continue;
}

// If the theme already has a template, or the template is already in the list (i.e. it came from the
// database) then we should not overwrite it with the one from the filesystem.
if (
$this->theme_has_template( $template_slug ) ||
count(
array_filter(
$already_found_templates,
function ( $template ) use ( $template_slug ) {
$template_obj = (object) $template; //phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
return $template_obj->slug === $template_slug;
}
)
) > 0 ) {
continue;
}

// At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
// or superseded by the theme.
$new_template_item = array(
'slug' => $template_slug,
'path' => $template_file,
'theme' => get_template_directory(),
'type' => 'wp_template',
'slug' => $template_slug,
'id' => 'woocommerce//' . $template_slug,
'path' => $template_file,
'type' => 'wp_template',
'theme' => 'woocommerce',
'source' => 'woocommerce',
'title' => BlockTemplateUtils::convert_slug_to_title( $template_slug ),
'description' => '',
);
$templates[] = $new_template_item;
$templates[] = (object) $new_template_item;
}
return $templates;
}

/**
* Get and build the block template objects from the block template files.
*
* @param array $slugs An array of slugs to retrieve templates for.
* @return array
*/
public function get_block_templates( $slugs = array() ) {
$templates_from_db = $this->get_block_templates_from_db( $slugs );
$templates_from_woo = $this->get_block_templates_from_woocommerce( $slugs, $templates_from_db );
return array_merge( $templates_from_db, $templates_from_woo );
}

/**
* Check if the theme has a template. So we know if to load our own in or not.
*
Expand All @@ -138,14 +350,14 @@ public function theme_has_template( $template_name ) {
* @param string $template_name Template to check.
* @return boolean
*/
public function default_block_template_is_available( $template_name ) {
public function block_template_is_available( $template_name ) {
if ( ! $template_name ) {
return false;
}

return is_readable(
$this->templates_directory . '/' . $template_name . '.html'
);
) || $this->get_block_templates( array( $template_name ) );
}

/**
Expand All @@ -159,19 +371,19 @@ public function render_block_template() {
if (
is_singular( 'product' ) &&
! $this->theme_has_template( 'single-product' ) &&
$this->default_block_template_is_available( 'single-product' )
$this->block_template_is_available( 'single-product' )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} elseif (
is_tax() &&
! $this->theme_has_template( 'taxonomy-product_cat' ) &&
$this->default_block_template_is_available( 'taxonomy-product_cat' )
$this->block_template_is_available( 'taxonomy-product_cat' )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} elseif (
( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) &&
! $this->theme_has_template( 'archive-product' ) &&
$this->default_block_template_is_available( 'archive-product' )
$this->block_template_is_available( 'archive-product' )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
}
Expand Down
Loading