From d86c497114a6de9f20fc324d0a455d8354cb784a Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Wed, 14 Oct 2026 16:52:20 +0200 Subject: [PATCH 1/2] Add TypeCollection::findByNameAndXmlNamespace Returns an Option instead of throwing when the type is missing. fetchByNameAndXmlNamespace is now built on top of it and keeps its exception. --- src/Metadata/Collection/TypeCollection.php | 22 +++++++++++++------ .../Collection/TypeCollectionTest.php | 17 ++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Metadata/Collection/TypeCollection.php b/src/Metadata/Collection/TypeCollection.php index 1a30c7d..180828c 100644 --- a/src/Metadata/Collection/TypeCollection.php +++ b/src/Metadata/Collection/TypeCollection.php @@ -7,10 +7,11 @@ use ArrayIterator; use Countable; use IteratorAggregate; +use Psl\Option\Option; use Soap\Engine\Exception\MetadataException; use Soap\Engine\Metadata\Model\Type; -use function array_key_exists; use function Psl\Dict\reindex; +use function Psl\Option\from_nullable; /** * @implements IteratorAggregate, Type> @@ -108,17 +109,24 @@ public function fetchFirstByName(string $name): Type throw MetadataException::typeNotFound($name); } + /** + * Convention: fetch* throws when the type is missing, find* returns an Option. + * + * @return Option + */ + public function findByNameAndXmlNamespace(string $name, string $namespace): Option + { + return from_nullable($this->qualifiedLookup[self::createLookupKey($name, $namespace)] ?? null); + } + /** * @throws MetadataException */ public function fetchByNameAndXmlNamespace(string $name, string $namespace): Type { - $lookupKey = self::createLookupKey($name, $namespace); - if (!array_key_exists($lookupKey, $this->qualifiedLookup)) { - throw MetadataException::typeNotFound($name); - } - - return $this->qualifiedLookup[$lookupKey]; + return $this->findByNameAndXmlNamespace($name, $namespace)->unwrapOrElse( + static fn (): never => throw MetadataException::typeNotFound($name) + ); } private static function createLookupKey(string $name, string $namespace): string diff --git a/tests/Unit/Metadata/Collection/TypeCollectionTest.php b/tests/Unit/Metadata/Collection/TypeCollectionTest.php index d680a66..5aaa2f0 100644 --- a/tests/Unit/Metadata/Collection/TypeCollectionTest.php +++ b/tests/Unit/Metadata/Collection/TypeCollectionTest.php @@ -58,6 +58,23 @@ public function test_it_can_fail_fetching_by_name_and_namespace_invalid_namespac $this->collection->fetchByNameAndXmlNamespace('Response', 'http://invalid'); } + public function test_it_can_find_by_name_and_namespace(): void + { + $option = $this->collection->findByNameAndXmlNamespace('Response', 'http://namesapce'); + static::assertTrue($option->isSome()); + static::assertSame('Response', $option->unwrap()->getName()); + } + + public function test_it_finds_nothing_by_name_and_namespace_for_invalid_name(): void + { + static::assertTrue($this->collection->findByNameAndXmlNamespace('nope', 'http://namesapce')->isNone()); + } + + public function test_it_finds_nothing_by_name_and_namespace_for_other_namespace(): void + { + static::assertTrue($this->collection->findByNameAndXmlNamespace('Response', 'http://invalid')->isNone()); + } + public function test_it_can_reduce(): void { $result = $this->collection->reduce(static fn (int $i, Type $item) => $i + 1, 0); From 1cda5d91bd610834eb11edb644697cc70cc44005 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Wed, 14 Oct 2026 18:37:04 +0200 Subject: [PATCH 2/2] Return null instead of an Option from findByNameAndXmlNamespace The lookup runs on hot encode and decode paths, and an Option allocates on every call. Nullable keeps it as cheap as the keyed lookup itself. --- src/Metadata/Collection/TypeCollection.php | 15 +++++---------- .../Metadata/Collection/TypeCollectionTest.php | 9 ++++----- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Metadata/Collection/TypeCollection.php b/src/Metadata/Collection/TypeCollection.php index 180828c..de06922 100644 --- a/src/Metadata/Collection/TypeCollection.php +++ b/src/Metadata/Collection/TypeCollection.php @@ -7,11 +7,9 @@ use ArrayIterator; use Countable; use IteratorAggregate; -use Psl\Option\Option; use Soap\Engine\Exception\MetadataException; use Soap\Engine\Metadata\Model\Type; use function Psl\Dict\reindex; -use function Psl\Option\from_nullable; /** * @implements IteratorAggregate, Type> @@ -110,13 +108,11 @@ public function fetchFirstByName(string $name): Type } /** - * Convention: fetch* throws when the type is missing, find* returns an Option. - * - * @return Option + * Convention: fetch* throws when the type is missing, find* returns null. */ - public function findByNameAndXmlNamespace(string $name, string $namespace): Option + public function findByNameAndXmlNamespace(string $name, string $namespace): ?Type { - return from_nullable($this->qualifiedLookup[self::createLookupKey($name, $namespace)] ?? null); + return $this->qualifiedLookup[self::createLookupKey($name, $namespace)] ?? null; } /** @@ -124,9 +120,8 @@ public function findByNameAndXmlNamespace(string $name, string $namespace): Opti */ public function fetchByNameAndXmlNamespace(string $name, string $namespace): Type { - return $this->findByNameAndXmlNamespace($name, $namespace)->unwrapOrElse( - static fn (): never => throw MetadataException::typeNotFound($name) - ); + return $this->findByNameAndXmlNamespace($name, $namespace) + ?? throw MetadataException::typeNotFound($name); } private static function createLookupKey(string $name, string $namespace): string diff --git a/tests/Unit/Metadata/Collection/TypeCollectionTest.php b/tests/Unit/Metadata/Collection/TypeCollectionTest.php index 5aaa2f0..1a050c5 100644 --- a/tests/Unit/Metadata/Collection/TypeCollectionTest.php +++ b/tests/Unit/Metadata/Collection/TypeCollectionTest.php @@ -60,19 +60,18 @@ public function test_it_can_fail_fetching_by_name_and_namespace_invalid_namespac public function test_it_can_find_by_name_and_namespace(): void { - $option = $this->collection->findByNameAndXmlNamespace('Response', 'http://namesapce'); - static::assertTrue($option->isSome()); - static::assertSame('Response', $option->unwrap()->getName()); + $type = $this->collection->findByNameAndXmlNamespace('Response', 'http://namesapce'); + static::assertSame('Response', $type?->getName()); } public function test_it_finds_nothing_by_name_and_namespace_for_invalid_name(): void { - static::assertTrue($this->collection->findByNameAndXmlNamespace('nope', 'http://namesapce')->isNone()); + static::assertNull($this->collection->findByNameAndXmlNamespace('nope', 'http://namesapce')); } public function test_it_finds_nothing_by_name_and_namespace_for_other_namespace(): void { - static::assertTrue($this->collection->findByNameAndXmlNamespace('Response', 'http://invalid')->isNone()); + static::assertNull($this->collection->findByNameAndXmlNamespace('Response', 'http://invalid')); } public function test_it_can_reduce(): void