WordCamp Karlsruhe Logo

Den richtigen Hook finden

Code-Erweiterung ohne Plugin Änderungen

Von Justin Joe Kostka

Wer bin ich?

Wer bin ich?

  • Justin Joe Kostka (jujoko7CF)
  • https://jujoko7cf.com
  • 24 Jahre alt
  • WordPress-Entwickler
  • Seit 2019 bei der WP-Wartung24
  • 5. Teilnahme an einem WordCamp
  • 2. Speaker-Auftritt

Was sind Hooks?

Was sind Hooks?

  • Ermöglichen die Ergänzung/Modifizierung von Code ohne direkte Änderung der Komponenten ( Core, Theme oder Plugin Dateien )
  • Anpassungen können z.B. in einem eigenen Plugin oder einem Child-Theme implementiert werden

Actions

Ausführen von Code an bestimmten Stellen, z.B.:

  • Ausgabe von HTML (wp_head)
  • Dedektieren einer Aktion (save_post)
  • Ausführen von Funktionen zum richtigen Zeitpunkt (template_redirect / rest_api_init)

							function do_action( $hook_name, ...$arg ) {...}
						

							function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {...}
						

Beispiel

wp-includes/post.php

								do_action( 'save_post', $post_id, $post, $update );
							
z.B. in wp-content/themes/child-theme/functions.php

									add_action( 'save_post', 'send_mail_on_post_saving' );
									function send_mail_on_post_saving() {
										wp_mail(...);
									}
								
z.B. in wp-content/themes/child-theme/functions.php

									add_action( 'save_post', 'send_mail_on_post_update', 10, 3 );
									function send_mail_on_post_update( $post_id, $post, $update ) {
										if ( ! $update ) {
											return;
										}

										$title = get_the_title( $post_id );

										wp_mail(...);
									}
								
Wichtig: Endlosschleifen vermeiden!

Filter

Anpassung von Daten vor ihrer Verarbeitung,
um z.B.:

  • Funktionalitäten ein-/auszuschalten (show_admin_bar)
  • die Ausgabe zu verändern (the_content / excerpt_length)
  • das Verhalten zu modifizieren (upload_mimes / wp_mail_from)

							function apply_filters( $hook_name, $value, ...$args ) {...}
						

							function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {...}
						

Beispiel

wp-includes/functions.php

								return apply_filters( 'upload_mimes', $t, $user );
							
z.B. in wp-content/themes/child-theme/functions.php

									add_filter( 'upload_mimes', 'add_svg_to_upload_mimes' );
									function add_svg_to_upload_mimes( $mimes ) {
										$mimes['svg'] = 'image/svg+xml';

										return $mimes;
									}
								
z.B. in wp-content/themes/child-theme/functions.php

									add_filter( 'upload_mimes', 'add_svg_to_upload_mimes', 10, 2 );
									function add_svg_to_upload_mimes( $mimes, $user ) {
										if ( $user->has_cap( 'upload_svg' ) ) {
											$mimes['svg'] = 'image/svg+xml';
										}

										return $mimes;
									}
								
Wichtig: Rückgabe des veränderten Werts nicht vergessen!

Sonderfälle - Actions & Filter ...

  • ... mit Referenz
    • do_action_ref_array (z.B. pre_get_posts)
    • apply_filters_ref_array (z.B. posts_search)
  • ... mit dynamischen Hook-Namen, wie z.B.:
    • 
      										do_action( 'transition_post_status', $new_status, $old_status, $post );
      									
      
      										do_action( "{$old_status}_to_{$new_status}", $post );
      									
    • 
      										$args = apply_filters( 'register_post_type_args', $args, $this->name );
      									
      
      										$args = apply_filters( "register_{$post_type}_post_type_args", $args, $this->name );
      									

Wie finde ich den passenden Hook?

Einfach mal Googeln!

Oftmals ist man nicht der Einzige mit diesen oder ähnlichen Anforderungen ...

Dokumentation

WordPress Developer Resources oder die Entwickler-Dokumentationen des jeweiligen Plugins / Themes durchsuchen...

