Last active
July 1, 2025 07:34
-
-
Save hirasso/839ed83ef187d312027843e95323b2fa to your computer and use it in GitHub Desktop.
An abstractor for links in ACF WYSIWYG fields.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* Copyright (c) Rasso Hilber | |
* https://rassohilber.com | |
* License: MIT | |
*/ | |
namespace Site\Base; | |
/** | |
* An abstractor for links in ACF WYSIWYG fields. | |
* Parses arbitrary URLs in your links and attempts to save them as post IDs. | |
* | |
* Helps preventing dead links when linked posts get deleted or changed. | |
* | |
* Call like this: ACFLinkAbstractor::init(); | |
*/ | |
class ACFLinkAbstractor | |
{ | |
public static function init(): void | |
{ | |
add_filter('acf/update_value/type=wysiwyg', [self::class, 'abstractLinks'], 1, 3); | |
add_filter('acf/load_value/type=wysiwyg', [self::class, 'deAbstractLinks'], 1, 3); | |
} | |
/** | |
* Abstract links (from permalink to {{link:ID}}) | |
*/ | |
public static function abstractLinks(mixed $value, int|string $post_id, array $field): mixed | |
{ | |
if (!is_string($value) || trim($value) === '') { | |
return $value; | |
} | |
$value = wp_unslash($value); | |
$replaced = preg_replace_callback( | |
'#<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#i', | |
function ($matches) { | |
$href = $matches[1]; | |
$text = $matches[2]; | |
$linkID = self::getLinkID($href); | |
if ($linkID) { | |
return sprintf('<a href="{{link:%s}}">%s</a>', $linkID, $text); | |
} | |
return $matches[0]; | |
}, | |
$value | |
); | |
return $replaced; | |
} | |
/** | |
* De-abstracts links (from {{link:ID}} to permalinks) | |
*/ | |
public static function deAbstractLinks(mixed $value, int|string $post_id, array $field): mixed | |
{ | |
if (!is_string($value) || trim($value) === '') { | |
return $value; | |
} | |
return preg_replace_callback( | |
'#<a\s+[^>]*href=["\']\{\{link:(\d+)\}\}["\'][^>]*>(.*?)</a>#i', | |
function ($matches) { | |
$raw = $matches[0]; | |
$postID = (int) $matches[1]; | |
$url = get_permalink($postID); | |
$text = $matches[2]; | |
if (!$url || get_post_status($postID) !== 'publish') { | |
return function_exists('get_current_screen') ? $raw : $text; | |
} | |
return sprintf('<a href="%s">%s</a>', esc_url($url), $text); | |
}, | |
$value | |
); | |
} | |
/** | |
* Attempt to get an ID from a URL. Currently only implemented for posts. | |
*/ | |
private static function getLinkID(string $url): ?string | |
{ | |
$query = self::resolveURL($url); | |
if (!$queriedObject = $query->get_queried_object()) { | |
return null; | |
} | |
return match($queriedObject::class) { | |
'WP_Post' => "$queriedObject->ID", | |
default => null | |
}; | |
} | |
/** | |
* Attempt to resolve a URL to a WP_Query. | |
* Works for custom post types, too. | |
* Like url_to_postid() on stereoids. | |
* Could even be extended to support custom post type archives | |
* or WP_Term links | |
* @see https://developer.wordpress.org/reference/functions/url_to_postid/ | |
*/ | |
private static function resolveURL(?string $url = null): \WP_Query | |
{ | |
/** | |
* @global \WP $wp | |
* @global \WP_Query $wp_the_query | |
*/ | |
global $wp, $wp_the_query; | |
$url ??= home_url($_SERVER['REQUEST_URI']); | |
$path = self::getPathRelativeToHomeURL($url); | |
remove_action('parse_request', 'rest_api_loaded'); | |
/** Augment $_SERVER as WP relies on it to parse query vars */ | |
$__SERVER = $_SERVER; | |
/** Write the path in REQUEST_URI */ | |
$_SERVER['REQUEST_URI'] = $path; | |
/** Trick WP into thinking it's on the frontend: */ | |
$_SERVER['PHP_SELF'] = 'index.php'; | |
/** | |
* Construct a new WP instance based on our augmented $_SERVER. | |
* We'll need the query_vars from this new instance later in our custom WP_Query. | |
*/ | |
$newWP = new \WP(); | |
$newWP->public_query_vars = $wp->public_query_vars; | |
$newWP->parse_request(); | |
$newWP->build_query_string(); | |
$queryVars = $newWP->query_vars; | |
/** Revert the augmented $_SERVER */ | |
$_SERVER = $__SERVER; | |
/** Store the global WP_Query so that we can revert it later */ | |
$_wp_the_query = $wp_the_query; | |
$query = new \WP_Query(); | |
/** | |
* Makes `$query->is_main_query()` return true for this query. | |
* That way, all relevant filters will be applied as expected. | |
*/ | |
$wp_the_query = $query; | |
/** Inject the query_vars from our new WP instance created above */ | |
$query->query($queryVars); | |
/** Revert the augmented global WP_Query */ | |
$wp_the_query = $_wp_the_query; | |
add_action('parse_request', 'rest_api_loaded'); | |
return $query; | |
} | |
/** | |
* Get the path relative to home | |
*/ | |
private static function getPathRelativeToHomeURL(string $url): string | |
{ | |
$homeURL = home_url(); | |
$homePath = parse_url($homeURL, PHP_URL_PATH) ?: ''; | |
$fullPath = wp_parse_url($url, PHP_URL_PATH) ?: ''; | |
return '/' . ltrim(substr($fullPath, strlen($homePath)), '/'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment