Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails
WCTransactionalEmailPostsGenerator{} │ WC 1.0
Class WCTransactionalEmailPostsGenerator
Handles the generation of WooCommerce transactional email templates. This class is responsible for initializing and managing default email templates, as well as generating new templates when required.
Hooks from the class
Usage
$WCTransactionalEmailPostsGenerator = new WCTransactionalEmailPostsGenerator(); // use class methods
Methods
- public __construct()
- public static build_filtered_post_data( string $email_type, $email )
- public static compute_canonical_post_content( $email )
- public generate_email_template_if_not_exists( $email_type )
- public generate_email_templates( $templates_to_generate )
- public generate_initial_email_templates()
- public get_email_template( $email )
- public init_default_transactional_emails()
- public initialize()
- public static render_block_template_html( $email )
- public static resolve_block_template_name( $email )
- public static resolve_block_template_path( $email )
- private generate_single_template( $email_type, $email_data )
Notes
- Package: Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails
WCTransactionalEmailPostsGenerator{} WCTransactionalEmailPostsGenerator{} code WC 10.8.1
class WCTransactionalEmailPostsGenerator {
/**
* WooCommerce Email Template Manager instance.
*
* @var WCTransactionalEmailPostsManager
*/
private $template_manager;
/**
* Default templates.
*
* @var array<string, \WC_Email>
*/
private $default_templates = array();
/**
* Transient name.
*
* @var string
*/
private $transient_name = 'wc_email_editor_initial_templates_generated';
/**
* Constructor.
*
* Initializes the WCTransactionalEmailPostsGenerator by setting up the template manager.
*/
public function __construct() {
$this->template_manager = WCTransactionalEmailPostsManager::get_instance();
}
/**
* Initialize the email template generator.
*
* This function initializes the email template generator by loading the default templates
* and generating initial email templates if needed.
*
* @internal
*/
public function initialize() {
if ( Constants::get_constant( 'WC_VERSION' ) === get_transient( $this->transient_name ) ) {
// if templates are already generated, we don't need to run this function again.
return true;
}
$this->init_default_transactional_emails();
$this->generate_initial_email_templates();
}
/**
* Initialize the default WooCommerce Transactional Emails.
*
* This function initializes the default templates for the core transactional emails.
* It fetches all the emails from WooCommerce and filters them to include only the core transactional emails.
*/
public function init_default_transactional_emails() {
if ( ! empty( $this->default_templates ) ) {
// If the default templates are already initialized, we don't need to run this function again.
return;
}
$core_transactional_emails = WCTransactionalEmails::get_transactional_emails();
$wc_emails = \WC_Emails::instance();
/**
* WooCommerce Transactional Emails instance.
*
* @var \WC_Email[]
*/
$email_types = $wc_emails->get_emails();
// Filter the emails to include only the core transactional emails.
$email_types = array_filter(
$email_types,
function ( $email ) use ( $core_transactional_emails ) {
return in_array( $email->id, $core_transactional_emails, true );
}
);
$this->default_templates = array_reduce(
$email_types,
function ( $acc, $email ) {
$acc[ $email->id ] = $email;
return $acc;
},
array()
);
}
/**
* Resolve the block template name for the given email.
*
* Returns `$email->template_block` when set, otherwise derives it from
* `$email->template_plain` by replacing the `plain` segment with `block`
* (e.g. `emails/plain/customer-invoice.php` becomes `emails/block/customer-invoice.php`).
*
* @param \WC_Email $email The email object.
* @return string The block template name, or an empty string if none can be resolved.
*
* @since 10.8.0
*/
public static function resolve_block_template_name( $email ): string {
if ( ! empty( $email->template_block ) ) {
return (string) $email->template_block;
}
$template_plain = (string) $email->template_plain;
if ( '' === $template_plain ) {
return '';
}
return str_replace( 'plain', 'block', $template_plain );
}
/**
* Resolve the absolute path of the block template for the given email.
*
* Uses {@see self::resolve_block_template_name()} for name resolution and then
* delegates to `wc_locate_template()` so theme overrides are honored.
*
* @param \WC_Email $email The email object.
* @return string The absolute template path, or an empty string if none can be resolved.
*
* @since 10.8.0
*/
public static function resolve_block_template_path( $email ): string {
$template_name = self::resolve_block_template_name( $email );
if ( '' === $template_name ) {
return '';
}
return (string) wc_locate_template(
$template_name,
'',
(string) $email->template_base
);
}
/**
* Get the email template for the given email.
*
* Looks for the initial email block content in plugins/woocommerce/templates/emails/block.
*
* @param \WC_Email $email The email object.
* @return string The email template.
*/
public function get_email_template( $email ) {
return self::render_block_template_html( $email );
}
/**
* Render the block template HTML for a given email.
*
* Resolves the block template (honouring theme overrides), falls back to the
* default block content on failure, and applies the
* `woocommerce_email_block_template_html` filter. Stateless so both the
* generator (via {@see self::get_email_template()}) and the divergence
* detector observe an identical rendering pipeline.
*
* @param \WC_Email $email The email object.
* @return string The rendered template HTML.
*
* @since 10.8.0
*/
public static function render_block_template_html( $email ): string {
$template_name = self::resolve_block_template_name( $email );
try {
$template_html = wc_get_template_html(
$template_name,
array(),
'',
(string) $email->template_base
);
} catch ( \Exception $e ) {
// wc_get_template_html() uses ob_start(), so we need to clean the output buffer if an exception is thrown.
if ( ob_get_level() > 0 ) {
ob_end_clean();
}
$template_html = '';
}
// wc_get_template_html does not throw an error when the template is not found.
// We need to check if the template is not found by checking the template_html content.
$has_template_error =
StringUtil::contains( $template_html, 'No such file or directory', false ) ||
StringUtil::contains( $template_html, 'Failed to open stream', false ) ||
StringUtil::contains( $template_html, 'Warning: include', false );
if ( is_wp_error( $template_html ) || empty( $template_html ) || $has_template_error ) {
$default_template_name = 'emails/block/default-block-content.php';
$template_html = wc_get_template_html(
$default_template_name,
array()
);
}
/**
* Filter the email template HTML.
*
* @param string $template_html The email template HTML.
* @param \WC_Email $email The email object.
* @since 10.7.0
*/
$filtered_template_html = apply_filters( 'woocommerce_email_block_template_html', $template_html, $email );
return is_string( $filtered_template_html ) ? $filtered_template_html : $template_html;
}
/**
* Generate initial email templates.
*
* This function generates the initial email templates for the core transactional emails.
* It checks if the templates are already generated and if not, it generates them.
*
* @return bool True if the templates are generated, false otherwise.
*/
public function generate_initial_email_templates() {
$core_transactional_emails = WCTransactionalEmails::get_transactional_emails();
$templates_to_generate = array();
foreach ( $core_transactional_emails as $email_type ) {
if ( empty( $this->template_manager->get_email_template_post_id( $email_type ) ) ) {
$templates_to_generate[] = $email_type;
}
}
if ( empty( $templates_to_generate ) ) {
return;
}
$result = $this->generate_email_templates( $templates_to_generate );
if ( is_wp_error( $result ) ) {
return false;
}
set_transient( $this->transient_name, Constants::get_constant( 'WC_VERSION' ), WEEK_IN_SECONDS );
// Flush rewrite rules to ensure the new templates are loaded.
flush_rewrite_rules();
return true;
}
/**
* Generate email template if it doesn't exist.
*
* This function generates an email template if it doesn't exist.
*
* @param string $email_type The email type.
* @return int The post ID of the generated template.
* @throws \Exception When post creation fails.
*/
public function generate_email_template_if_not_exists( $email_type ) {
$email_data = $this->default_templates[ $email_type ];
if ( $this->template_manager->get_email_template_post_id( $email_type ) || empty( $email_data ) ) {
return $this->template_manager->get_email_template_post_id( $email_type );
}
return $this->generate_single_template( $email_type, $email_data );
}
/**
* Generate email templates.
*
* This function generates the email templates for the given email types.
*
* @param array $templates_to_generate The email types to generate.
*/
public function generate_email_templates( $templates_to_generate ) {
global $wpdb;
$core_emails = array_filter(
$this->default_templates,
function ( $email_id ) use ( $templates_to_generate ) {
return in_array( $email_id, $templates_to_generate, true );
},
ARRAY_FILTER_USE_KEY
);
if ( empty( $core_emails ) ) {
return false;
}
// Start transaction.
$wpdb->query( 'START TRANSACTION' );
try {
foreach ( $core_emails as $email_type => $email_data ) {
$this->generate_single_template( $email_type, $email_data );
}
$wpdb->query( 'COMMIT' );
return true;
} catch ( \Exception $e ) {
$wpdb->query( 'ROLLBACK' );
return new \WP_Error( 'email_generation_failed', $e->getMessage() );
}
}
/**
* Build the `wp_insert_post()` payload for a given email and apply the
* `woocommerce_email_content_post_data` filter.
*
* Extracted so the generator and the divergence detector observe the exact
* same pre-insert post payload, guaranteeing by construction that the hash
* stamped in {@see self::generate_single_template()} and the hash recomputed
* in `WCEmailTemplateDivergenceDetector` hash identical input.
*
* @param string $email_type The email type identifier (e.g. `customer_processing_order`).
* @param \WC_Email $email The transactional email instance.
* @return array The post data array after the `woocommerce_email_content_post_data` filter runs.
*
* @since 10.8.0
*/
public static function build_filtered_post_data( string $email_type, $email ): array {
$post_data = array(
'post_type' => Integration::EMAIL_POST_TYPE,
'post_status' => 'publish',
'post_name' => $email_type,
'post_title' => $email->get_title(),
'post_excerpt' => $email->get_description(),
'post_content' => self::render_block_template_html( $email ),
'meta_input' => array(
'_wp_page_template' => ( new WooEmailTemplate() )->get_slug(),
),
);
/**
* Filter the email content post data before creating the post.
*
* Allows third-party integrators to modify the post data (title, content, meta, etc.)
* before the email content post is created.
*
* @since 10.5.0
* @param array $post_data The post data array to be used for wp_insert_post().
* @param string $email_type The email type identifier (e.g., 'customer_processing_order').
* @param \WC_Email $email The WooCommerce email object.
*/
$filtered_post_data = apply_filters( 'woocommerce_email_content_post_data', $post_data, $email_type, $email );
return is_array( $filtered_post_data ) ? $filtered_post_data : $post_data;
}
/**
* Compute the canonical `post_content` for a given email.
*
* Returns the `post_content` value that the generator would persist for this
* email after the `woocommerce_email_content_post_data` filter runs, i.e.
* the exact string whose sha1 is stamped into `_wc_email_template_source_hash`.
*
* Callers can hash the return value to obtain `currentCoreHash` for
* divergence detection.
*
* @param \WC_Email $email The transactional email instance.
* @return string The canonical post content.
*
* @since 10.8.0
*/
public static function compute_canonical_post_content( $email ): string {
$post_data = self::build_filtered_post_data( (string) $email->id, $email );
return (string) ( $post_data['post_content'] ?? '' );
}
/**
* Generate a single email template.
*
* This function generates a single email template post and sets its postmeta association.
*
* @param string $email_type The email type.
* @param \WC_Email $email_data The transactional email data.
* @return int The post ID of the generated template.
* @throws \Exception When post creation fails.
*/
private function generate_single_template( $email_type, $email_data ) {
$post_data = self::build_filtered_post_data( (string) $email_type, $email_data );
// Sync meta stamp for emails participating in template update propagation.
$sync_config = WCEmailTemplateSyncRegistry::get_email_sync_config( (string) $email_data->id );
if ( null !== $sync_config ) {
if ( ! isset( $post_data['meta_input'] ) || ! is_array( $post_data['meta_input'] ) ) {
$post_data['meta_input'] = array();
}
$post_data['meta_input'][ WCEmailTemplateDivergenceDetector::VERSION_META_KEY ] = (string) $sync_config['version'];
$post_data['meta_input'][ WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY ] = sha1( (string) ( $post_data['post_content'] ?? '' ) );
$post_data['meta_input'][ WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY ] = gmdate( 'Y-m-d H:i:s' );
$post_data['meta_input'][ WCEmailTemplateDivergenceDetector::LAST_CORE_RENDER_META_KEY ] = (string) ( $post_data['post_content'] ?? '' );
}
$post_id = wp_insert_post( $post_data, true );
if ( is_wp_error( $post_id ) ) {
throw new \Exception( esc_html( $post_id->get_error_message() ) );
}
$this->template_manager->save_email_template_post_id( $email_type, $post_id );
return $post_id;
}
}