From bcb736fed7e94464fad1294879fba8cc230b668a Mon Sep 17 00:00:00 2001 From: Maxim Harder Date: Mon, 21 Sep 2026 17:23:42 +0200 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=D1=83=D0=BF=D1=80=D0=BE=D1=89?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20ORM=20=D1=81=D1=85=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удаляет динамическую генерацию схемы через Compiler и заменяет её на статическую конфигурацию. Это упрощает инициализацию ORM и убирает зависимость от аннотаций. Теперь схема явно определена в коде, что облегчает отладку и тестирование. --- src/Persistence/CycleOrmFactory.php | 86 ++++++++++++++++---------- src/Persistence/EntityClassLocator.php | 38 ------------ 2 files changed, 55 insertions(+), 69 deletions(-) delete mode 100644 src/Persistence/EntityClassLocator.php diff --git a/src/Persistence/CycleOrmFactory.php b/src/Persistence/CycleOrmFactory.php index 39a17c0..cf4165a 100644 --- a/src/Persistence/CycleOrmFactory.php +++ b/src/Persistence/CycleOrmFactory.php @@ -4,11 +4,6 @@ namespace DevCraftClub\Shikimori\Persistence; -use Cycle\Annotated\Embeddings; -use Cycle\Annotated\Entities; -use Cycle\Annotated\MergeColumns; -use Cycle\Annotated\MergeIndexes; -use Cycle\Annotated\TableInheritance; use Cycle\Database\Config\DatabaseConfig as CycleDatabaseConfig; use Cycle\Database\Config\MySQL\TcpConnectionConfig as MysqlTcp; use Cycle\Database\Config\MySQLDriverConfig; @@ -21,15 +16,13 @@ use Cycle\ORM\EntityManager; use Cycle\ORM\EntityManagerInterface; use Cycle\ORM\Factory; +use Cycle\ORM\Mapper\Mapper; use Cycle\ORM\ORM; use Cycle\ORM\ORMInterface; use Cycle\ORM\Schema; -use Cycle\Schema\Compiler; -use Cycle\Schema\Generator\GenerateModifiers; -use Cycle\Schema\Generator\GenerateRelations; -use Cycle\Schema\Generator\RenderTables; -use Cycle\Schema\Generator\ValidateEntities; -use Cycle\Schema\Registry; +use Cycle\ORM\SchemaInterface; +use Cycle\ORM\Select\Repository; +use Cycle\ORM\Select\Source; use DevCraftClub\Shikimori\Config\SdkConfig; use DevCraftClub\Shikimori\Entity\AnimeEntity; use DevCraftClub\Shikimori\Exception\ConfigurationException; @@ -37,34 +30,65 @@ final class CycleOrmFactory { /** - * @param list $entityClasses - * * @return array{0: ORMInterface, 1: EntityManagerInterface} */ - public static function fromConfig(SdkConfig $config, array $entityClasses = [AnimeEntity::class]): array + public static function fromConfig(SdkConfig $config): array { $dbal = new DatabaseManager(self::buildDatabaseConfig($config)); - $locator = new EntityClassLocator($entityClasses); - $registry = new Registry($dbal); - - $generators = [ - new Embeddings($locator), - new Entities($locator), - new TableInheritance(), - new MergeColumns(), - new GenerateRelations(), - new GenerateModifiers(), - new ValidateEntities(), - new RenderTables(), - new MergeIndexes(), - ]; - - $schema = (new Compiler())->compile($registry, $generators); - $orm = new ORM(new Factory($dbal), new Schema($schema)); + $orm = new ORM(new Factory($dbal), new Schema(self::buildSchema())); return [$orm, new EntityManager($orm)]; } + /** + * @return array> + */ + private static function buildSchema(): array + { + $entity = AnimeEntity::class; + + return [ + $entity => [ + SchemaInterface::ENTITY => $entity, + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::DATABASE => 'default', + SchemaInterface::TABLE => 'shikimori_animes', + SchemaInterface::PRIMARY_KEY => ['id'], + SchemaInterface::FIND_BY_KEYS => ['id'], + SchemaInterface::COLUMNS => [ + 'id' => 'id', + 'name' => 'name', + 'russian' => 'russian', + 'kind' => 'kind', + 'status' => 'status', + 'score' => 'score', + 'episodes' => 'episodes', + 'episodesAired' => 'episodes_aired', + 'description' => 'description', + 'url' => 'url', + 'duration' => 'duration', + 'rating' => 'rating', + 'franchise' => 'franchise', + 'airedOn' => 'aired_on', + 'releasedOn' => 'released_on', + 'updatedAt' => 'updated_at', + 'poster' => 'poster', + 'genres' => 'genres', + 'studios' => 'studios', + 'fetchedAt' => 'fetched_at', + ], + SchemaInterface::RELATIONS => [], + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST => [], + SchemaInterface::SCHEMA => [], + SchemaInterface::TYPECAST_HANDLER => null, + SchemaInterface::GENERATED_FIELDS => ['id' => 2], + ], + ]; + } + private static function buildDatabaseConfig(SdkConfig $config): CycleDatabaseConfig { $driver = strtolower($config->getDbDriver()); diff --git a/src/Persistence/EntityClassLocator.php b/src/Persistence/EntityClassLocator.php deleted file mode 100644 index f4ac2c8..0000000 --- a/src/Persistence/EntityClassLocator.php +++ /dev/null @@ -1,38 +0,0 @@ - $classes - */ - public function __construct( - private readonly array $classes, - ) { - } - - /** - * @return array> - */ - public function getClasses(object|string|null $target = null): array - { - $result = []; - foreach ($this->classes as $class) { - if ($target !== null) { - $targetName = \is_string($target) ? $target : $target::class; - if (!\is_a($class, $targetName, true)) { - continue; - } - } - - $result[$class] = new \ReflectionClass($class); - } - - return $result; - } -} From c7f4546f71d80fd87f6ee4b42846d5998cedeeee Mon Sep 17 00:00:00 2001 From: Maxim Harder Date: Mon, 21 Sep 2026 17:33:40 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=D1=83=D0=BF=D1=80=D0=BE=D1=89?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2DTO=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D1=83=D1=82=D0=B8=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Извлекает повторяющуюся логику валидации и преобразования данных в отдельный класс ArrayUtil. Это делает код в fromArray методах чище и избегает дублирования. Также уточняет тип параметра в PHPDoc с string на array-key для корректности. --- composer.json | 2 +- phpstan.neon | 4 ++ src/Client/SlidingWindowRateLimiter.php | 12 ++-- src/Config/SdkConfig.php | 28 ++++++---- src/DTO/AnimeDTO.php | 74 +++++++++---------------- src/DTO/CharacterDTO.php | 2 +- src/DTO/ContestDTO.php | 2 +- src/DTO/GenreDTO.php | 2 +- src/DTO/MangaDTO.php | 8 +-- src/DTO/PersonDTO.php | 2 +- src/DTO/PosterDTO.php | 2 +- src/DTO/StudioDTO.php | 2 +- src/DTO/UserDTO.php | 2 +- src/DTO/UserRateDTO.php | 9 +-- src/Entity/AnimeEntity.php | 7 ++- src/Persistence/CycleEntityStore.php | 1 - src/Repository/UserRepository.php | 3 +- src/Util/ArrayUtil.php | 63 +++++++++++++++++---- 18 files changed, 127 insertions(+), 98 deletions(-) diff --git a/composer.json b/composer.json index 036ad16..b3213dc 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.40", - "phpstan/phpstan": "^1.10 || ^2.0", + "phpstan/phpstan": "^1.10 || ^2.2", "phpunit/phpunit": "^10.5", "symfony/var-dumper": "^6.4", "vlucas/phpdotenv": "^5.6" diff --git a/phpstan.neon b/phpstan.neon index 5be515f..8e683e4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,3 +4,7 @@ parameters: - src - tests ignoreErrors: + # Lombok Getter/Setter and DevTools With assign via magic methods. + - + identifier: property.unusedType + path: src/* diff --git a/src/Client/SlidingWindowRateLimiter.php b/src/Client/SlidingWindowRateLimiter.php index 580cf90..c23381c 100644 --- a/src/Client/SlidingWindowRateLimiter.php +++ b/src/Client/SlidingWindowRateLimiter.php @@ -56,13 +56,11 @@ public function acquire(): void $this->requests[] = $this->now(); - $lastTimestamp = end($this->requests); - if ($lastTimestamp !== false) { - $elapsed = $now - $lastTimestamp; - if ($elapsed < $minInterval && $elapsed > 0.0) { - $sleep = $minInterval - $elapsed; - usleep((int) ($sleep * 1_000_000)); - } + $lastTimestamp = $this->requests[array_key_last($this->requests)]; + $elapsed = $now - $lastTimestamp; + if ($elapsed < $minInterval && $elapsed > 0.0) { + $sleep = $minInterval - $elapsed; + usleep((int) ($sleep * 1_000_000)); } } diff --git a/src/Config/SdkConfig.php b/src/Config/SdkConfig.php index fcca4b0..324262e 100644 --- a/src/Config/SdkConfig.php +++ b/src/Config/SdkConfig.php @@ -193,43 +193,51 @@ private static function envNullableString(string $key, ?string $default): ?strin { $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); - if ($value === false || $value === '') { + if (!\is_string($value) || $value === '') { return $default; } - return (string) $value; + return $value; } private static function envInt(string $key, int $default): int { $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); - if ($value === false || $value === '') { - return $default; + if (\is_int($value)) { + return $value; + } + + if (\is_string($value) && $value !== '' && is_numeric($value)) { + return (int) $value; } - return (int) $value; + return $default; } private static function envNullableInt(string $key, ?int $default): ?int { $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); - if ($value === false || $value === '') { - return $default; + if (\is_int($value)) { + return $value; + } + + if (\is_string($value) && $value !== '' && is_numeric($value)) { + return (int) $value; } - return (int) $value; + return $default; } private static function envBool(string $key, bool $default): bool { $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); - if ($value === false || $value === '') { + if (!\is_string($value) || $value === '') { return $default; } - return \in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true); + return \in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } } diff --git a/src/DTO/AnimeDTO.php b/src/DTO/AnimeDTO.php index 8bd4397..c8cba56 100644 --- a/src/DTO/AnimeDTO.php +++ b/src/DTO/AnimeDTO.php @@ -8,6 +8,7 @@ use Devcraft\Abstracts\AbstractWith; use Devcraft\Attributes\With; use Devcraft\Attributes\WithItem; +use DevCraftClub\Shikimori\Util\ArrayUtil; use Lombok\Getter; use Lombok\Setter; @@ -119,43 +120,41 @@ public function __construct() private array $studios = []; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { $id = $data['id'] ?? 0; $anime = (new self()) ->withId(\is_int($id) || \is_string($id) ? $id : 0) - ->withName(\is_string($data['name'] ?? null) ? (string) $data['name'] : '') - ->withRussian(\is_string($data['russian'] ?? null) ? (string) $data['russian'] : null) - ->withKind(\is_string($data['kind'] ?? null) ? (string) $data['kind'] : null) - ->withStatus(\is_string($data['status'] ?? null) ? (string) $data['status'] : null) - ->withScore(self::optionalFloat($data['score'] ?? null)) - ->withEpisodes(self::optionalInt($data['episodes'] ?? null)) - ->withEpisodesAired(self::optionalInt($data['episodesAired'] ?? null)) - ->withDescription(\is_string($data['description'] ?? null) ? (string) $data['description'] : null) - ->withUrl(\is_string($data['url'] ?? null) ? (string) $data['url'] : null) - ->withDuration(self::optionalInt($data['duration'] ?? null)) - ->withRating(\is_string($data['rating'] ?? null) ? (string) $data['rating'] : null) - ->withFranchise(\is_string($data['franchise'] ?? null) ? (string) $data['franchise'] : null) - ->withAiredOn(\is_string($data['airedOn'] ?? null) ? (string) $data['airedOn'] : null) - ->withReleasedOn(\is_string($data['releasedOn'] ?? null) ? (string) $data['releasedOn'] : null) - ->withPoster(PosterDTO::fromArray(\is_array($data['poster'] ?? null) ? $data['poster'] : null)) - ->withUpdatedAt(self::parseDateTime($data['updatedAt'] ?? null)); - - $genres = $data['genres'] ?? null; - if (\is_array($genres)) { + ->withName(ArrayUtil::stringOrDefault($data, 'name', '')) + ->withRussian(ArrayUtil::optionalString($data, 'russian')) + ->withKind(ArrayUtil::optionalString($data, 'kind')) + ->withStatus(ArrayUtil::optionalString($data, 'status')) + ->withScore(ArrayUtil::optionalFloat($data, 'score')) + ->withEpisodes(ArrayUtil::optionalInt($data, 'episodes')) + ->withEpisodesAired(ArrayUtil::optionalInt($data, 'episodesAired')) + ->withDescription(ArrayUtil::optionalString($data, 'description')) + ->withUrl(ArrayUtil::optionalString($data, 'url')) + ->withDuration(ArrayUtil::optionalInt($data, 'duration')) + ->withRating(ArrayUtil::optionalString($data, 'rating')) + ->withFranchise(ArrayUtil::optionalString($data, 'franchise')) + ->withAiredOn(ArrayUtil::optionalString($data, 'airedOn')) + ->withReleasedOn(ArrayUtil::optionalString($data, 'releasedOn')) + ->withPoster(PosterDTO::fromArray(ArrayUtil::optionalArray($data, 'poster'))) + ->withUpdatedAt(ArrayUtil::optionalDateTime($data, 'updatedAt')); + + $genres = ArrayUtil::optionalListOfMaps($data, 'genres'); + if ($genres !== null) { foreach ($genres as $genre) { - if (\is_array($genre)) { - $anime = $anime->withGenresItem(GenreDTO::fromArray($genre)); - } + $anime = $anime->withGenresItem(GenreDTO::fromArray($genre)); } } - $studios = $data['studios'] ?? null; - if (\is_array($studios)) { + $studios = ArrayUtil::optionalListOfMaps($data, 'studios'); + if ($studios !== null) { foreach ($studios as $studio) { - $studioDto = StudioDTO::fromArray(\is_array($studio) ? $studio : null); + $studioDto = StudioDTO::fromArray($studio); if ($studioDto !== null) { $anime = $anime->withStudiosItem($studioDto); } @@ -198,27 +197,4 @@ public function toArray(): array ), ]; } - - private static function parseDateTime(mixed $value): ?DateTimeImmutable - { - if (!\is_string($value) || $value === '') { - return null; - } - - try { - return new DateTimeImmutable($value); - } catch (\Throwable) { - return null; - } - } - - private static function optionalInt(mixed $value): ?int - { - return \is_int($value) ? $value : null; - } - - private static function optionalFloat(mixed $value): ?float - { - return \is_int($value) || \is_float($value) ? (float) $value : null; - } } diff --git a/src/DTO/CharacterDTO.php b/src/DTO/CharacterDTO.php index 35fbfb5..6aa6970 100644 --- a/src/DTO/CharacterDTO.php +++ b/src/DTO/CharacterDTO.php @@ -67,7 +67,7 @@ public function __construct() private ?DateTimeImmutable $updatedAt = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { diff --git a/src/DTO/ContestDTO.php b/src/DTO/ContestDTO.php index d1fdc45..6468622 100644 --- a/src/DTO/ContestDTO.php +++ b/src/DTO/ContestDTO.php @@ -62,7 +62,7 @@ public function __construct() private ?DateTimeImmutable $updatedAt = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { diff --git a/src/DTO/GenreDTO.php b/src/DTO/GenreDTO.php index d5c46bd..2192f98 100644 --- a/src/DTO/GenreDTO.php +++ b/src/DTO/GenreDTO.php @@ -40,7 +40,7 @@ public function __construct() private ?string $kind = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { diff --git a/src/DTO/MangaDTO.php b/src/DTO/MangaDTO.php index 38ce381..49b527a 100644 --- a/src/DTO/MangaDTO.php +++ b/src/DTO/MangaDTO.php @@ -99,7 +99,7 @@ public function __construct() private array $genres = []; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { @@ -120,12 +120,10 @@ public static function fromArray(array $data): self ->withUpdatedAt(ArrayUtil::optionalDateTime($data, 'updatedAt')) ->withPoster(PosterDTO::fromArray(ArrayUtil::optionalArray($data, 'poster'))); - $genres = ArrayUtil::optionalArray($data, 'genres'); + $genres = ArrayUtil::optionalListOfMaps($data, 'genres'); if ($genres !== null) { foreach ($genres as $genre) { - if (\is_array($genre)) { - $manga = $manga->withGenresItem(GenreDTO::fromArray($genre)); - } + $manga = $manga->withGenresItem(GenreDTO::fromArray($genre)); } } diff --git a/src/DTO/PersonDTO.php b/src/DTO/PersonDTO.php index d7605ae..279a7b1 100644 --- a/src/DTO/PersonDTO.php +++ b/src/DTO/PersonDTO.php @@ -67,7 +67,7 @@ public function __construct() private ?DateTimeImmutable $updatedAt = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { diff --git a/src/DTO/PosterDTO.php b/src/DTO/PosterDTO.php index cfb7c89..092de1a 100644 --- a/src/DTO/PosterDTO.php +++ b/src/DTO/PosterDTO.php @@ -35,7 +35,7 @@ public function __construct() private ?string $mainUrl = null; /** - * @param array|null $data + * @param array|null $data */ public static function fromArray(?array $data): ?self { diff --git a/src/DTO/StudioDTO.php b/src/DTO/StudioDTO.php index 518c362..604ed62 100644 --- a/src/DTO/StudioDTO.php +++ b/src/DTO/StudioDTO.php @@ -35,7 +35,7 @@ public function __construct() private ?string $imageUrl = null; /** - * @param array|null $data + * @param array|null $data */ public static function fromArray(?array $data): ?self { diff --git a/src/DTO/UserDTO.php b/src/DTO/UserDTO.php index eac5b65..0f2a362 100644 --- a/src/DTO/UserDTO.php +++ b/src/DTO/UserDTO.php @@ -67,7 +67,7 @@ public function __construct() private ?DateTimeImmutable $createdAt = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { diff --git a/src/DTO/UserRateDTO.php b/src/DTO/UserRateDTO.php index 7db1b00..185bae6 100644 --- a/src/DTO/UserRateDTO.php +++ b/src/DTO/UserRateDTO.php @@ -77,12 +77,13 @@ public function __construct() private ?AnimeDTO $target = null; /** - * @param array $data + * @param array $data */ public static function fromArray(array $data): self { $id = $data['id'] ?? 0; - $target = $data['target'] ?? null; + $user = ArrayUtil::optionalArray($data, 'user'); + $target = ArrayUtil::optionalArray($data, 'target'); return (new self()) ->withId(\is_int($id) || \is_string($id) ? $id : 0) @@ -94,8 +95,8 @@ public static function fromArray(array $data): self ->withRewatches(ArrayUtil::optionalInt($data, 'rewatches')) ->withCreatedAt(ArrayUtil::optionalDateTime($data, 'createdAt')) ->withUpdatedAt(ArrayUtil::optionalDateTime($data, 'updatedAt')) - ->withUser(\is_array($data['user'] ?? null) ? UserDTO::fromArray($data['user']) : null) - ->withTarget(\is_array($target) ? AnimeDTO::fromArray($target) : null); + ->withUser($user !== null ? UserDTO::fromArray($user) : null) + ->withTarget($target !== null ? AnimeDTO::fromArray($target) : null); } /** diff --git a/src/Entity/AnimeEntity.php b/src/Entity/AnimeEntity.php index 7f3d8c5..4541e80 100644 --- a/src/Entity/AnimeEntity.php +++ b/src/Entity/AnimeEntity.php @@ -9,6 +9,7 @@ use DateTimeImmutable; use DevCraftClub\Shikimori\DTO\AnimeDTO; use DevCraftClub\Shikimori\Persistence\StorableEntity; +use DevCraftClub\Shikimori\Util\ArrayUtil; #[Entity(table: 'shikimori_animes')] class AnimeEntity implements StorableEntity @@ -108,9 +109,9 @@ public function updateFromArray(array $data): void $this->setAiredOn(\is_string($data['airedOn'] ?? null) ? (string) $data['airedOn'] : null); $this->setReleasedOn(\is_string($data['releasedOn'] ?? null) ? (string) $data['releasedOn'] : null); $this->setUpdatedAt(self::parseDateTime($data['updatedAt'] ?? null)); - $this->setPoster(\is_array($data['poster'] ?? null) ? $data['poster'] : null); - $this->setGenres(\is_array($data['genres'] ?? null) ? $data['genres'] : null); - $this->setStudios(\is_array($data['studios'] ?? null) ? $data['studios'] : null); + $this->setPoster(ArrayUtil::optionalArray($data, 'poster')); + $this->setGenres(ArrayUtil::optionalListOfMaps($data, 'genres')); + $this->setStudios(ArrayUtil::optionalListOfMaps($data, 'studios')); $this->markFetchedNow(); } diff --git a/src/Persistence/CycleEntityStore.php b/src/Persistence/CycleEntityStore.php index 8ba6f83..96cc001 100644 --- a/src/Persistence/CycleEntityStore.php +++ b/src/Persistence/CycleEntityStore.php @@ -18,7 +18,6 @@ public function __construct( public function findById(string $entityClass, int|string $id): ?StorableEntity { - /** @phpstan-ignore-next-line Cycle template resolution */ $repository = $this->orm->getRepository($entityClass); $entity = $repository->findByPK($id); diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index c1035e2..936a60e 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -10,6 +10,7 @@ use DevCraftClub\Shikimori\Exception\NetworkException; use DevCraftClub\Shikimori\Filter\UserListFilter; use DevCraftClub\Shikimori\Query\UserQuery; +use DevCraftClub\Shikimori\Util\ArrayUtil; final readonly class UserRepository { @@ -76,6 +77,6 @@ public function currentUser(): ?UserDTO return null; } - return UserDTO::fromArray($data); + return UserDTO::fromArray(ArrayUtil::stringKeyed($data)); } } diff --git a/src/Util/ArrayUtil.php b/src/Util/ArrayUtil.php index 9ce55dc..3d3b1fb 100644 --- a/src/Util/ArrayUtil.php +++ b/src/Util/ArrayUtil.php @@ -9,31 +9,51 @@ final class ArrayUtil { /** - * @param array $data + * @param array $data + * @return array + */ + public static function stringKeyed(array $data): array + { + $result = []; + foreach ($data as $key => $value) { + $result[(string) $key] = $value; + } + + return $result; + } + + /** + * @param array $data */ public static function stringOrDefault(array $data, string $key, string $default): string { - return \is_string($data[$key] ?? null) ? (string) $data[$key] : $default; + $value = $data[$key] ?? null; + + return \is_string($value) ? $value : $default; } /** - * @param array $data + * @param array $data */ public static function optionalString(array $data, string $key): ?string { - return \is_string($data[$key] ?? null) ? (string) $data[$key] : null; + $value = $data[$key] ?? null; + + return \is_string($value) ? $value : null; } /** - * @param array $data + * @param array $data */ public static function optionalInt(array $data, string $key): ?int { - return \is_int($data[$key] ?? null) ? (int) $data[$key] : null; + $value = $data[$key] ?? null; + + return \is_int($value) ? $value : null; } /** - * @param array $data + * @param array $data */ public static function optionalFloat(array $data, string $key): ?float { @@ -43,16 +63,39 @@ public static function optionalFloat(array $data, string $key): ?float } /** - * @param array $data + * @param array $data * @return array|null */ public static function optionalArray(array $data, string $key): ?array { - return \is_array($data[$key] ?? null) ? $data[$key] : null; + $value = $data[$key] ?? null; + + return \is_array($value) ? self::stringKeyed($value) : null; + } + + /** + * @param array $data + * @return list>|null + */ + public static function optionalListOfMaps(array $data, string $key): ?array + { + $value = $data[$key] ?? null; + if (!\is_array($value)) { + return null; + } + + $result = []; + foreach ($value as $item) { + if (\is_array($item)) { + $result[] = self::stringKeyed($item); + } + } + + return $result; } /** - * @param array $data + * @param array $data */ public static function optionalDateTime(array $data, string $key): ?DateTimeImmutable {