Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails

WCEmailTemplateChangeSummary::summarizepublic staticWC 10.9.0

Produce a structured + localized summary of differences between the merchant's woo_email post and the current canonical core render.

Returned payload shape (documented for callers; typed as array<string, mixed> so internal helpers can return through it without expanding every union into the signature):

All deltas are framed as "what would happen if the merchant applied the update," i.e. the "yours" → "core" direction:

  • version_fromstring_wc_email_template_version meta on the post (may be empty).
  • version_tostring — registry-side current version.
  • source_hash_tostring — sha1 of the canonical core content for this email type. Mirrors the post's _wc_email_template_source_hash meta. Empty string in fallback / no-config paths where the core content can't be computed.
  • added_blocksarray<int, array{name:string, label:string, path:array<int|string>}> — blocks that would be added to the post by applying (in core, not in post). name is the post-alias-normalized block name (e.g. core/heading); label is its humanized form for display; path is the core-side index path.
  • removed_blocksarray<int, array{name:string, label:string, path:array<int|string>}> — blocks that would be removed from the post by applying (in post, not in core). Same field semantics as added_blocks; path is the post-side index path.
  • copy_changesarray<int, array{block:string, before:string, after:string, occurrence:int, total:int, path:array<int|string>, auto_resolvable?:bool}>. before is the merchant's current text; after is the canonical core text. path is the post-side index path. auto_resolvable is emitted only on the three-way path: true when only core changed since base (safe to auto-apply), false when both sides changed (true conflict). Absent on two-way fallback payloads.
  • structural_changesarray<int, array{kind:string, description:string, path?:array<int|string>}>path is omitted for kind: 'reorder' entries.
  • summary_linesstring[] — pre-localized one-liners ready to render.
  • is_fallbackbool — true when the diff could not be produced.
  • cache_hitbool — diagnostic.

Method of the class: WCEmailTemplateChangeSummary{}

No Hooks.

Returns

Array. mixed>

Usage

$result = WCEmailTemplateChangeSummary::summarize( $post_id ): array;
$post_id(int) (required)
The woo_email post ID.

Changelog

Since 10.9.0 Introduced.

WCEmailTemplateChangeSummary::summarize() code WC 10.9.1

public static function summarize( int $post_id ): array {
	$post = get_post( $post_id );
	if ( ! $post instanceof \WP_Post ) {
		return self::fallback_payload( '', '' );
	}

	$posts_manager = WCTransactionalEmailPostsManager::get_instance();
	$email_id      = $posts_manager->get_email_type_from_post_id( $post_id );
	if ( ! is_string( $email_id ) || '' === $email_id ) {
		return self::fallback_payload( '', '' );
	}

	$sync_config = WCEmailTemplateSyncRegistry::get_email_sync_config( $email_id );
	if ( null === $sync_config ) {
		return self::fallback_payload( '', '' );
	}

	$emails = $posts_manager->get_emails_by_id();
	$email  = $emails[ $email_id ] ?? null;
	if ( ! $email instanceof \WC_Email ) {
		return self::fallback_payload( '', (string) $sync_config['version'] );
	}

	$post_content = (string) $post->post_content;

	try {
		$core_content = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
	} catch ( \Throwable $e ) {
		self::get_logger()->error(
			sprintf(
				'Email template change summary failed to compute canonical content for email "%s": %s',
				$email_id,
				$e->getMessage()
			),
			array(
				'email_id' => $email_id,
				'post_id'  => $post_id,
				'context'  => 'email_template_change_summary',
			)
		);
		return self::fallback_payload(
			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, true ),
			(string) $sync_config['version']
		);
	}

	$version_from = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, true );
	$version_to   = (string) $sync_config['version'];

	$post_hash = sha1( $post_content );
	$core_hash = sha1( $core_content );

	$base_render = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_CORE_RENDER_META_KEY, true );
	$base_hash   = '' !== $base_render ? sha1( $base_render ) : '';

	$cache_key = self::cache_key( $post_id, $post_hash, $core_hash, $base_hash, self::current_locale() );
	$cached    = get_transient( $cache_key );
	if ( is_array( $cached ) ) {
		$cached['cache_hit'] = true;
		return $cached;
	}

	// In-sync zero-result: post and core hash to the same content. Successful
	// diff with no deltas. `is_fallback` stays false (the docblock contract:
	// fallback = "diff could not be produced," not "diff produced no
	// changes"). Empty `summary_lines` lets consumers detect the no-op state
	// by emptiness alone — they construct any "you're up to date" copy
	// themselves.
	if ( $post_hash === $core_hash ) {
		$payload                   = self::empty_payload();
		$payload['version_from']   = $version_from;
		$payload['version_to']     = $version_to;
		$payload['source_hash_to'] = $core_hash;
		self::write_cache( $cache_key, $payload );
		return $payload;
	}

	$post_records = self::flatten_blocks( parse_blocks( $post_content ) );
	$core_records = self::flatten_blocks( parse_blocks( $core_content ) );

	if ( empty( $post_records ) || empty( $core_records ) ) {
		return self::fallback_payload( $version_from, $version_to );
	}

	// Branch on base meta availability. When the post has been touched by a
	// sync-eligible writer (generator, auto-applier, selective applier, reset,
	// backfill), it has `last_core_render` and we run the three-way diff: a
	// strict per-block (yours-vs-base, core-vs-base) comparison. The
	// inversion-guard heuristic is not needed in this branch — three-way is
	// deterministic on any post. Posts without the meta fall through to the
	// legacy two-way path, which keeps the inversion guard for safety.
	if ( '' !== $base_render ) {
		$base_records = self::flatten_blocks( parse_blocks( $base_render ) );
		$structured   = self::diff_records_three_way( $core_records, $base_records, $post_records );
	} else {
		$structured = self::diff_records( $core_records, $post_records );

		// Summary-inversion guard: a heavily one-sided expansion on the post
		// side looks like merchant work attributed to core. Without a stored
		// old-core render to disambiguate, fall back to the release-notes line.
		// Under the "yours → core" convention, post-side unmatched blocks land
		// in `removed_blocks` (would be removed by applying), so that's the
		// signal we count here.
		$post_total = count( $post_records );
		$core_total = count( $core_records );
		if (
			0 === count( $structured['added_blocks'] )
			&& 0 === count( $structured['copy_changes'] )
			&& count( $structured['removed_blocks'] ) >= self::INVERSION_GUARD_THRESHOLD
			&& $post_total >= ( self::INVERSION_GUARD_RATIO * $core_total )
		) {
			$payload = self::fallback_payload( $version_from, $version_to );
			self::write_cache( $cache_key, $payload );
			return $payload;
		}
	}

	$summary_lines = self::to_summary_lines( $structured );

	$payload = array(
		'version_from'       => $version_from,
		'version_to'         => $version_to,
		'source_hash_to'     => $core_hash,
		'added_blocks'       => $structured['added_blocks'],
		'removed_blocks'     => $structured['removed_blocks'],
		'copy_changes'       => $structured['copy_changes'],
		'structural_changes' => $structured['structural_changes'],
		'summary_lines'      => $summary_lines,
		'is_fallback'        => false,
		'cache_hit'          => false,
	);

	self::write_cache( $cache_key, $payload );

	return $payload;
}