diff --git a/src/Generator/Definitions/Definition.php b/src/Generator/Definitions/Definition.php new file mode 100644 index 00000000..1874a9b7 --- /dev/null +++ b/src/Generator/Definitions/Definition.php @@ -0,0 +1,18 @@ + + */ + public function collect(array $schema, string $path = ''): \Generator { + if (isset($schema['definitions'])) { + yield from $this->findNestedDefinitions($schema['definitions'], ($path ?: '#') . '/definitions'); + } + + if (isset($schema['$defs'])) { + yield from $this->findNestedDefinitions($schema['$defs'], ($path ?: '#') . '/$defs'); + } + } + + private function findNestedDefinitions(array $definitions, string $path): \Generator { + foreach ($definitions as $key => $value) { + $newPath = $path . '/' . $key; + yield $newPath => $this->pathToClassName($newPath, $value); + + if (is_array($value)) { + yield from $this->collect($value, $newPath); + } + } + } + + private function pathToClassName(string $path, array $schema): Definition { + $parts = array_map( + fn ($part) => str_replace(' ', '', ucwords(str_replace('_', ' ', $part))), + explode('/', ltrim(str_replace('$defs', 'Defs', $path), '#/')) + ); + + $className = array_pop($parts); + $namespace = $this->generatorRequest->getTargetNamespace() . '\\' . implode('\\', $parts); + $directory = $this->generatorRequest->getTargetDirectory() . '/' . implode('/', $parts); + + return new Definition( + namespace: $namespace, + directory: $directory, + classFQN: $namespace . '\\' . $className, + className: $className, + schema: $schema, + ); + } +} \ No newline at end of file diff --git a/src/Generator/Definitions/DefinitionsGenerator.php b/src/Generator/Definitions/DefinitionsGenerator.php new file mode 100644 index 00000000..2a35d938 --- /dev/null +++ b/src/Generator/Definitions/DefinitionsGenerator.php @@ -0,0 +1,33 @@ + $definitions + */ + public function generate(array $definitions, GeneratorRequest $generatorRequest): void + { + foreach ($definitions as $definition) { + $newRequest = $generatorRequest->withClass($definition->className) + ->withSchema($definition->schema) + ->withNamespace($definition->namespace) + ->withDirectory($definition->directory) + ; + + $this->schemaToClass->schemaToClass($newRequest); + } + } +} \ No newline at end of file diff --git a/src/Generator/Definitions/DefinitionsReferenceLookup.php b/src/Generator/Definitions/DefinitionsReferenceLookup.php new file mode 100644 index 00000000..716c39ee --- /dev/null +++ b/src/Generator/Definitions/DefinitionsReferenceLookup.php @@ -0,0 +1,38 @@ + $definitions + */ + public function __construct( + private array $definitions + ) + { + } + + public function lookupReference(string $reference): ReferencedType + { + if (isset($this->definitions[$reference])) { + return new ReferencedTypeClass($this->definitions[$reference]->classFQN); + } + return new ReferencedTypeUnknown(); + } + + public function lookupSchema(string $reference): array + { + if (isset($this->definitions[$reference])) { + return $this->definitions[$reference]->schema; + } + return []; + } +} \ No newline at end of file diff --git a/src/Generator/GeneratorRequest.php b/src/Generator/GeneratorRequest.php index ea3496fc..d702066b 100644 --- a/src/Generator/GeneratorRequest.php +++ b/src/Generator/GeneratorRequest.php @@ -17,7 +17,8 @@ class GeneratorRequest private array $schema; private ValidatedSpecificationFilesItem $spec; private SpecificationOptions $opts; - private ?ReferenceLookup $referenceLookup = null; + /** @var array */ + private array $referenceLookup = []; use GeneratorHookRunner; @@ -46,11 +47,28 @@ private static function semversifyVersionNumber(string|int $versionNumber): stri public function withReferenceLookup(ReferenceLookup $referenceLookup): self { $clone = clone $this; - $clone->referenceLookup = $referenceLookup; + $clone->referenceLookup = []; + $clone->referenceLookup[$referenceLookup::class] = $referenceLookup; return $clone; } + public function withAdditionalReferenceLookup(ReferenceLookup $referenceLookup): self + { + $clone = clone $this; + $clone->referenceLookup[$referenceLookup::class] = $referenceLookup; + + return $clone; + } + + /** + * @param class-string $referenceLookup + */ + public function hasReferenceLookup(string $referenceLookup): bool + { + return isset($this->referenceLookup[$referenceLookup]); + } + public function withSchema(array $schema): self { $clone = clone $this; @@ -69,6 +87,26 @@ public function withClass(string $targetClass): self return $clone; } + public function withNamespace(string $targetNamespace): self + { + $clone = clone $this; + $clone->spec = $this->spec->withTargetNamespace($targetNamespace); + + $clone->clearNonPropagatingHooks(); + + return $clone; + } + + public function withDirectory(string $targetDirectory): self + { + $clone = clone $this; + $clone->spec = $this->spec->withTargetDirectory($targetDirectory); + + $clone->clearNonPropagatingHooks(); + + return $clone; + } + public function withPHPVersion(string $targetPHPVersion): self { $clone = clone $this; @@ -182,19 +220,33 @@ public function getOptions(): SpecificationOptions public function lookupReference(string $ref): ReferencedType { - if ($this->referenceLookup === null) { + if (empty($this->referenceLookup)) { return new ReferencedTypeUnknown(); } - return $this->referenceLookup->lookupReference($ref); + foreach ($this->referenceLookup as $referenceLookup) { + $reference = $referenceLookup->lookupReference($ref); + if (!$reference instanceof ReferencedTypeUnknown) { + return $reference; + } + } + + return new ReferencedTypeUnknown(); } public function lookupSchema(string $ref): array { - if ($this->referenceLookup === null) { + if (empty($this->referenceLookup)) { return []; } - return $this->referenceLookup->lookupSchema($ref); + foreach ($this->referenceLookup as $referenceLookup) { + $schema = $referenceLookup->lookupSchema($ref); + if (!empty($schema)) { + return $schema; + } + } + + return []; } } diff --git a/src/Generator/SchemaToClass.php b/src/Generator/SchemaToClass.php index 0920ad09..dc03492a 100644 --- a/src/Generator/SchemaToClass.php +++ b/src/Generator/SchemaToClass.php @@ -4,6 +4,9 @@ namespace Helmich\Schema2Class\Generator; use Helmich\Schema2Class\Codegen\PropertyGenerator; +use Helmich\Schema2Class\Generator\Definitions\DefinitionsCollector; +use Helmich\Schema2Class\Generator\Definitions\DefinitionsGenerator; +use Helmich\Schema2Class\Generator\Definitions\DefinitionsReferenceLookup; use Helmich\Schema2Class\Generator\Property\IntersectProperty; use Helmich\Schema2Class\Generator\Property\NestedObjectProperty; use Helmich\Schema2Class\Generator\Property\PropertyCollection; @@ -38,6 +41,8 @@ public function schemaToClass(GeneratorRequest $req): void { $schema = $req->getSchema(); + $this->definitionsToSchemas($req); + if (isset($schema["enum"])) { $this->enumGenerator->schemaToEnum($req); return; @@ -129,4 +134,21 @@ public function schemaToClass(GeneratorRequest $req): void $this->writer->writeFile($filename, $content); } + private function definitionsToSchemas(GeneratorRequest &$req): void + { + if ($req->hasReferenceLookup(DefinitionsReferenceLookup::class)) { + return; + } + + $collector = new DefinitionsCollector($req); + $collectedDefinitions = iterator_to_array($collector->collect($req->getSchema())); + + $req = $req->withAdditionalReferenceLookup(new DefinitionsReferenceLookup( + $collectedDefinitions, + )); + + $generator = new DefinitionsGenerator($this); + $generator->generate($collectedDefinitions, $req); + } + } diff --git a/src/Spec/ValidatedSpecificationFilesItem.php b/src/Spec/ValidatedSpecificationFilesItem.php index f9cf4102..fc3bd589 100644 --- a/src/Spec/ValidatedSpecificationFilesItem.php +++ b/src/Spec/ValidatedSpecificationFilesItem.php @@ -62,4 +62,20 @@ public function withTargetClass(string $targetClass): self return $c; } + public function withTargetNamespace(string $targetNamespace): self + { + $c = clone $this; + $c->targetNamespace = $targetNamespace; + + return $c; + } + + public function withTargetDirectory(string $targetDirectory): self + { + $c = clone $this; + $c->targetDirectory = $targetDirectory; + + return $c; + } + } \ No newline at end of file diff --git a/tests/Generator/Fixtures/Definitions/Output/Definitions/Address.php b/tests/Generator/Fixtures/Definitions/Output/Definitions/Address.php new file mode 100644 index 00000000..a6e01bf5 --- /dev/null +++ b/tests/Generator/Fixtures/Definitions/Output/Definitions/Address.php @@ -0,0 +1,183 @@ + 'object', + 'properties' => [ + 'name' => [ + '$ref' => '#/definitions/address/$defs/name', + ], + 'city' => [ + 'type' => 'string', + ], + ], + 'required' => [ + 'city', + ], + '$defs' => [ + 'name' => [ + 'type' => 'object', + 'properties' => [ + 'first' => [ + 'type' => 'string', + ], + ], + ], + ], + ]; + + /** + * @var Address\Defs\Name|null + */ + private ?Address\Defs\Name $name = null; + + /** + * @var string + */ + private string $city; + + /** + * @param string $city + */ + public function __construct(string $city) + { + $this->city = $city; + } + + /** + * @return Address\Defs\Name|null + */ + public function getName() : ?Address\Defs\Name + { + return $this->name ?? null; + } + + /** + * @return string + */ + public function getCity() : string + { + return $this->city; + } + + /** + * @param Address\Defs\Name $name + * @return self + */ + public function withName(Address\Defs\Name $name) : self + { + $clone = clone $this; + $clone->name = $name; + + return $clone; + } + + /** + * @return self + */ + public function withoutName() : self + { + $clone = clone $this; + unset($clone->name); + + return $clone; + } + + /** + * @param string $city + * @return self + */ + public function withCity(string $city) : self + { + $validator = new \JsonSchema\Validator(); + $validator->validate($city, static::$schema['properties']['city']); + if (!$validator->isValid()) { + throw new \InvalidArgumentException($validator->getErrors()[0]['message']); + } + + $clone = clone $this; + $clone->city = $city; + + return $clone; + } + + /** + * Builds a new instance from an input array + * + * @param array|object $input Input data + * @param bool $validate Set this to false to skip validation; use at own risk + * @return Address Created instance + * @throws \InvalidArgumentException + */ + public static function buildFromInput(array|object $input, bool $validate = true) : Address + { + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + if ($validate) { + static::validateInput($input); + } + + $name = null; + if (isset($input->{'name'})) { + $name = Address\Defs\Name::buildFromInput($input->{'name'}, validate: $validate); + } + $city = $input->{'city'}; + + $obj = new self($city); + $obj->name = $name; + return $obj; + } + + /** + * Converts this object back to a simple array that can be JSON-serialized + * + * @return array Converted array + */ + public function toJson() : array + { + $output = []; + if (isset($this->name)) { + $output['name'] = $this->name->toJson(); + } + $output['city'] = $this->city; + + return $output; + } + + /** + * Validates an input array + * + * @param array|object $input Input data + * @param bool $return Return instead of throwing errors + * @return bool Validation result + * @throws \InvalidArgumentException + */ + public static function validateInput(array|object $input, bool $return = false) : bool + { + $validator = new \JsonSchema\Validator(); + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + $validator->validate($input, static::$schema); + + if (!$validator->isValid() && !$return) { + $errors = array_map(function(array $e): string { + return $e["property"] . ": " . $e["message"]; + }, $validator->getErrors()); + throw new \InvalidArgumentException(join(", ", $errors)); + } + + return $validator->isValid(); + } + + public function __clone() + { + } +} \ No newline at end of file diff --git a/tests/Generator/Fixtures/Definitions/Output/Definitions/Address/Defs/Name.php b/tests/Generator/Fixtures/Definitions/Output/Definitions/Address/Defs/Name.php new file mode 100644 index 00000000..98f0df2f --- /dev/null +++ b/tests/Generator/Fixtures/Definitions/Output/Definitions/Address/Defs/Name.php @@ -0,0 +1,139 @@ + 'object', + 'properties' => [ + 'first' => [ + 'type' => 'string', + ], + ], + ]; + + /** + * @var string|null + */ + private ?string $first = null; + + /** + * + */ + public function __construct() + { + } + + /** + * @return string|null + */ + public function getFirst() : ?string + { + return $this->first ?? null; + } + + /** + * @param string $first + * @return self + */ + public function withFirst(string $first) : self + { + $validator = new \JsonSchema\Validator(); + $validator->validate($first, static::$schema['properties']['first']); + if (!$validator->isValid()) { + throw new \InvalidArgumentException($validator->getErrors()[0]['message']); + } + + $clone = clone $this; + $clone->first = $first; + + return $clone; + } + + /** + * @return self + */ + public function withoutFirst() : self + { + $clone = clone $this; + unset($clone->first); + + return $clone; + } + + /** + * Builds a new instance from an input array + * + * @param array|object $input Input data + * @param bool $validate Set this to false to skip validation; use at own risk + * @return Name Created instance + * @throws \InvalidArgumentException + */ + public static function buildFromInput(array|object $input, bool $validate = true) : Name + { + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + if ($validate) { + static::validateInput($input); + } + + $first = null; + if (isset($input->{'first'})) { + $first = $input->{'first'}; + } + + $obj = new self(); + $obj->first = $first; + return $obj; + } + + /** + * Converts this object back to a simple array that can be JSON-serialized + * + * @return array Converted array + */ + public function toJson() : array + { + $output = []; + if (isset($this->first)) { + $output['first'] = $this->first; + } + + return $output; + } + + /** + * Validates an input array + * + * @param array|object $input Input data + * @param bool $return Return instead of throwing errors + * @return bool Validation result + * @throws \InvalidArgumentException + */ + public static function validateInput(array|object $input, bool $return = false) : bool + { + $validator = new \JsonSchema\Validator(); + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + $validator->validate($input, static::$schema); + + if (!$validator->isValid() && !$return) { + $errors = array_map(function(array $e): string { + return $e["property"] . ": " . $e["message"]; + }, $validator->getErrors()); + throw new \InvalidArgumentException(join(", ", $errors)); + } + + return $validator->isValid(); + } + + public function __clone() + { + } +} \ No newline at end of file diff --git a/tests/Generator/Fixtures/Definitions/Output/Foo.php b/tests/Generator/Fixtures/Definitions/Output/Foo.php new file mode 100644 index 00000000..227a5f15 --- /dev/null +++ b/tests/Generator/Fixtures/Definitions/Output/Foo.php @@ -0,0 +1,203 @@ + 'http://json-schema.org/draft-07/schema#', + '$id' => 'http://json-schema.org/draft-07/schema#', + 'title' => 'definitions test', + 'type' => 'object', + 'additionalProperties' => false, + 'definitions' => [ + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + '$ref' => '#/definitions/address/$defs/name', + ], + 'city' => [ + 'type' => 'string', + ], + ], + 'required' => [ + 'city', + ], + '$defs' => [ + 'name' => [ + 'type' => 'object', + 'properties' => [ + 'first' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + ], + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'address' => [ + '$ref' => '#/definitions/address', + ], + ], + 'required' => [ + 'id', + ], + ]; + + /** + * @var int + */ + private int $id; + + /** + * @var Definitions\Address|null + */ + private ?Definitions\Address $address = null; + + /** + * @param int $id + */ + public function __construct(int $id) + { + $this->id = $id; + } + + /** + * @return int + */ + public function getId() : int + { + return $this->id; + } + + /** + * @return Definitions\Address|null + */ + public function getAddress() : ?Definitions\Address + { + return $this->address ?? null; + } + + /** + * @param int $id + * @return self + */ + public function withId(int $id) : self + { + $validator = new \JsonSchema\Validator(); + $validator->validate($id, static::$schema['properties']['id']); + if (!$validator->isValid()) { + throw new \InvalidArgumentException($validator->getErrors()[0]['message']); + } + + $clone = clone $this; + $clone->id = $id; + + return $clone; + } + + /** + * @param Definitions\Address $address + * @return self + */ + public function withAddress(Definitions\Address $address) : self + { + $clone = clone $this; + $clone->address = $address; + + return $clone; + } + + /** + * @return self + */ + public function withoutAddress() : self + { + $clone = clone $this; + unset($clone->address); + + return $clone; + } + + /** + * Builds a new instance from an input array + * + * @param array|object $input Input data + * @param bool $validate Set this to false to skip validation; use at own risk + * @return Foo Created instance + * @throws \InvalidArgumentException + */ + public static function buildFromInput(array|object $input, bool $validate = true) : Foo + { + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + if ($validate) { + static::validateInput($input); + } + + $id = (int)($input->{'id'}); + $address = null; + if (isset($input->{'address'})) { + $address = Definitions\Address::buildFromInput($input->{'address'}, validate: $validate); + } + + $obj = new self($id); + $obj->address = $address; + return $obj; + } + + /** + * Converts this object back to a simple array that can be JSON-serialized + * + * @return array Converted array + */ + public function toJson() : array + { + $output = []; + $output['id'] = $this->id; + if (isset($this->address)) { + $output['address'] = $this->address->toJson(); + } + + return $output; + } + + /** + * Validates an input array + * + * @param array|object $input Input data + * @param bool $return Return instead of throwing errors + * @return bool Validation result + * @throws \InvalidArgumentException + */ + public static function validateInput(array|object $input, bool $return = false) : bool + { + $validator = new \JsonSchema\Validator(); + $input = is_array($input) ? \JsonSchema\Validator::arrayToObjectRecursive($input) : $input; + $validator->validate($input, static::$schema); + + if (!$validator->isValid() && !$return) { + $errors = array_map(function(array $e): string { + return $e["property"] . ": " . $e["message"]; + }, $validator->getErrors()); + throw new \InvalidArgumentException(join(", ", $errors)); + } + + return $validator->isValid(); + } + + public function __clone() + { + } +} \ No newline at end of file diff --git a/tests/Generator/Fixtures/Definitions/schema.json b/tests/Generator/Fixtures/Definitions/schema.json new file mode 100644 index 00000000..ae30ab9a --- /dev/null +++ b/tests/Generator/Fixtures/Definitions/schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "definitions test", + "type": "object", + "additionalProperties": false, + "definitions": { + "address": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/address/$defs/name" + }, + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "$defs": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + } + } + } + } + } + }, + "properties": { + "id": { + "type": "integer" + }, + "address": { + "$ref": "#/definitions/address" + } + }, + "required": [ + "id" + ] +} \ No newline at end of file diff --git a/tests/Generator/SchemaDefinitionsCollectorTest.php b/tests/Generator/SchemaDefinitionsCollectorTest.php new file mode 100644 index 00000000..a5893ae2 --- /dev/null +++ b/tests/Generator/SchemaDefinitionsCollectorTest.php @@ -0,0 +1,84 @@ + 'http://json-schema.org/draft-07/schema#', + '$id' => 'http://json-schema.org/draft-07/schema#', + 'title' => 'definitions test', + 'type' => 'object', + 'additionalProperties' => false, + 'definitions' => [ + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => [ + 'type' => 'string' + ] + ], + '$defs' => [ + 'name' => [ + 'type' => 'string' + ] + ] + ], + ], + '$defs' => [ + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => [ + 'type' => 'string' + ] + ] + ] + ] + ]; + + $definitionsGenerator = new DefinitionsCollector(new GeneratorRequest( + schema: $schema, + spec: new ValidatedSpecificationFilesItem('TargetNamespace', 'TargetClass', 'targetDirectory'), + opts: new SpecificationOptions(), + )); + $definitions = $definitionsGenerator->collect($schema); + $definitionsArray = iterator_to_array($definitions); + + $this->assertCount(3, $definitionsArray); + + $this->assertArrayHasKey('#/$defs/address', $definitionsArray); + $this->assertArrayHasKey('#/definitions/address', $definitionsArray); + $this->assertArrayHasKey('#/definitions/address/$defs/name', $definitionsArray); + + $this->assertInstanceOf(Definition::class, $definitionsArray['#/$defs/address']); + $this->assertInstanceOf(Definition::class, $definitionsArray['#/definitions/address']); + $this->assertInstanceOf(Definition::class, $definitionsArray['#/definitions/address/$defs/name']); + + $this->assertSame('TargetNamespace\Defs', $definitionsArray['#/$defs/address']->namespace); + $this->assertSame('TargetNamespace\Definitions', $definitionsArray['#/definitions/address']->namespace); + $this->assertSame('TargetNamespace\Definitions\Address\Defs', $definitionsArray['#/definitions/address/$defs/name']->namespace); + + $this->assertSame('targetDirectory/Defs', $definitionsArray['#/$defs/address']->directory); + $this->assertSame('targetDirectory/Definitions', $definitionsArray['#/definitions/address']->directory); + $this->assertSame('targetDirectory/Definitions/Address/Defs', $definitionsArray['#/definitions/address/$defs/name']->directory); + + $this->assertSame('TargetNamespace\Defs\Address', $definitionsArray['#/$defs/address']->classFQN); + $this->assertSame('TargetNamespace\Definitions\Address', $definitionsArray['#/definitions/address']->classFQN); + $this->assertSame('TargetNamespace\Definitions\Address\Defs\Name', $definitionsArray['#/definitions/address/$defs/name']->classFQN); + + $this->assertSame('Address', $definitionsArray['#/$defs/address']->className); + $this->assertSame('Address', $definitionsArray['#/definitions/address']->className); + $this->assertSame('Name', $definitionsArray['#/definitions/address/$defs/name']->className); + } +} \ No newline at end of file diff --git a/tests/Generator/SchemaToClassTest.php b/tests/Generator/SchemaToClassTest.php index a999b7d6..4d82f605 100644 --- a/tests/Generator/SchemaToClassTest.php +++ b/tests/Generator/SchemaToClassTest.php @@ -3,6 +3,7 @@ namespace Helmich\Schema2Class\Generator; +use FilesystemIterator; use Helmich\Schema2Class\Example\CustomerAddress; use Helmich\Schema2Class\Loader\SchemaLoader; use Helmich\Schema2Class\Spec\SpecificationOptions; @@ -10,6 +11,8 @@ use Helmich\Schema2Class\Writer\DebugWriter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Yaml\Yaml; use function PHPUnit\Framework\assertThat; @@ -40,9 +43,11 @@ public static function loadCodeGenerationTestCases(): array $optionsFile = join(DIRECTORY_SEPARATOR, [$testCaseDir, $entry, "options.yaml"]); $outputDir = join(DIRECTORY_SEPARATOR, [$testCaseDir, $entry, "Output"]); - $output = @opendir($outputDir); - if ($output === false) { + try { + $outputDirectoryIterator = new RecursiveDirectoryIterator($outputDir, FilesystemIterator::SKIP_DOTS); + $outputIterator = new RecursiveIteratorIterator($outputDirectoryIterator); + } catch (\UnexpectedValueException) { throw new \Exception("Could not open output directory for test case '{$entry}'"); } @@ -57,12 +62,20 @@ public static function loadCodeGenerationTestCases(): array $opts = SpecificationOptions::buildFromInput($optsYaml); } - while ($outputEntry = readdir($output)) { - if (substr($outputEntry, -4) !== ".php") { + /** @var \SplFileInfo $fileInfo */ + foreach ($outputIterator as $fileInfo) { + if ($fileInfo->getExtension() !== 'php') { continue; } - $expectedFiles[$outputEntry] = trim(file_get_contents(join(DIRECTORY_SEPARATOR, [$outputDir, $outputEntry]))); + $outputEntry = $fileInfo->getBasename(); + + /** @var RecursiveDirectoryIterator $directoryIterator */ + $directoryIterator = $outputIterator->getInnerIterator(); + if ($directoryIterator->getSubPath()) { + $outputEntry = join([$directoryIterator->getSubPath(), DIRECTORY_SEPARATOR, $outputEntry]); + } + $expectedFiles[$outputEntry] = trim(file_get_contents($fileInfo->getPathname())); } $testCases[$entry] = [$entry, $schema, $expectedFiles, $opts];