channel ID => values (see for_channel()); secrets are stored encrypted (SecretBox). * * @package Tidelist */ namespace Tidelist\Settings; use Tidelist\Channel\ChannelSettings; use Tidelist\Channel\SafetySwitches; use Tidelist\Security\SecretBox; /** * Plugin settings, stored in one non-autoloaded option. */ final class Settings implements SafetySwitches { const OPTION = 'tidelist_settings'; /** * Encryption. * * @var SecretBox */ private $box; /** * Functions that move older settings into the current shape: callable( array $stored ): array. They must be * safe to run again on settings that are already current. * * @var callable[] */ private $migrations; /** * Cached values. * * @var array|null */ private $cache = null; /** * Constructor. * * @param SecretBox $box Encryption. * @param callable[] $migrations Settings migrations (one per channel that changed its keys). */ public function __construct( SecretBox $box, array $migrations = array() ) { $this->box = $box; $this->migrations = $migrations; } /** * Default values. Dry-run is ON and pause is OFF by default. */ public static function defaults(): array { return array( 'dry_run' => true, 'paused' => false, 'image_base_url' => '', // Empty = this site's own address. 'delete_on_uninstall' => false, // Remove all data when the plugin is deleted. 'contact_name' => '', 'contact_phone' => '', 'title_template' => '', // Empty = ListingBuilder::DEFAULT_TITLE. 'description_template' => '', // Empty = ListingBuilder::DEFAULT_DESCRIPTION. 'shop_footer' => '', // Printed where {shop_footer} stands in the description. 'category_overrides' => array(), // Parent product ID => marketplace category ID. 'category_map' => array(), // Account ID => (WooCommerce category ID => marketplace category ID). 'attribute_map' => array(), // Account ID => (marketplace category ID => (attribute code => rule)). 'photo_exclusions' => array(), // Product ID => upload paths not to send. 'channels' => array(), // Channel ID => that channel's own settings. ); } /** * All settings with defaults applied. */ public function all(): array { if ( null === $this->cache ) { $this->cache = array_merge( self::defaults(), array_intersect_key( $this->migrated(), self::defaults() ) ); if ( '' === trim( (string) $this->cache['image_base_url'] ) && function_exists( 'home_url' ) ) { $this->cache['image_base_url'] = untrailingslashit( home_url() ); } } return $this->cache; } /** * One setting. * * @param string $key Key. * @return mixed */ public function get( string $key ) { $all = $this->all(); return $all[ $key ] ?? null; } /** * Saves changed values. Unknown keys are ignored. Values must already be sanitized. * * @param array $changes Key => value. */ public function update( array $changes ): void { $all = array_merge( $this->all(), array_intersect_key( $changes, self::defaults() ) ); update_option( self::OPTION, $all, false ); $this->cache = $all; } /** * Writes the stored settings in their current shape when they were stored in an older one (the migration * runs on every read; this makes it permanent). Does nothing when nothing changed. */ public function persist_migration(): void { $stored = get_option( self::OPTION, null ); if ( ! is_array( $stored ) ) { return; } $migrated = $this->migrated(); if ( $migrated !== $stored ) { update_option( self::OPTION, array_merge( self::defaults(), array_intersect_key( $migrated, self::defaults() ) ), false ); $this->cache = null; } } /** * Global pause switch. */ public function is_paused(): bool { return (bool) $this->get( 'paused' ); } /** * Dry-run switch. */ public function is_dry_run(): bool { return (bool) $this->get( 'dry_run' ); } /** * The settings of one channel. * * @param string $channel_id Channel ID. * @param array $defaults Default values of the channel's fields. */ public function for_channel( string $channel_id, array $defaults ): ChannelSettings { return new ScopedSettings( $this, $this->box, $channel_id, $defaults ); } /** * The stored option with every migration applied. */ private function migrated(): array { $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); foreach ( $this->migrations as $migration ) { $stored = (array) call_user_func( $migration, $stored ); } return $stored; } }