Skip to content

Instantly share code, notes, and snippets.

@hirasso
Last active July 1, 2025 07:34
Show Gist options
  • Save hirasso/839ed83ef187d312027843e95323b2fa to your computer and use it in GitHub Desktop.
Save hirasso/839ed83ef187d312027843e95323b2fa to your computer and use it in GitHub Desktop.
An abstractor for links in ACF WYSIWYG fields.
<?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