Veröffentlicht am Schreib einen Kommentar

Bestehenden Block mit neuen Attributen ausstatten

Durch eine veränderte Verarbeitung eines Blocks war es erforderlich, selbigen mit zusätzlichen Attributen auszustatten. Doch wie geht das? Leider habe ich dazu keinerlei Information gefunden. Ein manuelles Abspeichern aller Seiten, auf denen dieser Block eingesetzt wurde, war aufgrund der Menge keine Option. Daher musste eine andere Variante her.

Das Problem

Konkret ging es darum, bei einem eigens erstellten Kontaktformular-Block zusätzliche Informationen in einem separaten individuellen Inhaltstyp (CPT) zu speichern, um die Spam-Erkennung zu verbessern. Dazu musste ich aber jeden Kontaktformular-Block den dafür gespeicherten Inhalten zuweisen, also brauchte er eine Art ID. Da ich diese bisher noch nicht abgespeichert hatte, muss das nun nachträglich passieren.

Um diese ID zu jedem Block hinzuzufügen, muss ich diese beim Block speichern. Für neue Blöcke kein Problem, das erledigt React nach einer kleinen Erweiterung des Blocks von allein. Problematisch ist es aber bei allen zig hundert vorhandenen Blöcken. Diese alle neu abzuspeichern ist dabei kein wirklich sinnvoller Weg und die Fehleranfälligkeit, indem hier oder da noch etwas durchrutscht, viel zu hoch. Denn letztendlich brauche ich für meine Anpassung wirklich bei jedem Kontaktformular-Block diese ID.

Lösung: Block-Attribute bearbeiten

Da alle Attribute, die nicht direkt über den HTML-Quellcode des Blocks erkannt werden, als JSON zu Beginn des Block-Markups gespeichert werden, kann ich dieses bearbeiten. In meinem Fall sieht das in etwa so aus:

<!-- wp:my/contact-form {"formId":"853998cf-87f3-4c4c-830d-0edf0472a393","formType":"contact"} -->

Da WordPress diesen HTML-Code bereits selbst parsen kann, habe ich erst einmal nachgesehen, wie WordPress das innerhalb der Klasse WP_Block_Parser_Block selbst macht und daraus meine Abwandlung entwickelt. Letztendlich kam das dabei heraus:

$has_match = preg_match_all(
	'/<!--\s+wp:my\/contact-form\s+(?P<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?}\s+)?-->/s',
	$post->post_content,
	$matches,
	PREG_OFFSET_CAPTURE
);

Dabei werden sämtliche Attribute dieses Blocks in $matches['attributes'] gespeichert und können dann auf das Vorhandensein einer ID (in meinem Fall: formId) hin geprüft werden und falls es noch keine gibt, kann ich sie hinzufügen.

Migrations-Klasse

Das Ganze habe ich in eine eigene Klasse gepackt, die auch noch alle Ergebnisse (oder auch, falls es keine Ergebnisse gibt) protokolliert:

<?php
namespace my\Block_Contact_Form;
use WP_Post;
use function array_merge;
use function array_unique;
use function count;
use function get_current_blog_id;
use function get_posts;
use function home_url;
use function json_decode;
use function preg_match_all;
use function str_replace;
use function trim;
use function wp_generate_uuid4;
use function wp_json_encode;
use function wp_update_post;
use const PREG_OFFSET_CAPTURE;
use const SORT_REGULAR;

/**
 * Migration class to migrate old blocks.
 * 
 * @author	Matthias Kittsteiner
 * @license	GPL2 <https://www.gnu.org/licenses/gpl-2.0.html>
 */
class Migration {
	/**
	 * @var		\my\Block_Contact_Form\Migration
	 */
	private static $instance;
	
	/**
	 * @var		array Migration log
	 */
	private $log = [];
	
	/**
	 * Get a unique instance of the class.
	 * 
	 * @return	\my\Block_Contact_Form\Migration
	 */
	public static function get_instance(): Migration {
		if ( static::$instance === null ) {
			static::$instance = new static();
		}
		
		return static::$instance;
	}
	
