List of tags that can be used to group settings.
*/
protected $tags = [];
public function __construct($id, StorageInterface $store = null, $params = []) {
parent::__construct($id, $store, $params);
if ( isset($params['isEditable']) && is_callable($params['isEditable']) ) {
$this->isEditableCallback = $params['isEditable'];
}
if ( isset($params['deleteWhenBlank']) ) {
$this->deleteWhenBlank = (bool)$params['deleteWhenBlank'];
}
if ( isset($params['supportsPostMessage']) ) {
$this->supportsPostMessage = (bool)$params['supportsPostMessage'];
}
if ( isset($params['tags']) ) {
$this->tags = $params['tags'];
}
}
/**
* Validate and sanitize a setting value.
*
* On success, this method returns the sanitized value. If there is
* a validation error, it returns a WP_Error instance instead.
*
* @param \WP_Error $errors
* @param array|mixed $value
* @param bool $stopOnFirstError Only applies to settings that have children. Other settings may ignore this parameter.
* @return \WP_Error|mixed
*/
abstract public function validate($errors, $value, $stopOnFirstError = false);
/**
* Update the value of this setting.
*
* This method assumes that the value has already been validated and sanitized,
* and that any applicable permissions have been checked.
*
* This may not immediately write the new value to the database. Call the save()
* method on the underlying storage to ensure that the value is saved.
*
* @param $validValue
* @return boolean
*/
abstract public function update($validValue);
/**
* @param mixed $customDefault
* @return mixed
*/
abstract public function getValue($customDefault = null);
/**
* @return mixed
*/
abstract public function getDefaultValue();
/**
* Enable preview mode for the current request. This will make the setting
* return the specified value instead of its actual value.
*
* Note: Usually, the preview value will be validated before it's passed
* to this method. However, in some cases, a value can be sent to the preview
* frame before it's saved in the changeset, so it will only have undergone
* JS-based validation, not full server-side validation. This means it's
* a good idea to validate the preview value even if that will sometimes
* duplicate work that has already been done.
*
* Also, even a value that has already been validated and saved in a changeset
* can occasionally become invalid later. For example, an image could be deleted
* from the media library. In exceptional cases, validation rules themselves
* could change as part of a plugin update.
*
* @param $unsafeValue
* @param \WP_Error|null $errors Optional. To avoid the creation of temporary
* WP_Error instances during value validation, you can provide an existing error
* object to this method.
*
* @return void
*/
public function preview($unsafeValue, $errors = null) {
if ( $errors === null ) {
$errors = new WP_Error();
}
$validationResult = $this->validate($errors, $unsafeValue, true);
if ( is_wp_error($validationResult) ) {
$previewValue = $this->getDefaultValue();
} else {
$previewValue = $validationResult;
}
if ( $this->store ) {
$this->store->setPreviewValue($previewValue);
}
}
/**
* Validate a value that has been submitted via an HTML form.
*
* For most settings, this is simply an alias for the `validate()` method.
* However, some settings may contain values that can't be directly represented
* in an HTML form, like `null` or objects. Values like that will need to be
* encoded/decoded when used in HTML. This method provides a way to decode
* and validate submitted form values.
*
* When the data comes from a form, you should use this method instead
* of `validate()` to ensure that the data is properly decoded.
*
* @param \WP_Error $errors
* @param array|mixed $value
* @param bool $stopOnFirstError
* @return \WP_Error|mixed
*/
public function validateFormValue($errors, $value, $stopOnFirstError = false) {
return $this->validate($errors, $value, $stopOnFirstError);
}
public function getDataType() {
return $this->dataType;
}
public function getRecommendedControls() {
return $this->recommendedControls;
}
/**
* Is the current user allowed to change this setting?
*
* @return bool
*/
public function isEditableByUser() {
if ( isset($this->isEditableCallback) ) {
return call_user_func($this->isEditableCallback);
}
return true;
}
/**
* Is it currently OK to delete this setting from storage?
*
* For example, some settings may choose to be removed when their value
* is empty, NULL, or equal to the default value.
*
* @return bool
*/
public function canBeDeleted() {
if ( $this->deleteWhenBlank ) {
$value = $this->getValue();
if ( ($value === null) || ($value === '') ) {
return true;
} else if ( is_array($value) && empty($value) ) {
return true;
}
}
return false;
}
/**
* Apply one or more validators to a setting value.
*
* Each validator should be a callable that takes two arguments:
* - The value to validate
* - A WP_Error object to which validation errors should be added.
*
* The callable should return the validated value, or a WP_Error instance
* if there was a validation error.
*
* @param callable[] $validators
* @param mixed $value
* @param \WP_Error $errors
* @param boolean $stopOnFirstError
* @return mixed|\WP_Error
*/
protected static function applyValidators($validators, $value, $errors, $stopOnFirstError = false) {
$convertedValue = $value;
$hasErrors = false;
foreach ($validators as $validator) {
$result = call_user_func($validator, $convertedValue, $errors);
if ( is_wp_error($result) ) {
$hasErrors = true;
if ( $stopOnFirstError ) {
return $result;
}
} else {
$convertedValue = $result;
}
}
return $hasErrors ? $errors : $convertedValue;
}
/**
* Notify the setting that its value has been updated.
*
* Usually, the setting itself will call this method from its update() method.
* If you change the value without calling the update() method, such as by
* directly updating a child of a struct, you should call this method manually.
*
* @return void
*/
public function notifyUpdated() {
if ( $this->store && $this->canBeDeleted() ) {
$this->store->deleteValue();
}
//Disallow recursive notifications as it could lead to infinite loops.
//If necessary, we can change this later.
if ( !$this->isNotifyingSubscribers ) {
$this->isNotifyingSubscribers = true;
//We'll notify subscribers now, so remove the setting from the notification
//queue. This can be redundant if this method gets called while processing
//the queue, but it's probably not a big deal.
if ( static::$updateNotificationQueue ) {
static::$updateNotificationQueue->remove($this);
}
foreach ($this->updateSubscribers as $callback) {
call_user_func($callback, $this);
}
$this->isNotifyingSubscribers = false;
}
}
/**
* @param callable $callback
* @return void
*/
public function subscribe($callback) {
$this->updateSubscribers[] = $callback;
}
/**
* @return bool
*/
public function supportsPostMessage() {
return $this->supportsPostMessage;
}
public function enablePostMessageSupport() {
$this->supportsPostMessage = true;
return $this;
}
//region Tags
/**
* @return string[]
*/
public function getTags() {
return $this->tags;
}
/**
* @param string[] $tags
* @return $this
*/
public function addTags(...$tags) {
$this->tags = array_unique(array_merge($this->tags, $tags));
return $this;
}
/**
* @param string $tag
* @return bool
*/
public function hasTag($tag) {
return in_array($tag, $this->tags);
}
//endregion
/**
* @return array|null
*/
public function serializeValidationRules() {
return null; //It's up to subclasses to implement this.
}
protected static function getNotificationQueue() {
if ( !static::$updateNotificationQueue ) {
static::$updateNotificationQueue = new UniqueSettingQueue();
}
return static::$updateNotificationQueue;
}
public static function sendPendingNotifications() {
if ( !static::$updateNotificationQueue ) {
return;
}
$queue = static::$updateNotificationQueue;
$queue->processAll();
}
/**
* @param AbstractSetting[] $settings
* @return void
*/
public static function saveAll($settings) {
static::sendPendingNotifications();
//Find and deduplicate the stores that contain these settings.
$stores = new \SplObjectStorage();
foreach ($settings as $setting) {
if ( $setting->store ) {
$stores->attach($setting->store->getSmallestSavable());
}
}
//Tell each store to save its data.
foreach ($stores as $store) {
/** @var StorageInterface $store */
$store->save();
}
}
/**
* @param AbstractSetting[] $settingsToWatch
* @param callable $callback Expected signature: function($updatedSettings) => void
*/
public static function subscribeDeferred($settingsToWatch, $callback) {
if ( empty($settingsToWatch) ) {
return;
}
$queue = static::getNotificationQueue();
new DeferredUpdateSubscriber($queue, $settingsToWatch, $callback);
//The subscriber object doesn't need to be stored anywhere because it will
//automatically add itself as a subscriber to each setting.
}
/**
* Recursively iterate over a collection of settings.
*
* This method will not recurse into composite settings, but it will return
* the children of regular structs.
*
* @param iterable $settings
* @param bool $leavesOnly When encountering a struct setting, only return its
* children and not the struct itself. Does not apply
* to composite settings.
* @param string|int|null $parentKey The key of the parent setting. Used to generate
* an iterator key for each setting.
* @return \Generator
*/
public static function recursivelyIterateSettings($settings, $leavesOnly = false, $parentKey = null) {
foreach ($settings as $key => $setting) {
if ( $parentKey !== null ) {
$effectiveKey = ((string)$parentKey) . '.' . $key;
} else {
$effectiveKey = $key;
}
//Descend into structs and arrays, except composite settings.
//WP 4.9.6+ includes a polyfill for is_iterable().
$isContainer = is_iterable($setting) && !($setting instanceof CompositeSetting);
if ( ($setting instanceof AbstractSetting) && (!$isContainer || !$leavesOnly) ) {
yield $effectiveKey => $setting;
}
if ( $isContainer ) {
/** @var iterable $setting */
yield from self::recursivelyIterateSettings($setting, $leavesOnly, $effectiveKey);
}
}
}
/**
* Recursively serialize a collection of settings for use in JavaScript.
*
* Optionally, you can provide a callback that will be called for each setting.
* It can be used to modify the serialized data. The callback will be called
* with two arguments:
* - The serialized data as an associative array.
* - The setting object.
*
* The callback should return an associative array. Alternatively, it can return
* `null` to exclude the setting from the result.
*
* @param AbstractSetting[] $settings
* @param int|null $flags
* @param callable $customizer Optional. A callback that can be used to modify each
* setting's serialized data.
* @return array A map of setting IDs to serialized settings.
*/
public static function serializeSettingsForJs(
$settings,
$flags = self::SERIALIZE_INCLUDE_ALL,
$customizer = null
) {
//Right now, the serialization process is fairly straightforward, so we
//just do it here. If it becomes more complex, we could add a serializeForJs()
//method to individual settings, or add a separate serializer class.
if ( $flags === null ) {
$flags = self::SERIALIZE_INCLUDE_ALL;
}
$leavesOnly = (bool)($flags & self::SERIALIZE_LEAVES_ONLY);
$serialized = [];
foreach (self::recursivelyIterateSettings($settings, $leavesOnly) as $setting) {
$emptyArraysAsObjects = ($setting->getDataType() === 'map');
$data = [];
if ( $flags & self::SERIALIZE_INCLUDE_ID ) {
$data['id'] = $setting->id;
}
if ( $flags & self::SERIALIZE_INCLUDE_DEFAULT ) {
$data['default'] = $setting->getDefaultValue();
if ( $emptyArraysAsObjects && is_array($data['default']) && empty($data['default']) ) {
$data['default'] = (object)$data['default'];
}
}
if ( $flags & self::SERIALIZE_INCLUDE_VALUE ) {
//Note: This will use the previewed value if one is available.
$data['value'] = $setting->getValue();
if ( $emptyArraysAsObjects && is_array($data['value']) && empty($data['value']) ) {
$data['value'] = (object)$data['value'];
}
}
if ( $flags & self::SERIALIZE_INCLUDE_GROUP_TITLE ) {
$groupTitle = $setting->getCustomGroupTitle();
if ( ($groupTitle !== null) && ($groupTitle !== '') ) {
$data['groupTitle'] = $groupTitle;
}
}
if ( $flags & self::SERIALIZE_INCLUDE_POST_MESSAGE_SUPPORT ) {
if ( $setting->supportsPostMessage() ) {
$data['supportsPostMessage'] = true;
}
}
if ( $flags & self::SERIALIZE_INCLUDE_TAGS ) {
$tags = $setting->getTags();
if ( !empty($tags) ) {
$data['tags'] = $tags;
}
}
if ( $flags & self::SERIALIZE_INCLUDE_VALIDATION ) {
$validationRules = $setting->serializeValidationRules();
if ( !empty($validationRules) ) {
$data['validation'] = $validationRules;
}
}
if ( $customizer ) {
$data = call_user_func($customizer, $data, $setting);
//Skip settings excluded by the callback.
if ( $data === null ) {
continue;
}
}
$serialized[$setting->id] = $data;
}
return $serialized;
}
}