Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active October 25, 2018 04:10
Show Gist options
  • Save westonruter/9e9510c7f57dfbd37eb8c0162b971aa2 to your computer and use it in GitHub Desktop.
Save westonruter/9e9510c7f57dfbd37eb8c0162b971aa2 to your computer and use it in GitHub Desktop.
<?php
/**
* Class Customize_Postmeta_Setting.
*
* @package WPSE_257322
*/
namespace WPSE_257322;
/**
* Class Customize_Postmeta_Setting
*
* This is adapted from `WP_Customize_Postmeta_Setting` in the Customize Posts plugin.
*
* @link https://github.com/xwp/wp-customize-posts/blob/0.8.5/php/class-wp-customize-postmeta-setting.php
*/
class Customize_Single_Postmeta_Setting extends \WP_Customize_Setting {
const ID_PATTERN = '#^wpse_257322_postmeta\[(?P<post_id>\d+)]\[(?P<meta_key>.+?)]$#';
const TYPE = 'wpse_257322_postmeta';
/**
* Type of setting.
*
* @access public
* @var string
*/
public $type = self::TYPE;
/**
* Post type.
*
* @access public
* @var string
*/
public $post_type;
/**
* Post ID.
*
* @access public
* @var string
*/
public $post_id;
/**
* Meta key.
*
* @access public
* @var string
*/
public $meta_key;
/**
* Parse setting ID.
*
* @param string $setting_id Setting ID.
* @return array|bool Parsed setting ID or false if parse error.
*/
static function parse_setting_id( $setting_id ) {
if ( ! preg_match( self::ID_PATTERN, $setting_id, $matches ) ) {
return false;
}
$matches['post_id'] = intval( $matches['post_id'] );
if ( $matches['post_id'] <= 0 ) {
return false;
}
return wp_array_slice_assoc( $matches, array( 'post_id', 'meta_key' ) );
}
/**
* Create setting ID.
*
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @return string Setting ID.
*/
static function create_setting_id( $post_id, $meta_key ) {
return sprintf( '%s[%d][%s]', self::TYPE, $post_id, $meta_key );
}
/**
* WP_Customize_Post_Setting constructor.
*
* @access public
*
* @param \WP_Customize_Manager $manager Manager.
* @param string $id Setting ID.
* @param array $args Setting args.
* @throws \Exception If the ID is in an invalid format.
* @global array $wp_meta_keys
*/
public function __construct( \WP_Customize_Manager $manager, $id, $args = array() ) {
global $wp_meta_keys;
$parsed_setting_id = self::parse_setting_id( $id );
if ( ! $parsed_setting_id ) {
throw new \Exception( 'Illegal setting ID format.' );
}
$args = array_merge( $args, $parsed_setting_id );
$post = get_post( $args['post_id'] );
if ( ! $post ) {
throw new \Exception( 'Unknown post' );
}
$post_type_obj = get_post_type_object( $post->post_type );
if ( ! $post_type_obj ) {
throw new \Exception( 'Unrecognized post type: ' . $post->post_type );
}
if ( ! registered_meta_key_exists( 'post', $args['meta_key'] ) ) {
throw new \Exception( 'Unregistered meta key: ' . $args['meta_key'] );
}
$registered_meta = $wp_meta_keys['post'][ $args['meta_key'] ];
if ( empty( $registered_meta['single'] ) ) {
throw new \Exception( 'Only single postmeta are currently supported.' );
}
if ( empty( $args['capability'] ) ) {
// See \WPSE_257322\filter_map_meta_cap().
$args['capability'] = sprintf( 'edit_post_meta[%d][%s]', $args['post_id'], $args['meta_key'] );
}
parent::__construct( $manager, $id, $args );
}
/**
* Return a post's setting value.
*
* @return mixed Meta value.
*/
public function value() {
$single = false; // For the sake of disambiguating empty values in filtering.
$values = get_post_meta( $this->post_id, $this->meta_key, $single );
$value = array_shift( $values );
if ( ! isset( $value ) ) {
$value = $this->default;
}
return $value;
}
/**
* Sanitize (and validate) an input.
*
* Note for non-single postmeta, the validation and sanitization callbacks will be applied on each item in the array.
*
* @see update_metadata()
* @access public
*
* @param string $value The value to sanitize.
* @return mixed|\WP_Error|null Sanitized post array or WP_Error if invalid (or null if not WP 4.6-alpha).
*/
public function sanitize( $value ) {
$has_setting_validation = method_exists( 'WP_Customize_Setting', 'validate' );
$meta_type = 'post';
$object_id = $this->post_id;
$meta_key = $this->meta_key;
$prev_value = ''; // Updating plural meta is not supported.
/**
* Filter a Customize setting value in form.
*
* @param mixed $value Value of the setting.
* @param \WP_Customize_Setting $this WP_Customize_Setting instance.
*/
$value = apply_filters( "customize_sanitize_{$this->id}", $value, $this );
// Apply sanitization if value didn't fail validation.
if ( ! is_wp_error( $value ) && ! is_null( $value ) ) {
$value = sanitize_meta( $meta_key, $value, $meta_type );
}
if ( is_wp_error( $value ) ) {
return $has_setting_validation ? $value : null;
}
/** This filter is documented in wp-includes/meta.php */
$check = apply_filters( "update_{$meta_type}_metadata", null, $object_id, $meta_key, $value, $prev_value );
if ( null !== $check ) {
/* translators: placeholder is meta key */
return $has_setting_validation ? new \WP_Error( 'not_allowed', sprintf( __( 'Update to post meta "%s" blocked.', 'wpse-257322' ), $meta_key ) ) : null;
}
return $value;
}
/**
* Add filter to preview customized value.
*
* @return bool
*/
public function preview() {
if ( ! $this->is_previewed ) {
add_filter( 'get_post_metadata', array( $this, 'filter_get_post_meta_to_preview' ), 1000, 4 );
$this->is_previewed = true;
}
return true;
}
/**
* Filter postmeta to inject customized post meta values.
*
* @param null|array|string $value The value get_metadata() should return - a single metadata value, or an array of values.
* @param int $object_id Object ID.
* @param string $meta_key Meta key.
* @param bool $single Whether to return only the first value of the specified $meta_key.
* @return mixed Value.
*/
public function filter_get_post_meta_to_preview( $value, $object_id, $meta_key, $single ) {
// Short-circuit if a plugin already modified the value.
if ( null !== $value ) {
return $value;
}
// Short circuit if object is not the post associated with this setting.
if ( intval( $object_id ) !== $this->post_id ) {
return $value;
}
// Abort if not filtering this key. Note that get_post_meta( $id ) to get all values is not implemented here.
if ( $this->meta_key !== $meta_key ) {
return $value;
}
// Abort if there is no customized value in the changeset.
$customized_value = $this->post_value( null );
if ( null === $customized_value ) {
return $value;
}
$value = $customized_value;
return $single ? $value : array( $value );
}
/**
* Update the post.
*
* Please note that the capability check will have already been done.
*
* @see \WP_Customize_Setting::save()
*
* @param string $meta_value The value to update.
* @return bool The result of saving the value.
*/
protected function update( $meta_value ) {
$result = update_post_meta( $this->post_id, $this->meta_key, $meta_value );
return ( false !== $result );
}
}
<?php
/**
* Plugin Name: Post/Page Content SHOUTING!
* Description: Demonstration of a PHP-centric approach to registering page/post-specific sections and controls in the customizer.
* Plugin URI: http://wordpress.stackexchange.com/questions/257322/customizer-loading-settings-controls-sections-panels-based-on-a-id-page-id
* Author: Weston Ruter, XWP
* Author URI: https://make.xwp.co/
* License: GPLv2+
*/
/*
* Copyright (c) 2017 XWP (https://make.xwp.co/)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2 or, at
* your discretion, any later version, as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
namespace WPSE_257322;
/**
* Register meta key.
*/
function register_meta_key() {
register_meta( 'post', 'should_shout', array(
'single' => true,
) );
}
add_action( 'init', __NAMESPACE__ . '\register_meta_key' );
/**
* Apply filters on the_content to SHOUT!
*
* @param string $content Content.
* @return string Content.
*/
function filter_post_content_to_shout( $content ) {
if ( get_post_meta( get_the_ID(), 'should_shout', true ) ) {
$content = strtoupper( $content );
}
return $content;
}
add_filter( 'the_content', __NAMESPACE__ . '\filter_post_content_to_shout' );
/**
* Autoload class.
*
* @param string $class Class to autoload.
*/
function load_classes( $class ) {
if ( __NAMESPACE__ . '\Customize_Single_Postmeta_Setting' === $class ) {
require_once dirname( __FILE__ ) . '/class-customize-single-postmeta-setting.php';
}
}
spl_autoload_register( __NAMESPACE__ . '\load_classes' );
/**
* Filters a dynamic setting's constructor args to ensure they are recognized when updating or publishing a changeset.
*
* @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
* @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
* @return false|array Setting args.
*/
function filter_dynamic_setting_args( $setting_args, $setting_id ) {
$parsed_setting_id = Customize_Single_Postmeta_Setting::parse_setting_id( $setting_id );
if ( false === $parsed_setting_id ) {
return $setting_args;
}
$post = get_post( $parsed_setting_id['post_id'] );
if ( ! $post || ! post_type_exists( $post->post_type ) ) {
return $setting_args;
}
if ( false === $setting_args ) {
$setting_args = array();
}
$setting_args = array_merge( $setting_args, array(
'type' => 'wpse_257322_postmeta', // See wpse_257322_filter_dynamic_setting_class().
'transport' => 'refresh',
'default' => false,
'sanitize_callback' => function( $value ) {
return (bool) $value;
},
) );
return $setting_args;
}
add_filter( 'customize_dynamic_setting_args', __NAMESPACE__ . '\filter_dynamic_setting_args', 10, 2 );
/**
* Filter dynamic setting class.
*
* @param string $setting_class WP_Customize_Setting or a subclass.
* @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
* @param array $setting_args WP_Customize_Setting or a subclass.
* @return string Setting class.
*/
function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
unset( $setting_id ); // Unused.
if ( isset( $setting_args['type'] ) && 'wpse_257322_postmeta' === $setting_args['type'] ) {
$setting_class = __NAMESPACE__ . '\Customize_Single_Postmeta_Setting';
}
return $setting_class;
}
add_filter( 'customize_dynamic_setting_class', __NAMESPACE__ . '\filter_dynamic_setting_class', 10, 3 );
/**
* Map dynamic postmeta capabilities to static capabilities.
*
* This is used so that a static capability string can be used in the setting but
* have it map dynamically at runtime.
*
* @param array $caps Returns the user's actual capabilities.
* @param string $cap Capability name.
* @param int $user_id The user ID.
* @return array Caps.
*/
function filter_map_meta_cap( $caps, $cap, $user_id ) {
if ( preg_match( '/^edit_post_meta\[\d+/', $cap ) ) {
$keys = explode( '[', str_replace( ']', '', $cap ) );
$map_meta_cap_args = array(
array_shift( $keys ),
$user_id,
intval( array_shift( $keys ) ),
array_shift( $keys ),
);
$caps = call_user_func_array( 'map_meta_cap', $map_meta_cap_args );
}
return $caps;
}
add_filter( 'map_meta_cap', __NAMESPACE__ . '\filter_map_meta_cap', 10, 3 );
/**
* Register section, control, and setting for given post.
*
* @param int $post_id Post/page ID.
* @global \WP_Customize_Manager $wp_customize
*/
function register_post_constructs( $post_id ) {
global $wp_customize;
$post = get_post( $post_id );
if ( empty( $post ) ) {
return;
}
$section = $wp_customize->add_section( "wpse-257322-post-meta-{$post->ID}", array(
/* translators: post title */
'title' => sprintf( __( 'Meta: %s', 'wpse-257322' ), $post->post_title ),
) );
// Used for both the setting and the control.
$customize_id = Customize_Single_Postmeta_Setting::create_setting_id( $post_id, 'should_shout' );
$dynamic_settings = $wp_customize->add_dynamic_settings( array( $customize_id ) ); // See wpse_257322_filter_dynamic_setting_args().
$wp_customize->add_control( $customize_id, array(
'label' => __( 'SHOUT TEXT?', 'wpse-257322' ),
'type' => 'checkbox',
'settings' => $customize_id,
'section' => $section->id,
) );
/**
* Dynamically-added settings.
*
* @var \WP_Customize_Setting[] $dynamic_settings
*/
foreach ( $dynamic_settings as $dynamic_setting ) {
$dynamic_setting->preview();
}
}
/**
* Register for controls.
*
* @global \WP_Customize_Manager $wp_customize
*/
function customize_register_for_controls() {
global $wp_customize;
$queried_object_id = url_to_postid( $wp_customize->get_preview_url() );
if ( $queried_object_id ) {
register_post_constructs( $queried_object_id );
}
}
add_action( 'customize_controls_init', __NAMESPACE__ . '\customize_register_for_controls', 9 ); // Priority 9 so before prepare_controls.
/**
* Register for preview.
*/
function customize_register_for_preview() {
if ( ! is_customize_preview() ) {
return;
}
$post_id = get_queried_object_id();
if ( $post_id ) {
register_post_constructs( $post_id );
}
}
add_action( 'wp', __NAMESPACE__ . '\customize_register_for_preview' );
@markkap
Copy link

markkap commented Feb 21, 2017

question, why can't you just use get_the_ID() instead of the url_to_postid in

} elseif ( isset( $_SERVER['REQUEST_URI'] ) ) {
	$queried_object_id = url_to_postid( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
}

not sure if there is a performance difference, but it will probably be more readable

@westonruter
Copy link
Author

@markkap Good question. The reason is that customize_register fires before wp and thus $wp_query hasn't been initialized yet.

@westonruter
Copy link
Author

@markkap Scratch that. I just refactored it to implement late-registration of the settings so that the queried object and the previewed URL are available.

@ParhamG
Copy link

ParhamG commented Oct 18, 2017

Hey @westonruter! This is great! I have a question though: how would you manage navigation to another post? Currently, the section disappears as soon as you navigate to another post and only shows up for the post that you initially opened the customizer for.

@mrtieutien90
Copy link

I have problem same @ParhamG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment