130 lines
3.4 KiB
PHP
130 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
|
* 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<mixed> Allowed key types: 'string', 'int' */
|
|
protected array $keyTypes = [];
|
|
|
|
/** @var array<mixed> 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());
|
|
}
|
|
|
|
}
|