Query Monitor Plugin

Alle ausgeführten Actions & Filters anzeigen lassen ...

Admin Bar Admin Bar opened Query Monitor Query Monitor Hook Query Monitor Callback Query Monitor Component

Code nach Hooks durchsuchen

Den kompletten Code der jeweiligen Komponente, z.B. über die eigene IDE oder andere Tools, nach do_action oder apply_filters durchsuchen...

Die anzupassende Stelle finden

  • Code nach Klassen, IDs oder anderen HTML-Attributen durchsuchen
  • Passenden Template-Dateien (z.B. header.php, single.php, ...) ansehen
  • Nach Optionsnamen, Meta-Keys oder ähnlichem suchen

Ggf. findet sich direkt ein Hook an der gewünschten Stelle und man kann mit der Anpassung loslegen ...

Was tun, wenn dort kein Hook vorhanden ist?

Den Code rückwärts durchsuchen, um eventuell weiter oben Hooks zu finden ...

Tipps und Tricks

  1. Filter können wie Actions verwendet werden – Rückgabe nicht vergessen!
  2. Viele WordPress Core Funktionen enthalten Hooks (z.B. get_option / get_the_title)
  3. Werte in vorherigen Hooks sammeln und an spätere Filter weitergeben
  4. Unspezifische Hooks durch Nutzung anderer Hooks davor und danach einschränken

Beispiele

E-Mail-Versand nach Beitragsanpassung


add_action( 'post_updated', 'send_update_notification', 10, 3 );
/**
 * Send an email notification when a post is updated.
 *
 * @param int     $post_ID    Post ID.
 * @param WP_Post $post_after Post object after the update.
 * @param WP_Post $post_before Post object before the update.
 */
function send_update_notification(
	$post_ID,
	$post_after,
	$post_before
) {

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return;
	}

	// Compare old and new post data to find changes
	$fields_to_check = array(
		'post_title'            => 'Title',
		'post_content'          => 'Content',
		'post_excerpt'          => 'Excerpt',
		'post_status'           => 'Status',
		'post_author'           => 'Author',
		'post_date'             => 'Date',
		'post_modified'         => 'Modified Date',
		'post_type'             => 'Type',
		'post_parent'           => 'Parent',
		'menu_order'            => 'Menu Order',
		'comment_status'        => 'Comment Status',
		'ping_status'           => 'Ping Status',
		'post_password'         => 'Password',
		'post_name'             => 'Slug',
		'to_ping'               => 'To Ping',
		'pinged'                => 'Pinged',
		'post_content_filtered' => 'Filtered Content',
		'post_mime_type'        => 'MIME Type',
		'guid'                  => 'GUID'
	);

	$changes = array();
	foreach ( $fields_to_check as $field => $label ) {
		if ( $post_before->$field !== $post_after->$field ) {
			$changes[ $label ] = array(
				'before' => $post_before->$field,
				'after' => $post_after->$field
			);
		}
	}
	$changes = apply_filters(
		'custom/update_notification_changes',
		$changes,
		$post_ID,
		$post_after,
		$post_before,
		$fields_to_check
	);

	// If no changes, do nothing
	if ( empty( $changes ) ) {
		return;
	}

	// Prepare the email content
	$subject = apply_filters(
		'custom/update_notification_subject',
		'Post Updated: ' . $post_after->post_title,
		$post_ID,
		$post_after,
		$post_before
	);

	$message = "The following changes were made to the post:\n\n";
	foreach ( $changes as $field => $change ) {
		$message .= $field . ":\n";
		$message .= 'Before: ' . $change['before'] . "\n";
		$message .= 'After: ' . $change['after'] . "\n\n";
	}

	do_action(
		'custom/update_notification_before_send',
		$post_ID,
		$post_after,
		$post_before,
		$changes
	);

	// Send the email
	wp_mail( $email_address, $subject, $message );
}
						

Beispiele

Beitragstyp in E-Mail-Betreff hinzufügen


add_action( 'post_updated', 'send_update_notification', 10, 3 );
/**
 * Send an email notification when a post is updated.
 *
 * @param int     $post_ID    Post ID.
 * @param WP_Post $post_after Post object after the update.
 * @param WP_Post $post_before Post object before the update.
 */
function send_update_notification(
	$post_ID,
	$post_after,
	$post_before
) {

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return;
	}

	// Compare old and new post data to find changes
	$fields_to_check = array(
		'post_title'            => 'Title',
		'post_content'          => 'Content',
		'post_excerpt'          => 'Excerpt',
		'post_status'           => 'Status',
		'post_author'           => 'Author',
		'post_date'             => 'Date',
		'post_modified'         => 'Modified Date',
		'post_type'             => 'Type',
		'post_parent'           => 'Parent',
		'menu_order'            => 'Menu Order',
		'comment_status'        => 'Comment Status',
		'ping_status'           => 'Ping Status',
		'post_password'         => 'Password',
		'post_name'             => 'Slug',
		'to_ping'               => 'To Ping',
		'pinged'                => 'Pinged',
		'post_content_filtered' => 'Filtered Content',
		'post_mime_type'        => 'MIME Type',
		'guid'                  => 'GUID'
	);

	$changes = array();
	foreach ( $fields_to_check as $field => $label ) {
		if ( $post_before->$field !== $post_after->$field ) {
			$changes[ $label ] = array(
				'before' => $post_before->$field,
				'after' => $post_after->$field
			);
		}
	}
	$changes = apply_filters(
		'custom/update_notification_changes',
		$changes,
		$post_ID,
		$post_after,
		$post_before,
		$fields_to_check
	);

	// If no changes, do nothing
	if ( empty( $changes ) ) {
		return;
	}

	// Prepare the email content
	$subject = apply_filters(
		'custom/update_notification_subject',
		'Post Updated: ' . $post_after->post_title,
		$post_ID,
		$post_after,
		$post_before
	);

	$message = "The following changes were made to the post:\n\n";
	foreach ( $changes as $field => $change ) {
		$message .= $field . ":\n";
		$message .= 'Before: ' . $change['before'] . "\n";
		$message .= 'After: ' . $change['after'] . "\n\n";
	}

	do_action(
		'custom/update_notification_before_send',
		$post_ID,
		$post_after,
		$post_before,
		$changes
	);

	// Send the email
	wp_mail( $email_address, $subject, $message );
}
							

add_filter(
	'custom/update_notification_subject',
	'add_post_type_to_subject',
	10,
	4
);
function add_post_type_to_subject(
	$subject,
	$post_ID,
	$post_after,
	$post_before
) {
	$subject .= ' (' . $post_after->post_type . ')';
	return $subject;
}
							

Beispiele

E-Mail auch ohne Änderungen senden


add_action( 'post_updated', 'send_update_notification', 10, 3 );
/**
 * Send an email notification when a post is updated.
 *
 * @param int     $post_ID    Post ID.
 * @param WP_Post $post_after Post object after the update.
 * @param WP_Post $post_before Post object before the update.
 */
function send_update_notification(
	$post_ID,
	$post_after,
	$post_before
) {

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return;
	}

	// Compare old and new post data to find changes
	$fields_to_check = array(
		'post_title'            => 'Title',
		'post_content'          => 'Content',
		'post_excerpt'          => 'Excerpt',
		'post_status'           => 'Status',
		'post_author'           => 'Author',
		'post_date'             => 'Date',
		'post_modified'         => 'Modified Date',
		'post_type'             => 'Type',
		'post_parent'           => 'Parent',
		'menu_order'            => 'Menu Order',
		'comment_status'        => 'Comment Status',
		'ping_status'           => 'Ping Status',
		'post_password'         => 'Password',
		'post_name'             => 'Slug',
		'to_ping'               => 'To Ping',
		'pinged'                => 'Pinged',
		'post_content_filtered' => 'Filtered Content',
		'post_mime_type'        => 'MIME Type',
		'guid'                  => 'GUID'
	);

	$changes = array();
	foreach ( $fields_to_check as $field => $label ) {
		if ( $post_before->$field !== $post_after->$field ) {
			$changes[ $label ] = array(
				'before' => $post_before->$field,
				'after' => $post_after->$field
			);
		}
	}
	$changes = apply_filters(
		'custom/update_notification_changes',
		$changes,
		$post_ID,
		$post_after,
		$post_before,
		$fields_to_check
	);

	// If no changes, do nothing
	if ( empty( $changes ) ) {
		return;
	}

	// Prepare the email content
	$subject = apply_filters(
		'custom/update_notification_subject',
		'Post Updated: ' . $post_after->post_title,
		$post_ID,
		$post_after,
		$post_before
	);

	$message = "The following changes were made to the post:\n\n";
	foreach ( $changes as $field => $change ) {
		$message .= $field . ":\n";
		$message .= 'Before: ' . $change['before'] . "\n";
		$message .= 'After: ' . $change['after'] . "\n\n";
	}

	do_action(
		'custom/update_notification_before_send',
		$post_ID,
		$post_after,
		$post_before,
		$changes
	);

	// Send the email
	wp_mail( $email_address, $subject, $message );
}
						

