From 58cb700e085d794ee1aac1fef7b22b2e1770e81e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 10 May 2023 16:12:15 +0100 Subject: [PATCH] Refactor checkout templates to share logic (#9411) --- src/BlockTemplatesController.php | 136 +++++++++++++++++- src/BlockTypes/ClassicTemplate.php | 2 +- src/Templates/AbstractPageTemplate.php | 138 ++++++++++++++++++ src/Templates/CartTemplate.php | 68 +++------ src/Templates/CheckoutTemplate.php | 177 +++--------------------- src/Templates/OrderReceivedTemplate.php | 47 +++---- src/Utils/BlockTemplateUtils.php | 6 +- 7 files changed, 333 insertions(+), 241 deletions(-) create mode 100644 src/Templates/AbstractPageTemplate.php diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php index a8be46b5798..c009b953d6c 100644 --- a/src/BlockTemplatesController.php +++ b/src/BlockTemplatesController.php @@ -1,7 +1,6 @@ package->is_experimental_build() ) { add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 ); } @@ -566,18 +579,18 @@ public function render_block_template() { } } elseif ( is_cart() && - ! BlockTemplateUtils::theme_has_template( CartTemplate::SLUG ) && $this->block_template_is_available( CartTemplate::SLUG ) + ! BlockTemplateUtils::theme_has_template( CartTemplate::get_slug() ) && $this->block_template_is_available( CartTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } elseif ( is_checkout() && - ! BlockTemplateUtils::theme_has_template( CheckoutTemplate::SLUG ) && $this->block_template_is_available( CheckoutTemplate::SLUG ) + ! BlockTemplateUtils::theme_has_template( CheckoutTemplate::get_slug() ) && $this->block_template_is_available( CheckoutTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } elseif ( is_wc_endpoint_url( 'order-received' ) - && ! BlockTemplateUtils::theme_has_template( OrderReceivedTemplate::SLUG ) - && $this->block_template_is_available( OrderReceivedTemplate::SLUG ) + && ! BlockTemplateUtils::theme_has_template( OrderReceivedTemplate::get_slug() ) + && $this->block_template_is_available( OrderReceivedTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } else { @@ -646,4 +659,117 @@ function_exists( 'is_shop' ) && return $post_type_name; } + + /** + * Registers rewrite endpoints for templates during init. + */ + public function register_template_endpoints() { + $query_vars = WC()->query->get_query_vars(); + $cart_page = CartTemplate::get_legacy_page(); + $cart_endpoint = get_option( 'woocommerce_cart_page_endpoint', $cart_page ? $cart_page->post_name : CartTemplate::get_slug() ); + $checkout_page = CheckoutTemplate::get_legacy_page(); + $checkout_endpoint = get_option( 'woocommerce_checkout_page_endpoint', $checkout_page ? $checkout_page->post_name : CheckoutTemplate::get_slug() ); + + add_rewrite_endpoint( $checkout_endpoint . '/' . $query_vars['order-received'], \EP_ROOT, $query_vars['order-received'] ); + add_rewrite_endpoint( $checkout_endpoint . '/' . $query_vars['order-pay'], \EP_ROOT, $query_vars['order-pay'] ); + add_rewrite_endpoint( $checkout_endpoint, \EP_ROOT, CheckoutTemplate::get_slug() ); + add_rewrite_endpoint( $cart_endpoint, \EP_ROOT, CartTemplate::get_slug() ); + } + + /** + * Filters the `is_checkout` function so we can return true when the endpoint is active, or if one of its other endpoints are in use (e.g. order received). + * + * @param boolean $return True when on the checkout page. + * @return boolean + */ + public function is_checkout_endpoint( $return ) { + global $wp; + + if ( isset( $wp->query_vars[ CheckoutTemplate::get_slug() ] ) || isset( $wp->query_vars[ OrderReceivedTemplate::get_slug() ] ) ) { + return true; + } + + return $return; + } + + /** + * Filters the `is_cart` function so we can return true when the endpoint is active. + * + * @param boolean $return True when on the checkout page. + * @return boolean + */ + public function is_cart_endpoint( $return ) { + global $wp; + + if ( isset( $wp->query_vars[ CartTemplate::get_slug() ] ) ) { + return true; + } + + return $return; + } + + /** + * Replace the cart PAGE URL with the template endpoint URL. + * + * @return string + */ + public function get_cart_url() { + return site_url( '/' . CartTemplate::get_slug() ); + } + + /** + * Replace the checkout PAGE URL with the template endpoint URL. + * + * @return string + */ + public function get_checkout_url() { + return site_url( '/' . CheckoutTemplate::get_slug() ); + } + + /** + * Replaces page settings in WooCommerce with text based permalinks which point to a template. + * + * @param array $settings Settings pages. + * @return array + */ + public function template_permalink_settings( $settings ) { + foreach ( $settings as $key => $setting ) { + if ( 'woocommerce_checkout_page_id' === $setting['id'] ) { + $checkout_page = CheckoutTemplate::get_legacy_page(); + $settings[ $key ] = [ + 'title' => __( 'Checkout page', 'woo-gutenberg-products-block' ), + 'desc' => sprintf( + // translators: %1$s: opening anchor tag, %2$s: closing anchor tag. + __( 'The checkout page template can be %1$s edited here%2$s.', 'woo-gutenberg-products-block' ), + '', + '' + ), + 'desc_tip' => __( 'This is the URL to the checkout page.', 'woo-gutenberg-products-block' ), + 'id' => 'woocommerce_checkout_page_endpoint', + 'type' => 'permalink', + 'default' => $checkout_page ? $checkout_page->post_name : CheckoutTemplate::get_slug(), + 'autoload' => false, + ]; + } + if ( 'woocommerce_cart_page_id' === $setting['id'] ) { + $cart_page = CartTemplate::get_legacy_page(); + $settings[ $key ] = [ + 'title' => __( 'Cart page', 'woo-gutenberg-products-block' ), + 'desc' => sprintf( + // translators: %1$s: opening anchor tag, %2$s: closing anchor tag. + __( 'The cart page template can be %1$s edited here%2$s.', 'woo-gutenberg-products-block' ), + '', + '' + ), + 'desc_tip' => __( 'This is the URL to the cart page.', 'woo-gutenberg-products-block' ), + 'id' => 'woocommerce_cart_page_endpoint', + 'type' => 'permalink', + 'default' => $cart_page ? $cart_page->post_name : CartTemplate::get_slug(), + 'autoload' => false, + ]; + } + } + + return $settings; + } } diff --git a/src/BlockTypes/ClassicTemplate.php b/src/BlockTypes/ClassicTemplate.php index 5720eb51cf7..090f15b8e8d 100644 --- a/src/BlockTypes/ClassicTemplate.php +++ b/src/BlockTypes/ClassicTemplate.php @@ -63,7 +63,7 @@ protected function render( $attributes, $content, $block ) { $frontend_scripts::load_scripts(); } - if ( OrderReceivedTemplate::SLUG === $attributes['template'] ) { + if ( OrderReceivedTemplate::get_slug() === $attributes['template'] ) { return $this->render_order_received(); } diff --git a/src/Templates/AbstractPageTemplate.php b/src/Templates/AbstractPageTemplate.php new file mode 100644 index 00000000000..51968c5c49d --- /dev/null +++ b/src/Templates/AbstractPageTemplate.php @@ -0,0 +1,138 @@ +init(); + } + } + + /** + * Initialization method. + */ + protected function init() { + add_filter( 'page_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 ); + add_filter( 'frontpage_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 ); + add_filter( 'woocommerce_blocks_template_content', array( $this, 'page_template_content' ), 10, 2 ); + add_action( 'current_screen', array( $this, 'page_template_editor_redirect' ) ); + add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) ); + } + + /** + * Returns the template slug. + * + * @return string + */ + abstract public static function get_slug(); + + /** + * Returns the page object assigned to this template/page used for legacy purposes. Pages are no longer required. + * + * @return \WP_Post|null Post object or null. + */ + abstract public static function get_legacy_page(); + + /** + * Should return true on pages/endpoints/routes where the template should be shown. + * + * @return boolean + */ + abstract protected function is_active_template(); + + /** + * Should return the title of the page. + * + * @return string + */ + abstract protected function get_template_title(); + + /** + * Returns the URL to edit the template. + * + * @return string + */ + protected function get_edit_template_url() { + return admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . $this->get_slug() ); + } + + /** + * Get the default content for a template. + * + * Overridden by child class to include their own logic. + * + * @param string $template_content The original content of the template. + * @return string + */ + protected function get_default_template_content( $template_content ) { + return $template_content; + } + + /** + * When the page should be displaying the template, add it to the hierarchy. + * + * This places the template name e.g. `cart`, at the beginning of the template hierarchy array. The hook priority + * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead + * of this one when using the default priority of 10. + * + * @param array $templates Templates that match the pages_template_hierarchy. + */ + public function page_template_hierarchy( $templates ) { + if ( $this->is_active_template() ) { + array_unshift( $templates, $this->get_slug() ); + } + return $templates; + } + + /** + * Returns the default template content. + * + * @param string $template_content The content of the template. + * @param object $template_file The template file object. + * @return string + */ + public function page_template_content( $template_content, $template_file ) { + if ( $this->get_slug() !== $template_file->slug ) { + return $template_content; + } + return $this->get_default_template_content( $template_content ); + } + + /** + * Redirect the edit page screen to the template editor. + * + * @param \WP_Screen $current_screen Current screen information. + */ + public function page_template_editor_redirect( \WP_Screen $current_screen ) { + $page = $this->get_legacy_page(); + $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( $page && $edit_page_id === $page->id ) { + wp_safe_redirect( $this->get_edit_template_url() ); + exit; + } + } + + /** + * Filter the page title when the template is active. + * + * @param string $title Page title. + * @return string + */ + public function page_template_title( $title ) { + if ( $this->is_active_template() ) { + return $this->get_template_title(); + } + return $title; + } +} diff --git a/src/Templates/CartTemplate.php b/src/Templates/CartTemplate.php index 4064b53aa9b..5de42b8cf73 100644 --- a/src/Templates/CartTemplate.php +++ b/src/Templates/CartTemplate.php @@ -6,78 +6,52 @@ * * @internal */ -class CartTemplate { - - const SLUG = 'cart'; - +class CartTemplate extends AbstractPageTemplate { /** - * Constructor. + * Template slug. * - * Templates require FSE theme support, so this will only init if a FSE theme is active. + * @return string */ - public function __construct() { - // Templates require FSE theme support. - if ( ! wc_current_theme_is_fse_theme() ) { - return; - } - $this->init(); + public static function get_slug() { + return 'cart'; } /** - * Initialization method. + * Returns the page object assigned to this template/page used for legacy purposes. Pages are no longer required. + * + * @return \WP_Post|null Post object or null. */ - protected function init() { - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); - add_action( 'current_screen', array( $this, 'template_editor_redirect' ) ); - add_filter( 'woocommerce_blocks_template_content', array( $this, 'default_template_content' ), 10, 3 ); + public static function get_legacy_page() { + $page_id = wc_get_page_id( 'cart' ); + return $page_id ? get_post( $page_id ) : null; } /** - * When the page is displaying the cart and a block theme is active, render the Cart Template. - * - * This places the template name e.g. `cart`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. + * True when viewing the cart page or cart endpoint. * - * @param array $templates Templates that match the pages_template_hierarchy. + * @return boolean */ - public function update_page_template_hierarchy( $templates ) { - if ( is_cart() ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; + protected function is_active_template() { + return is_cart(); } /** - * Redirect a page to the template editor if it's the checkout page and a block theme is active. + * Should return the title of the page. * - * @param \WP_Screen $current_screen Current screen information. + * @return string */ - public function template_editor_redirect( \WP_Screen $current_screen ) { - $page_id = wc_get_page_id( 'cart' ) ?: false; - $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( $edit_page_id === $page_id ) { - wp_safe_redirect( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2Fcart' ) ); - exit; - } + protected function get_template_title() { + return __( 'Cart', 'woo-gutenberg-products-block' ); } /** * Migrates an existing page using blocks to the block templates. * * @param string $template_content The content of the template. - * @param object $template_file The template file object. - * @param string $template_type The type of template. * @return string */ - public function default_template_content( $template_content, $template_file, $template_type ) { - if ( self::SLUG !== $template_file->slug ) { - return $template_content; - } - - $page_id = wc_get_page_id( 'cart' ); - $page = $page_id ? get_post( $page_id ) : false; + public function get_default_template_content( $template_content ) { + $page = $this->get_legacy_page(); if ( $page && ! empty( $page->post_content ) ) { $template_content = ' diff --git a/src/Templates/CheckoutTemplate.php b/src/Templates/CheckoutTemplate.php index 64d8fcdd356..316a7c3c389 100644 --- a/src/Templates/CheckoutTemplate.php +++ b/src/Templates/CheckoutTemplate.php @@ -1,200 +1,57 @@ init(); - } - - /** - * Initialization method. - */ - protected function init() { - // Register the endpoint in the rewrite rules. - add_action( 'init', array( $this, 'add_endpoint' ) ); - add_filter( 'woocommerce_is_checkout', array( $this, 'is_checkout' ) ); - add_filter( 'pre_get_document_title', array( $this, 'endpoint_page_title' ) ); - - // Handle settings for endpoints. - add_filter( 'woocommerce_settings_pages', array( $this, 'add_endpoint_field_to_settings_page' ) ); - add_action( 'after_switch_theme', 'flush_rewrite_rules' ); - add_action( 'update_option_woocommerce_checkout_pay_endpoint', 'flush_rewrite_rules' ); - - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); - add_filter( 'frontpage_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); - add_filter( 'woocommerce_blocks_template_content', array( $this, 'default_template_content' ), 10, 3 ); - add_action( 'current_screen', array( $this, 'template_editor_redirect' ) ); - } - +class CheckoutTemplate extends AbstractPageTemplate { /** - * Get the default endpoint for the template. This defaults to checkout unless a page already exists. + * Template slug. * * @return string */ - protected function get_default_endpoint() { - $default = __( 'checkout', 'woo-gutenberg-products-block' ); - - if ( wc_get_page_id( 'checkout' ) ) { - $page_data = get_post( wc_get_page_id( 'checkout' ) ); - return $page_data->post_name ?: $default; - } - - return $default; - } - - /** - * URL to edit the template. - * - * @return string - */ - protected function get_edit_template_url() { - return admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2Fcheckout' ); - } - - /** - * Add a query var for the checkout. This will allow to to detect when the user is viewing the checkout. - */ - public function add_endpoint() { - $endpoint = get_option( 'woocommerce_checkout_page_endpoint', $this->get_default_endpoint() ); - $query_vars = WC()->query->get_query_vars(); - - add_rewrite_endpoint( $endpoint . '/' . $query_vars['order-received'], \EP_ROOT, $query_vars['order-received'] ); - add_rewrite_endpoint( $endpoint . '/' . $query_vars['order-pay'], \EP_ROOT, $query_vars['order-pay'] ); - add_rewrite_endpoint( $endpoint, \EP_ROOT, 'checkout' ); + public static function get_slug() { + return 'checkout'; } /** - * Returns true when the endpoint is showing. + * Returns the page object assigned to this template/page used for legacy purposes. Pages are no longer required. * - * @return boolean + * @return \WP_Post|null Post object or null. */ - protected function is_endpoint() { - global $wp; - return isset( $wp->query_vars['checkout'] ); + public static function get_legacy_page() { + $page_id = wc_get_page_id( 'checkout' ); + return $page_id ? get_post( $page_id ) : null; } /** - * Filters the `is_checkout` function so we can return true when the endpoint is active. + * True when viewing the cart page or cart endpoint. * - * @param boolean $return True when on the checkout page. * @return boolean */ - public function is_checkout( $return ) { - if ( $this->is_endpoint() ) { - return true; - } - return $return; + public function is_active_template() { + return is_checkout(); } /** - * Filter the page title when the endpoint is active. + * Should return the title of the page. * - * @param string $title Page title. * @return string */ - public function endpoint_page_title( $title ) { - if ( $this->is_endpoint() ) { - return __( 'Checkout', 'woo-gutenberg-products-block' ); - } - return $title; - } - - /** - * Update Woo Settings page to include the checkout endpoint instead of the checkout page dropdown. - * - * @param array $settings Settings pages. - * @return array - */ - public function add_endpoint_field_to_settings_page( $settings ) { - $default_endpoint = $this->get_default_endpoint(); - - foreach ( $settings as $key => $setting ) { - if ( 'woocommerce_checkout_page_id' === $setting['id'] ) { - $settings[ $key ] = [ - 'title' => __( 'Checkout page', 'woo-gutenberg-products-block' ), - 'desc' => sprintf( - // translators: %1$s: opening anchor tag, %2$s: closing anchor tag. - __( 'The checkout page template can be %1$s edited here%2$s.', 'woo-gutenberg-products-block' ), - '', - '' - ), - 'desc_tip' => __( 'This is the URL to the checkout page.', 'woo-gutenberg-products-block' ), - 'id' => 'woocommerce_checkout_page_endpoint', - 'type' => 'permalink', - 'default' => $default_endpoint, - 'autoload' => false, - ]; - - add_action( 'woocommerce_admin_field_permalink', array( SettingsUtils::class, 'permalink_input_field' ) ); - } - } - return $settings; - } - - /** - * When the page is displaying the checkout and a block theme is active, render the Checkout Template. - * - * This places the template name e.g. `checkout`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. - * - * @param array $templates Templates that match the pages_template_hierarchy. - */ - public function update_page_template_hierarchy( $templates ) { - if ( is_checkout() ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; - } - - /** - * Redirect a page to the template editor if it's the checkout page and a block theme is active. - * - * @param \WP_Screen $current_screen Current screen information. - */ - public function template_editor_redirect( \WP_Screen $current_screen ) { - $page_id = wc_get_page_id( 'checkout' ); - $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( $edit_page_id === $page_id ) { - wp_safe_redirect( $this->get_edit_template_url() ); - exit; - } + protected function get_template_title() { + return __( 'Checkout', 'woo-gutenberg-products-block' ); } /** * Migrates an existing page using blocks to the block templates. * * @param string $template_content The content of the template. - * @param object $template_file The template file object. - * @param string $template_type The type of template. * @return string */ - public function default_template_content( $template_content, $template_file, $template_type ) { - if ( self::SLUG !== $template_file->slug ) { - return $template_content; - } - - $page_id = wc_get_page_id( 'checkout' ); - $page = $page_id ? get_post( $page_id ) : false; + public function get_default_template_content( $template_content ) { + $page = $this->get_legacy_page(); if ( $page && ! empty( $page->post_content ) ) { $template_content = ' diff --git a/src/Templates/OrderReceivedTemplate.php b/src/Templates/OrderReceivedTemplate.php index e6707f06b3a..d2845b85010 100644 --- a/src/Templates/OrderReceivedTemplate.php +++ b/src/Templates/OrderReceivedTemplate.php @@ -6,43 +6,40 @@ * * @internal */ -class OrderReceivedTemplate { - - const SLUG = 'order-received'; - +class OrderReceivedTemplate extends AbstractPageTemplate { /** - * Constructor. + * Template slug. * - * Templates require FSE theme support, so this will only init if a FSE theme is active. + * @return string */ - public function __construct() { - // Templates require FSE theme support. - if ( ! wc_current_theme_is_fse_theme() ) { - return; - } - $this->init(); + public static function get_slug() { + return 'order-received'; } /** - * Initialization method. + * Returns the page object assigned to this template/page used for legacy purposes. Pages are no longer required. + * + * @return \WP_Post|null Post object or null. */ - protected function init() { - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); + public static function get_legacy_page() { + return null; } /** - * When it's the Order Received page and a block theme is active, render the Order Received Template. + * True when viewing the cart page or cart endpoint. * - * This places the template name e.g. `order-received`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. + * @return boolean + */ + protected function is_active_template() { + return is_wc_endpoint_url( 'order-received' ); + } + + /** + * Should return the title of the page. * - * @param array $templates Templates that match the pages_template_hierarchy. + * @return string */ - public function update_page_template_hierarchy( $templates ) { - if ( is_wc_endpoint_url( 'order-received' ) ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; + protected function get_template_title() { + return __( 'Order Received', 'woo-gutenberg-products-block' ); } } diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php index b1ab42daf73..0c7aa8f0348 100644 --- a/src/Utils/BlockTemplateUtils.php +++ b/src/Utils/BlockTemplateUtils.php @@ -350,15 +350,15 @@ public static function get_plugin_block_template_types() { 'title' => _x( 'Mini Cart', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Mini Cart drawer.', 'woo-gutenberg-products-block' ), ), - CartTemplate::SLUG => array( + CartTemplate::get_slug() => array( 'title' => _x( 'Cart', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Cart.', 'woo-gutenberg-products-block' ), ), - CheckoutTemplate::SLUG => array( + CheckoutTemplate::get_slug() => array( 'title' => _x( 'Checkout', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Checkout.', 'woo-gutenberg-products-block' ), ), - OrderReceivedTemplate::SLUG => array( + OrderReceivedTemplate::get_slug() => array( 'title' => _x( 'Order Received', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Displays the order confirmation page.', 'woo-gutenberg-products-block' ), ),