By that I mean that the handy and heavily-used get_post_meta() function, and the get_metadata() function for which the former is a wrapper, do not call apply_filters() on the data they return. Of all the places I've found lacking support for hooks, this one is perhaps the most infuriating.
WordPress will let you replace a call for a given piece of metadata, but it has no support for letting you modify that piece as it comes out of the database as far as I've found. If you follow the source for get_post_meta(), you end up in get_metadata_raw(). About midway down, past a giant comment, you'll see this odd bit:
$check = apply_filters("get_{$meta_type}_metadata", null, $object_id, $meta_key, $single, $meta_type );
Seeing as it's passing in null for the value, not something from the database, this is clearly not for filtering the target piece of metadata. It's instead for short-circuiting the rest of the code in the function. If you return a non-null value on that hook, that value will replace what you'd otherwise get should the function be left to continue on its way. I guess that might be useful at times, but it's certainly less useful than regular ol' filtering.
How do you filter metadata, then? Continuing down the source for get_metadata_raw() we see wp_cache_get and maybe_unserialize, but neither of those can be filtered either. Are we up a paddle without a creek? Yeah, pretty much!
One straightforward option is to wrap get_post_meta() yourself, calling your new function instead and running your own apply_filters() within, but this of course only works in your own code. Everything else will use the usual function.
function wp_post_meta($pid, $key, $single) {
return apply_filters('wp_post_meta', get_post_meta($pid, $key, $single));
}
To have the chance to filter all metadata calls, you've got to be stupid creative. You have to get in there early, maybe check for a certain post type, prevent infinite loops by removing and restoring your filter within itself, and then call your own hook. And is it even worth all that mess in the end? Probably not!
// Remember: `$data` is null by default; we make it… not.
function early_get_post_metadata($data, $pid, $key, $single) {
remove_filter('get_post_metadata', __FUNCTION__, 5);
$data = get_post_meta($pid, $key, $single);
add_filter('get_post_metadata', __FUNCTION__, 5);
return apply_filters('get_post_meta', $data, $pid, $key, $single);
}
if (! is_admin()) {
add_filter('get_post_metadata', 'early_get_post_metadata', 5, 4);
}
Since the default priority of hooks in WP is 10, we go in earlier with 5 to help ensure we can do our stuff before someone else gets involved. Whatever you use, make sure it matches in the relevant spots or you're gonna have an infinitely recursive bad time.
All this silly mess has finally allowed us to do what we wanted all along: Check a call to get_post_meta() for, for example, a specific key and mess with it.
add_filter('get_post_meta', function ($data, $pid, $key, $single) {
if ($key === 'whatever_key_youre_looking_for') {
// Do stuff to `$data` as needed.
}
return $data;
}, 10, 4);
Bit of a #sheesh situation going on here, but there's not much we can do about it. Yes, I'm aware that it does seem like WP doesn't much want us messing with metadata like this, and I'm sure it has its reasons, but sometimes you gotta do what you gotta do. And if it eventually goes awry, a good "See, what had happened was…" explanation usually works.