add_filter(
	'custom/update_notification_changes',
	'send_mail_if_update_without_changes',
	PHP_INT_MAX,
	5
);
function send_mail_if_update_without_changes(
	$changes,
	$post_ID,
	$post_after,
	$post_before,
	$fields_to_check
) {
	if ( ! empty( $changes ) ) {
		return $changes;
	}

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return $changes;
	}

	wp_mail(
		$email_address,
		'No changes',
		"No changes were made to post \"{$post_before->post_title}\"."
	);

	return $changes;
}
							

Beispiele

Admin-E-Mail als Fallback-Empfänger


add_action( 'post_updated', 'send_update_notification', 10, 3 );
/**
 * Send an email notification when a post is updated.
 *
 * @param int     $post_ID    Post ID.
 * @param WP_Post $post_after Post object after the update.
 * @param WP_Post $post_before Post object before the update.
 */
function send_update_notification(
	$post_ID,
	$post_after,
	$post_before
) {

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return;
	}

	// Compare old and new post data to find changes
	$fields_to_check = array(
		'post_title'            => 'Title',
		'post_content'          => 'Content',
		'post_excerpt'          => 'Excerpt',
		'post_status'           => 'Status',
		'post_author'           => 'Author',
		'post_date'             => 'Date',
		'post_modified'         => 'Modified Date',
		'post_type'             => 'Type',
		'post_parent'           => 'Parent',
		'menu_order'            => 'Menu Order',
		'comment_status'        => 'Comment Status',
		'ping_status'           => 'Ping Status',
		'post_password'         => 'Password',
		'post_name'             => 'Slug',
		'to_ping'               => 'To Ping',
		'pinged'                => 'Pinged',
		'post_content_filtered' => 'Filtered Content',
		'post_mime_type'        => 'MIME Type',
		'guid'                  => 'GUID'
	);

	$changes = array();
	foreach ( $fields_to_check as $field => $label ) {
		if ( $post_before->$field !== $post_after->$field ) {
			$changes[ $label ] = array(
				'before' => $post_before->$field,
				'after' => $post_after->$field
			);
		}
	}
	$changes = apply_filters(
		'custom/update_notification_changes',
		$changes,
		$post_ID,
		$post_after,
		$post_before,
		$fields_to_check
	);

	// If no changes, do nothing
	if ( empty( $changes ) ) {
		return;
	}

	// Prepare the email content
	$subject = apply_filters(
		'custom/update_notification_subject',
		'Post Updated: ' . $post_after->post_title,
		$post_ID,
		$post_after,
		$post_before
	);

	$message = "The following changes were made to the post:\n\n";
	foreach ( $changes as $field => $change ) {
		$message .= $field . ":\n";
		$message .= 'Before: ' . $change['before'] . "\n";
		$message .= 'After: ' . $change['after'] . "\n\n";
	}

	do_action(
		'custom/update_notification_before_send',
		$post_ID,
		$post_after,
		$post_before,
		$changes
	);

	// Send the email
	wp_mail( $email_address, $subject, $message );
}
						