	/**
	 * Migrate old contact forms to get a form ID and store data in a custom post.
	 */
	public function migrate(): void {
		$this->log[ get_current_blog_id() ] = [
			'Processing: ' . home_url(),
		];
		// get posts, pages and reusable blocks
		$posts = array_unique( array_merge(
			get_posts( [
				'numberposts' => -1,
				'post_type' => 'post',
			] ),
			get_posts( [
				'numberposts' => -1,
				'post_type' => 'page',
			] ),
			get_posts( [
				'numberposts' => -1,
				'post_type' => 'wp_block',
			] )
		), SORT_REGULAR );
		
		foreach ( $posts as $post ) {
			$has_set_form_ids = $this->set_form_ids( $post );
			
			if ( $has_set_form_ids ) {
				$this->set_form_data( $post );
			}
		}
		
		error_log( print_r( $this->log, true ), 3, __DIR__ . '/migrate.log' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
		// reset log after migration
		$this->log = [];
	}
	
	/**
	 * Set form data as custom post.
	 * 
	 * @param	\WP_Post	$post Current post object
	 */
	private function set_form_data( WP_Post $post ): void {
		$this->log[ get_current_blog_id() ][] = 'Saving form data for page ' . $post->post_title;
		
		Block_Contact_Form::get_instance()->set_form_data( $post->ID, $post );
	}
	
	/**
	 * Set form IDs for all forms in a post.
	 * 
	 * @param	\WP_Post	$post Current post object
	 * @return	bool Whether form IDs have been set for this post or not
	 */
	private function set_form_ids( WP_Post $post ): bool {
		// get all contact forms
		$has_match = preg_match_all(
			'/<!--\s+wp:my\/contact-form\s+(?P<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?}\s+)?-->/s',
			$post->post_content,
			$matches,
			PREG_OFFSET_CAPTURE
		);
		
		if ( ! $has_match ) {
			$this->log[ get_current_blog_id() ][] = '[NO MATCH] ' . $post->post_type . ' ' . $post->post_title . ' has no match.';
			return false;
		}
		
		if ( empty( $matches['attrs'] ) ) {
			$this->log[ get_current_blog_id() ][] = '[NO ATTRIBUTES] ' . $post->post_type . ' ' . $post->post_title . ' has no attributes.';
			return false;
		}
		
		$this->log[ get_current_blog_id() ][] = '[MATCHES] ' . $post->post_type . ' ' . $post->post_title . ' has ' . count( $matches['attrs'] ) . ' match(es).';
		
		foreach ( $matches['attrs'] as $attrs ) {
			if ( empty( $attrs[0] ) ) {
				continue;
			}
			
			// get attributes JSON as array
			$json_attrs = json_decode( trim( $attrs[0] ), true );
			
			// form ID already exists, ignore this form
			if ( ! empty( $json_attrs['formId'] ) ) {
				$this->log[ get_current_blog_id() ][] = '[HAS ID] Form ' . $json_attrs['formId'] . ' already has an ID.';
				continue;
			}
			
			// generate form ID
			$json_attrs['formId'] = wp_generate_uuid4();
			// replace old attributes with updated ones
			$post->post_content = str_replace( $attrs[0], wp_json_encode( $json_attrs ) . ' ', $post->post_content );
		}
		
		// update post with new post content
		return (bool) wp_update_post( $post );
	}
}

Aufgerufen werden kann die Klasse dann mit:
\my\Block_Contact_Form\Migration::get_instance()->migrate();

Das Protokoll befindet sich nach dem Durchlauf im selben Verzeichnis in der migrate.log und listet pro Site ein Array mit Ergebnissen auf. Das kann dann folgendermaßen aussehen:

Array
(
    [7012] => Array
        (
            [0] => Processing: https://example.com
            [1] => [MATCHES] page Contact has 1 match(es).
            [2] => [HAS ID] Form 739ba1ca-28e6-43a5-a393-c3b52d9b5515 already has an ID.
            [3] => Saving form data for page Kontakt
            [4] => [NO MATCH] page Imprint has no match.
            [5] => [NO MATCH] page Pivacy Policy has no match.
            [6] => [NO MATCH] page Home has no match.
			[7] => [MATCHES] wp_block Contact Form Standard has 1 match(es).
            [8] => Saving form data for page Contact Form Standard
        )

)
Array
(
    [7015] => Array
        (
            [0] => Processing: https://example.com/sub
            [1] => [NO MATCH] page Imprint has no match.
            [2] => [NO MATCH] page Privacy Policy has no match.
            [3] => [NO MATCH] page Home has no match.
        )

)

Disclaimer: Das Skript lief nur unter PHP 7.3 und PHP 7.4. Während ich PHP 8.0 als unproblematisch erachte, kann ich nicht sagen, wie es mit älteren PHP-Versionen aussieht.

Dieses Beispiel zeigt hier das Hinzufügen zusätzlicher Attribute. Da man jedoch bei den Ergebnissen die gesamte Liste an Attributen bekommt, ist es durchaus möglich, auch bestehende Attribute zu verändern oder zu entfernen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.