* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXF\Resource\Selector; use JsonSerializable; use KTXF\Json\JsonDeserializable; /** * Abstract base class for all selector types * Provides common functionality for hierarchical selectors */ abstract class SelectorAbstract extends \ArrayObject implements JsonSerializable, JsonDeserializable { protected const TYPE_STRING = 'string'; protected const TYPE_INT = 'int'; protected const TYPE_BOOL = 'bool'; /** @var array Allowed key types: 'string', 'int' */ protected array $keyTypes = []; /** @var array Allowed scalar value types: 'string', 'int', 'bool' */ protected array $valueTypes = []; /** @var class-string selector class for nested structures */ protected string $nestedSelector = SelectorAbstract::class; /** @var string Human-readable name for this selector type */ protected string $selectorName = 'Selector'; /** * Serialize to JSON-compatible array * * @return array */ public function jsonSerialize(): array { $result = []; foreach ($this as $key => $value) { if ($value instanceof JsonSerializable) { $result[$key] = $value->jsonSerialize(); } else { $result[$key] = $value; } } return $result; } /** * Deserialize from JSON-compatible array * @param array $data * @return void * @throws \InvalidArgumentException */ public function jsonDeserialize(array|string $data): static { if (is_string($data)) { $data = json_decode($data, true); } foreach ($data as $key => $value) { if ($this->nestedSelector !== null && is_array($value)) { $selector = new $this->nestedSelector(); $selector->jsonDeserialize($value); $this->offsetSet($key, $selector); } else { $this->offsetSet($key, $value); } } return $this; } /** * Validate if a key is of the correct type for this selector * * @param mixed $key * @return bool */ protected function validateKey(mixed $key): bool { return in_array(gettype($key), $this->keyTypes, true); } /** * Validate if a value is of the correct type for this selector * * @param mixed $value * @return bool */ protected function validateValue(mixed $value): bool { if ($this->nestedSelector !== null && $value instanceof $this->nestedSelector) { return true; } return in_array(gettype($value), $this->valueTypes, true); } /** * Override offsetSet to enforce type checking * * @param mixed $key * @param mixed $value * @return void * @throws \InvalidArgumentException */ #[\Override] public function offsetSet($key, $value): void { if (!$this->validateKey($key)) { throw new \InvalidArgumentException("{$this->selectorName} keys must be one of [" . implode(', ', $this->keyTypes) . "], got " . gettype($key)); } if (!$this->validateValue($value)) { throw new \InvalidArgumentException("{$this->selectorName} values must be one of [" . implode(', ', $this->valueTypes) . "], got " . gettype($value)); } parent::offsetSet($key, $value); } /** * Get all identifiers (keys or values depending on selector type) * * @return array */ public function identifiers(): array { return array_keys($this->getArrayCopy()); } }