add_filter(
	'option_update_notification_email',
	'send_mail_default_to_admin'
);
function send_mail_default_to_admin( $value ) {
	return empty( $value ) ? get_option( 'admin_email' ) : $value;
}
							

Beispiele

Anzahl der Änderungen in der E-Mail anzeigen


add_action( 'post_updated', 'send_update_notification', 10, 3 );
/**
 * Send an email notification when a post is updated.
 *
 * @param int     $post_ID    Post ID.
 * @param WP_Post $post_after Post object after the update.
 * @param WP_Post $post_before Post object before the update.
 */
function send_update_notification(
	$post_ID,
	$post_after,
	$post_before
) {

	// Get the email address from the options
	$email_address = get_option( 'update_notification_email' );

	// If no email address is set, do nothing
	if ( empty( $email_address ) ) {
		return;
	}

	// Compare old and new post data to find changes
	$fields_to_check = array(
		'post_title'            => 'Title',
		'post_content'          => 'Content',
		'post_excerpt'          => 'Excerpt',
		'post_status'           => 'Status',
		'post_author'           => 'Author',
		'post_date'             => 'Date',
		'post_modified'         => 'Modified Date',
		'post_type'             => 'Type',
		'post_parent'           => 'Parent',
		'menu_order'            => 'Menu Order',
		'comment_status'        => 'Comment Status',
		'ping_status'           => 'Ping Status',
		'post_password'         => 'Password',
		'post_name'             => 'Slug',
		'to_ping'               => 'To Ping',
		'pinged'                => 'Pinged',
		'post_content_filtered' => 'Filtered Content',
		'post_mime_type'        => 'MIME Type',
		'guid'                  => 'GUID'
	);

	$changes = array();
	foreach ( $fields_to_check as $field => $label ) {
		if ( $post_before->$field !== $post_after->$field ) {
			$changes[ $label ] = array(
				'before' => $post_before->$field,
				'after' => $post_after->$field
			);
		}
	}
	$changes = apply_filters(
		'custom/update_notification_changes',
		$changes,
		$post_ID,
		$post_after,
		$post_before,
		$fields_to_check
	);

	// If no changes, do nothing
	if ( empty( $changes ) ) {
		return;
	}

	// Prepare the email content
	$subject = apply_filters(
		'custom/update_notification_subject',
		'Post Updated: ' . $post_after->post_title,
		$post_ID,
		$post_after,
		$post_before
	);

	$message = "The following changes were made to the post:\n\n";
	foreach ( $changes as $field => $change ) {
		$message .= $field . ":\n";
		$message .= 'Before: ' . $change['before'] . "\n";
		$message .= 'After: ' . $change['after'] . "\n\n";
	}

	do_action(
		'custom/update_notification_before_send',
		$post_ID,
		$post_after,
		$post_before,
		$changes
	);

	// Send the email
	wp_mail( $email_address, $subject, $message );
}
						

add_action(
	'custom/update_notification_before_send',
	array( 'Modify_Update_Notification_Message', 'save_changes' ),
	10,
	4
);
class Modify_Update_Notification_Message {
	static private $changes;

	static function save_changes(
		$post_ID,
		$post_after,
		$post_before,
		$changes
	) {
		static::$changes = $changes;
		add_filter(
			'wp_mail',
			array( __CLASS__, 'add_change_count_to_message' )
		);
	}

	static function add_change_count_to_message( $atts ) {
		remove_filter(
			'wp_mail',
			array( __CLASS__, 'add_change_count_to_message' )
		);

		$change_count = count( static::$changes );
		$atts['message'] .= "\n\nTotal changes: $change_count";

		static::$changes = null;

		return $atts;
	}
}
							

Fazit

Fazit

  1. Hooks ermöglichen saubere, flexible Anpassungen
  2. Verschiedene Wege, um den richtigen Hook zu finden
  3. Manche Hooks sind leicht zu entdecken, andere können sehr versteckt sein
  4. Wartungsfreundlicher als direkte Code-Anpassungen oder das Überschreiben von Template

Vielen Dank fürs Zuhören!