bool` map so toggling a feature is a single, * race-free write. * * @package Bulkora */ namespace Bulkora; defined( 'ABSPATH' ) || exit; /** * Typed, cached access to plugin options. */ class Settings { const GROUP_GENERAL = 'general'; const GROUP_ADVANCED = 'advanced'; const GROUP_MODULES = 'modules'; /** * Option name for each group. * * @var array */ private static $option_keys = array( self::GROUP_GENERAL => 'bulkora_general', self::GROUP_ADVANCED => 'bulkora_advanced', self::GROUP_MODULES => 'bulkora_module_status', ); /** * Per-request cache of resolved group values. * * @var array */ private $cache = array(); /** * Default values for scalar setting groups. * * The module group intentionally defaults to an empty map: its defaults * are owned by the registry and resolved by {@see is_enabled()}. * * @param string|null $group Optional group to restrict the result to. * @return array */ public function defaults( $group = null ) { $defaults = array( self::GROUP_GENERAL => array( 'enabled' => true, 'placement' => 'replace', 'switcher' => true, 'max_rows' => 5, 'button_label' => __( 'Add All to Cart', 'bulkora-multiple-variation-order' ), 'add_row_label' => __( 'Add Another Variation', 'bulkora-multiple-variation-order' ), 'show_review_list' => true, 'show_subtotal' => true, ), self::GROUP_ADVANCED => array( 'delete_data_on_uninstall' => false, ), self::GROUP_MODULES => array(), ); /** * Filter the registered default settings. * * @param array $defaults Defaults keyed by group. */ $defaults = apply_filters( 'bulkora_settings_defaults', $defaults ); if ( null === $group ) { return $defaults; } return isset( $defaults[ $group ] ) ? $defaults[ $group ] : array(); } /** * Sanitisation schema per group (key => type). * * @param string $group Group name. * @return array */ private function schema( $group ) { switch ( $group ) { case self::GROUP_GENERAL: return array( 'enabled' => 'bool', 'placement' => 'select', 'switcher' => 'bool', 'max_rows' => 'int', 'button_label' => 'text', 'add_row_label' => 'text', 'show_review_list' => 'bool', 'show_subtotal' => 'bool', ); case self::GROUP_ADVANCED: return array( 'delete_data_on_uninstall' => 'bool', ); } return array(); } /** * Allowed values for a select field. * * @param string $group Group name. * @param string $key Setting key. * @return array */ private function select_options( $group, $key ) { if ( self::GROUP_GENERAL === $group && 'placement' === $key ) { return $this->placements(); } return array(); } /** * Placement choices, keyed by value. * * @return array */ public function placements() { return array( 'replace' => __( 'Replace the default variation form', 'bulkora-multiple-variation-order' ), 'below' => __( 'Show below the default variation form', 'bulkora-multiple-variation-order' ), ); } /** * Option name for a group. * * @param string $group Group name. * @return string */ public function option_key( $group ) { return isset( self::$option_keys[ $group ] ) ? self::$option_keys[ $group ] : ''; } /** * Resolve every value of a group, merged over its defaults. * * @param string $group Group name. * @return array */ public function all( $group ) { if ( isset( $this->cache[ $group ] ) ) { return $this->cache[ $group ]; } $key = $this->option_key( $group ); $stored = '' !== $key ? get_option( $key, array() ) : array(); $stored = is_array( $stored ) ? $stored : array(); $this->cache[ $group ] = array_merge( $this->defaults( $group ), $stored ); return $this->cache[ $group ]; } /** * Read a single key from a group. * * @param string $group Group name. * @param string $key Setting key. * @param mixed $default Fallback when the key is missing. * @return mixed */ public function get( $group, $key, $default = null ) { $data = $this->all( $group ); return array_key_exists( $key, $data ) ? $data[ $key ] : $default; } /** * Whether a module is enabled. * * Falls back to the module's own default when the user has never * explicitly toggled it, which keeps newly shipped modules working out of * the box. * * @param string $group {@see GROUP_MODULES}. * @param string $id Module identifier. * @param bool $default_enabled Registry-provided default. * @return bool */ public function is_enabled( $group, $id, $default_enabled = true ) { $data = $this->all( $group ); if ( array_key_exists( $id, $data ) ) { return (bool) $data[ $id ]; } return (bool) $default_enabled; } /** * Sanitise a raw group payload against the schema. * * Only keys present in the payload are returned, so a partially submitted * form never wipes unrelated values. * * @param string $group Group name. * @param array $values Raw values. * @return array */ public function sanitize_group( $group, array $values ) { $schema = $this->schema( $group ); $sanitized = array(); foreach ( $schema as $key => $type ) { if ( ! array_key_exists( $key, $values ) || ! is_scalar( $values[ $key ] ) ) { continue; } switch ( $type ) { case 'bool': $sanitized[ $key ] = rest_sanitize_boolean( $values[ $key ] ); break; case 'int': $sanitized[ $key ] = absint( $values[ $key ] ); break; case 'select': $clean = sanitize_key( (string) $values[ $key ] ); $allowed = $this->select_options( $group, $key ); if ( array_key_exists( $clean, $allowed ) ) { $sanitized[ $key ] = $clean; } break; case 'text': default: $sanitized[ $key ] = sanitize_text_field( (string) $values[ $key ] ); break; } } if ( array_key_exists( 'max_rows', $sanitized ) ) { $sanitized['max_rows'] = min( 20, max( 1, (int) $sanitized['max_rows'] ) ); } return $sanitized; } /** * Merge sanitised values into a scalar settings group. * * @param string $group Group name. * @param array $values Raw values. * @return bool */ public function update( $group, array $values ) { return $this->set_group( $group, $this->sanitize_group( $group, $values ) ); } /** * Persist an explicit status map for the module group. * * @param string $group Group name. * @param array $map Map of module id => bool. * @return bool */ public function set_status( $group, array $map ) { $clean = array(); foreach ( $map as $id => $enabled ) { $id = sanitize_key( $id ); if ( '' === $id ) { continue; } $clean[ $id ] = (bool) $enabled; } return $this->set_group( $group, $clean ); } /** * Replace a group's stored value and refresh the cache. * * @param string $group Group name. * @param array $value Value to store. * @return bool */ public function set_group( $group, array $value ) { $key = $this->option_key( $group ); if ( '' === $key ) { return false; } $this->cache[ $group ] = array_merge( $this->defaults( $group ), $value ); return update_option( $key, $value ); } /** * Restore a group to its defaults. * * @param string $group Group name. * @return bool */ public function reset( $group ) { return $this->set_group( $group, $this->defaults( $group ) ); } /** * Create any missing option rows using current defaults. * * @return void */ public function install_defaults() { foreach ( array_keys( self::$option_keys ) as $group ) { $key = $this->option_key( $group ); if ( '' !== $key && false === get_option( $key, false ) ) { add_option( $key, $this->defaults( $group ) ); } } } /** * Drop the in-memory cache (used after out-of-band writes). * * @return void */ public function flush_cache() { $this->cache = array(); } }