*/
protected $settings = [];
/**
* @var callable
*/
protected $childUpdateCallback;
protected $childSubscriptionsAdded = false;
protected $isInUpdateLoop = false;
public function __construct($id, StorageInterface $store = null, $params = []) {
//Minor optimization: Create the callback array once and reuse it for every child.
$this->childUpdateCallback = [$this, 'notifyChildWasUpdated'];
if ( $store === null ) {
$store = new Storage\NullStorage();
}
parent::__construct($id, $store, $params);
}
/**
* @param \WP_Error $errors
* @param array $value
* @return \WP_Error|array
*/
public function validate($errors, $value, $stopOnFirstError = false) {
if ( !is_array($value) ) {
$errors->add('struct_value_invalid', 'Struct value must be an associative array');
return $errors;
}
$validatedValues = [];
$foundErrors = false;
foreach ($this->settings as $key => $setting) {
if ( array_key_exists($key, $value) ) {
$validity = $setting->validate($errors, $value[$key], $stopOnFirstError);
if ( is_wp_error($validity) ) {
$foundErrors = true;
if ( $stopOnFirstError ) {
break;
}
} else {
$validatedValues[$key] = $validity;
}
}
}
if ( $foundErrors ) {
return $errors;
} else {
return $validatedValues;
}
}
public function update($validValue) {
$this->isInUpdateLoop = true;
$isSuccess = true;
foreach ($this->settings as $key => $setting) {
if ( array_key_exists($key, $validValue) ) {
$isSuccess = $isSuccess && $setting->update($validValue[$key]);
}
}
$this->isInUpdateLoop = false;
$this->notifyUpdated();
return $isSuccess;
}
public function getValue($customDefault = []) {
$result = is_array($customDefault) ? $customDefault : [];
foreach ($this->settings as $key => $setting) {
$result[$key] = $setting->getValue();
}
return $result;
}
public function preview($unsafeValue, $errors = null) {
if ( !is_array($unsafeValue) ) {
return;
}
if ( $errors === null ) {
$errors = new \WP_Error();
}
foreach ($this->settings as $key => $setting) {
if ( array_key_exists($key, $unsafeValue) ) {
$setting->preview($unsafeValue[$key], $errors);
}
}
}
/**
* @return array
*/
public function getDefaultValue() {
return [];
}
public function canBeDeleted() {
if ( $this->deleteWhenBlank ) {
//In addition to other "blank" states, a struct can also be deleted
//if it has no children or if all of its children can be deleted.
$isDeletable = true;
foreach ($this->settings as $setting) {
if ( !$setting->canBeDeleted() ) {
$isDeletable = false;
break;
}
}
if ( $isDeletable ) {
return true;
}
}
return parent::canBeDeleted();
}
public function subscribe($callback) {
parent::subscribe($callback);
//Optimization: Listen for child updates only if we have subscribers.
if ( !empty($this->updateSubscribers) && !$this->childSubscriptionsAdded ) {
$this->childSubscriptionsAdded = true;
foreach ($this->settings as $setting) {
$setting->subscribe($this->childUpdateCallback);
}
}
}
/**
* @internal This method needs to be public because it's used as a callback,
* but you should not call it directly.
*
* @noinspection PhpUnusedParameterInspection In the current implementation,
* we don't care which specific child was updated, only that one was.
*
* @param AbstractSetting $childSetting
* @return void
*/
public function notifyChildWasUpdated(AbstractSetting $childSetting) {
//If we're inside the foreach loop in update(), we don't need to notify
//our subscribers here - update() will do it after the loop.
//Also, we currently don't support recursive notifications, so if a child
//is updated while we're sending notifications, we'll just ignore it.
if ( $this->isInUpdateLoop || $this->isNotifyingSubscribers ) {
return;
}
//Optimization: Multiple children can be updated at the same time, and
//it would be inefficient to notify the parent's subscribers every time.
//Instead, we'll put the parent in a queue. The code performing the update
//should send pending notifications when it's done.
static::getNotificationQueue()->enqueue($this);
}
/**
* @param string $key
* @return AbstractSetting|null
*/
public function getChild($key) {
if ( array_key_exists($key, $this->settings) ) {
return $this->settings[$key];
}
return null;
}
public function getChildValue($childSettingKey, $defaultValue = null) {
if ( array_key_exists($childSettingKey, $this->settings) ) {
return $this->settings[$childSettingKey]->getValue($defaultValue);
}
return $defaultValue;
}
public function makeChildId($childKey) {
return $this->id . '.' . $childKey;
}
/**
* Create a child setting of the specified type.
*
* @param string $childKey
* @param class-string $className
* @param ...$constructorParams
* @return AbstractSetting
*/
protected function createChild($childKey, $className, ...$constructorParams) {
$child = new $className(
$this->makeChildId($childKey),
$this->store->buildSlot($childKey),
...$constructorParams
);
if ( $this->shouldEnablePostMessageForChildren() ) {
$child->enablePostMessageSupport();
}
//Children inherit the parent's tags.
$child->addTags(...$this->tags);
$this->registerChild($childKey, $child);
return $child;
}
protected function registerChild($childKey, AbstractSetting $child) {
$this->settings[$childKey] = $child;
if ( $this->childSubscriptionsAdded ) {
$child->subscribe($this->childUpdateCallback);
}
}
public function enablePostMessageSupport() {
parent::enablePostMessageSupport();
if ( $this->shouldEnablePostMessageForChildren() ) {
foreach ($this->settings as $setting) {
$setting->enablePostMessageSupport();
}
}
}
protected function shouldEnablePostMessageForChildren() {
return $this->supportsPostMessage;
}
/** @noinspection PhpLanguageLevelInspection */
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return array_key_exists($offset, $this->settings);
}
/**
* @param string $offset
* @return AbstractSetting|null
* @noinspection PhpLanguageLevelInspection
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
return $this->getChild($offset);
}
/** @noinspection PhpLanguageLevelInspection */
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
throw new \LogicException(
'Cannot add or replace a child of a struct. The setting list is read-only.'
);
}
/** @noinspection PhpLanguageLevelInspection */
#[\ReturnTypeWillChange]
public function offsetUnset($offset) {
throw new \LogicException(
'Cannot remove a child of a struct. The setting list is read-only.'
);
}
/** @noinspection PhpLanguageLevelInspection */
#[\ReturnTypeWillChange]
public function getIterator() {
return new \ArrayIterator($this->settings);
}
}