├── LICENSE ├── composer.json ├── psalm-baseline.xml └── src ├── AbstractDefinition.php ├── Attribute └── Field.php ├── Exception ├── DefinitionException.php ├── GeneratorException.php └── InvalidTypeException.php ├── Generator.php ├── GeneratorInterface.php ├── Parser ├── ClassParser.php ├── ClassParserInterface.php ├── Parser.php ├── ParserInterface.php ├── Property.php ├── PropertyInterface.php ├── Type.php └── TypeInterface.php ├── Schema.php └── Schema ├── Definition.php ├── Format.php ├── Property.php ├── PropertyOption.php ├── PropertyOptions.php ├── Reference.php └── Type.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Spiral Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spiral/json-schema-generator", 3 | "description": "Provides the ability to generate JSON schemas from Data Transfer Object (DTO) classes", 4 | "keywords": [], 5 | "homepage": "https://github.com/spiral/json-schema-generator", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Anton Titov (wolfy-j)", 10 | "email": "wolfy-j@spiralscout.com" 11 | }, 12 | { 13 | "name": "Pavel Buchnev (butschster)", 14 | "email": "pavel.buchnev@spiralscout.com" 15 | }, 16 | { 17 | "name": "Aleksei Gagarin (roxblnfk)", 18 | "email": "alexey.gagarin@spiralscout.com" 19 | }, 20 | { 21 | "name": "Maksim Smakouz (msmakouz)", 22 | "email": "maksim.smakouz@spiralscout.com" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=8.1", 27 | "symfony/property-info": "^6.4.18 || ^7.2", 28 | "phpstan/phpdoc-parser": "^1.33 | ^2.1", 29 | "phpdocumentor/reflection-docblock": "^5.3" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^10.5.45", 33 | "spiral/code-style": "^2.2.2", 34 | "vimeo/psalm": "^5.26.1 || ^6.10" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Spiral\\JsonSchemaGenerator\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Spiral\\JsonSchemaGenerator\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "cs:fix": "php-cs-fixer fix -v", 48 | "test": "phpunit", 49 | "psalm": "psalm", 50 | "psalm:baseline": "psalm --set-baseline=psalm-baseline.xml" 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true 57 | } 58 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/AbstractDefinition.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected array $properties = []; 15 | 16 | /** 17 | * @param non-empty-string $name 18 | */ 19 | public function addProperty(string $name, Property $property): self 20 | { 21 | $this->properties[$name] = $property; 22 | 23 | return $this; 24 | } 25 | 26 | protected function renderProperties(array $schema): array 27 | { 28 | if ($this->properties === []) { 29 | return $schema; 30 | } 31 | 32 | $schema['properties'] = []; 33 | 34 | // Building properties 35 | $required = []; 36 | foreach ($this->properties as $name => $property) { 37 | $schema['properties'][$name] = $property->jsonSerialize(); 38 | 39 | if ($property->required) { 40 | $required[] = $name; 41 | } 42 | } 43 | 44 | if ($required !== []) { 45 | $schema['required'] = $required; 46 | } 47 | 48 | return $schema; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Attribute/Field.php: -------------------------------------------------------------------------------- 1 | parser->parse($class); 30 | 31 | // check cached 32 | if (isset($this->cache[$class->getName()])) { 33 | return $this->cache[$class->getName()]; 34 | } 35 | 36 | $schema = new Schema(); 37 | 38 | $dependencies = []; 39 | // Generating properties 40 | foreach ($class->getProperties() as $property) { 41 | $psc = $this->generateProperty($property); 42 | if ($psc === null) { 43 | continue; 44 | } 45 | 46 | // does it refer to any other classes 47 | $dependencies = [...$dependencies, ...$psc->getDependencies()]; 48 | 49 | $schema->addProperty($property->getName(), $psc); 50 | } 51 | 52 | // Generating dependencies 53 | $dependencies = \array_unique($dependencies); 54 | $rollingDependencies = []; 55 | $doneDependencies = []; 56 | 57 | do { 58 | foreach ($dependencies as $dependency) { 59 | $dependency = $this->parser->parse($dependency); 60 | $definition = $this->generateDefinition($dependency, $rollingDependencies); 61 | if ($definition === null) { 62 | continue; 63 | } 64 | 65 | $schema->addDefinition($dependency->getShortName(), $definition); 66 | } 67 | 68 | $doneDependencies = [...$doneDependencies, ...$dependencies]; 69 | $rollingDependencies = \array_diff($rollingDependencies, $doneDependencies); 70 | if ($rollingDependencies === []) { 71 | break; 72 | } 73 | 74 | $dependencies = $rollingDependencies; 75 | } while (true); 76 | 77 | // caching 78 | $this->cache[$class->getName()] = $schema; 79 | 80 | return $schema; 81 | } 82 | 83 | protected function generateDefinition(ClassParserInterface $class, array &$dependencies = []): ?Definition 84 | { 85 | $properties = []; 86 | if ($class->isEnum()) { 87 | return new Definition( 88 | type: $class->getName(), 89 | options: $class->getEnumValues(), 90 | title: $class->getShortName(), 91 | ); 92 | } 93 | 94 | // class properties 95 | foreach ($class->getProperties() as $property) { 96 | $psc = $this->generateProperty($property); 97 | if ($psc === null) { 98 | continue; 99 | } 100 | 101 | $dependencies = [...$dependencies, ...$psc->getDependencies()]; 102 | $properties[$property->getName()] = $psc; 103 | } 104 | 105 | return new Definition(type: $class->getName(), title: $class->getShortName(), properties: $properties); 106 | } 107 | 108 | protected function generateProperty(PropertyInterface $property): ?Property 109 | { 110 | // Looking for Field attribute 111 | $title = ''; 112 | $description = ''; 113 | $default = null; 114 | $format = null; 115 | 116 | $attribute = $property->findAttribute(Field::class); 117 | if ($attribute !== null) { 118 | $title = $attribute->title; 119 | $description = $attribute->description; 120 | $default = $attribute->default; 121 | $format = $attribute->format; 122 | } 123 | 124 | if ($default === null && $property->hasDefaultValue()) { 125 | $default = $property->getDefaultValue(); 126 | } 127 | 128 | $type = $property->getType(); 129 | 130 | $options = []; 131 | if ($property->isCollection()) { 132 | $options = \array_map( 133 | static fn(TypeInterface $type) => $type->getName(), 134 | $property->getCollectionValueTypes(), 135 | ); 136 | } 137 | 138 | $required = $default === null && !$type->allowsNull(); 139 | if ($type->isBuiltin()) { 140 | return new Property($type->getName(), $options, $title, $description, $required, $default, $format); 141 | } 142 | 143 | // Class or enum 144 | $class = $type->getName(); 145 | 146 | return \is_string($class) && \class_exists($class) 147 | ? new Property($class, [], $title, $description, $required, $default, $format) 148 | : null; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private array $constructorParameters = []; 26 | 27 | private readonly PropertyInfoExtractorInterface $propertyInfo; 28 | 29 | /** 30 | * @param \ReflectionClass|class-string $class 31 | */ 32 | public function __construct(\ReflectionClass|string $class) 33 | { 34 | if (\is_string($class)) { 35 | try { 36 | $class = new \ReflectionClass($class); 37 | } catch (\ReflectionException $e) { 38 | throw new GeneratorException($e->getMessage(), $e->getCode(), $e); 39 | } 40 | } 41 | 42 | $this->class = $class; 43 | $this->propertyInfo = $this->createPropertyInfo(); 44 | 45 | if ($this->class->hasMethod('__construct')) { 46 | $constructor = $this->class->getMethod('__construct'); 47 | foreach ($constructor->getParameters() as $parameter) { 48 | $this->constructorParameters[$parameter->getName()] = $parameter; 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * @return class-string 55 | */ 56 | public function getName(): string 57 | { 58 | return $this->class->getName(); 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function getShortName(): string 65 | { 66 | return $this->class->getShortName(); 67 | } 68 | 69 | /** 70 | * @return array 71 | */ 72 | public function getProperties(): array 73 | { 74 | $properties = []; 75 | foreach ($this->class->getProperties() as $property) { 76 | // skipping private, protected, static properties, properties without type 77 | if ($property->isPrivate() || $property->isProtected() || $property->isStatic() || !$property->hasType()) { 78 | continue; 79 | } 80 | 81 | /** 82 | * @var \ReflectionNamedType|null $type 83 | */ 84 | $type = $property->getType(); 85 | if (!$type instanceof \ReflectionNamedType) { 86 | continue; 87 | } 88 | 89 | $properties[] = new Property( 90 | property: $property, 91 | type: new Type(name: $type->getName(), builtin: $type->isBuiltin(), nullable: $type->allowsNull()), 92 | hasDefaultValue: $this->hasPropertyDefaultValue($property), 93 | defaultValue: $this->getPropertyDefaultValue($property), 94 | collectionValueTypes: $this->getPropertyCollectionTypes($property->getName()), 95 | ); 96 | } 97 | 98 | return $properties; 99 | } 100 | 101 | public function isEnum(): bool 102 | { 103 | return $this->class->isEnum(); 104 | } 105 | 106 | public function getEnumValues(): array 107 | { 108 | if (!$this->isEnum()) { 109 | throw new GeneratorException(\sprintf('Class `%s` is not an enum.', $this->class->getName())); 110 | } 111 | 112 | $values = []; 113 | foreach ($this->class->getReflectionConstants() as $constant) { 114 | $value = $constant->getValue(); 115 | \assert($value instanceof \BackedEnum); 116 | 117 | $values[] = $value->value; 118 | } 119 | 120 | return $values; 121 | } 122 | 123 | /** 124 | * @param non-empty-string $property 125 | * 126 | * @return array 127 | */ 128 | private function getPropertyCollectionTypes(string $property): array 129 | { 130 | $types = $this->propertyInfo->getTypes($this->class->getName(), $property); 131 | 132 | $collectionTypes = []; 133 | foreach ($types ?? [] as $type) { 134 | if ($type->isCollection()) { 135 | $collectionTypes = [...$type->getCollectionValueTypes(), ...$collectionTypes]; 136 | } 137 | } 138 | 139 | $result = []; 140 | foreach ($collectionTypes as $type) { 141 | /** 142 | * @var non-empty-string $name 143 | */ 144 | $name = $type->getBuiltinType() === SchemaType::Object->value 145 | ? $type->getClassName() 146 | : $type->getBuiltinType(); 147 | 148 | $result[] = new Type( 149 | name: $name, 150 | builtin: $type->getBuiltinType() !== SchemaType::Object->value, 151 | nullable: $type->isNullable(), 152 | ); 153 | } 154 | 155 | return $result; 156 | } 157 | 158 | private function hasPropertyDefaultValue(\ReflectionProperty $property): bool 159 | { 160 | $parameter = $this->constructorParameters[$property->getName()] ?? null; 161 | 162 | return $property->hasDefaultValue() || ($parameter !== null && $parameter->isDefaultValueAvailable()); 163 | } 164 | 165 | private function getPropertyDefaultValue(\ReflectionProperty $property): mixed 166 | { 167 | if ($property->hasDefaultValue()) { 168 | $default = $property->getDefaultValue(); 169 | } 170 | 171 | $parameter = $this->constructorParameters[$property->getName()] ?? null; 172 | if ($parameter !== null && $property->isPromoted() && $parameter->isDefaultValueAvailable()) { 173 | $default = $parameter->getDefaultValue(); 174 | } 175 | 176 | return $default ?? null; 177 | } 178 | 179 | private function createPropertyInfo(): PropertyInfoExtractorInterface 180 | { 181 | return new PropertyInfoExtractor(typeExtractors: [ 182 | new PhpStanExtractor(), 183 | new PhpDocExtractor(), 184 | new ReflectionExtractor(), 185 | ]); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Parser/ClassParserInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function getProperties(): array; 23 | 24 | public function isEnum(): bool; 25 | 26 | public function getEnumValues(): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | property->getName(); 28 | } 29 | 30 | /** 31 | * @template T 32 | * 33 | * @param class-string $name The class name of the attribute. 34 | * 35 | * @return T|null The attribute or {@see null}, if the requested attribute does not exist. 36 | */ 37 | public function findAttribute(string $name): ?object 38 | { 39 | $name = $this->property->getAttributes($name); 40 | if ($name !== []) { 41 | return $name[0]->newInstance(); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | public function hasDefaultValue(): bool 48 | { 49 | return $this->hasDefaultValue; 50 | } 51 | 52 | public function getDefaultValue(): mixed 53 | { 54 | return $this->defaultValue; 55 | } 56 | 57 | public function isCollection(): bool 58 | { 59 | $type = $this->type->getName(); 60 | if (!$type instanceof SchemaType) { 61 | return false; 62 | } 63 | 64 | return $type->value === SchemaType::Array->value; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getCollectionValueTypes(): array 71 | { 72 | return $this->collectionValueTypes; 73 | } 74 | 75 | public function getType(): TypeInterface 76 | { 77 | return $this->type; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Parser/PropertyInterface.php: -------------------------------------------------------------------------------- 1 | $name The class name of the attribute. 18 | * 19 | * @return T|null The attribute or {@see null}, if the requested attribute does not exist. 20 | */ 21 | public function findAttribute(string $name): ?object; 22 | 23 | public function hasDefaultValue(): bool; 24 | 25 | public function getDefaultValue(): mixed; 26 | 27 | public function isCollection(): bool; 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getCollectionValueTypes(): array; 33 | 34 | public function getType(): TypeInterface; 35 | } 36 | -------------------------------------------------------------------------------- /src/Parser/Type.php: -------------------------------------------------------------------------------- 1 | name = $this->builtin ? SchemaType::fromBuiltIn($name) : $name; 29 | } 30 | 31 | /** 32 | * @return class-string|SchemaType 33 | */ 34 | public function getName(): string|SchemaType 35 | { 36 | return $this->name; 37 | } 38 | 39 | public function isBuiltin(): bool 40 | { 41 | return $this->builtin; 42 | } 43 | 44 | public function allowsNull(): bool 45 | { 46 | return $this->nullable; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Parser/TypeInterface.php: -------------------------------------------------------------------------------- 1 | definitions[$name] = $definition; 16 | return $this; 17 | } 18 | 19 | public function jsonSerialize(): array 20 | { 21 | $schema = $this->renderProperties([]); 22 | 23 | if ($this->definitions !== []) { 24 | $schema['definitions'] = []; 25 | 26 | foreach ($this->definitions as $name => $definition) { 27 | $schema['definitions'][$name] = $definition->jsonSerialize(); 28 | } 29 | } 30 | 31 | return $schema; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Schema/Definition.php: -------------------------------------------------------------------------------- 1 | $property) { 23 | if (!$property instanceof Property) { 24 | throw new DefinitionException(\sprintf( 25 | 'Property `%s` is not an instance of `%s`.', 26 | // type name or class name 27 | \is_object($property) ? \get_class($property) : \gettype($property), 28 | Property::class, 29 | )); 30 | } 31 | 32 | $this->addProperty($name, $property); 33 | } 34 | } 35 | 36 | public function jsonSerialize(): array 37 | { 38 | $schema = []; 39 | if ($this->title !== '') { 40 | $schema['title'] = $this->title; 41 | } 42 | 43 | if ($this->description !== '') { 44 | $schema['description'] = $this->description; 45 | } 46 | 47 | if ($this->type instanceof Type) { 48 | $schema['type'] = $this->type->value; 49 | } else { 50 | $schema = $this->renderType($schema); 51 | } 52 | 53 | return $this->renderProperties($schema); 54 | } 55 | 56 | private function renderType(array $schema): array 57 | { 58 | if ($this->properties !== []) { 59 | $schema['type'] = 'object'; 60 | return $this->renderProperties($schema); 61 | } 62 | 63 | $rf = new \ReflectionClass($this->type); 64 | if (!$rf->isEnum()) { 65 | throw new DefinitionException(\sprintf( 66 | 'Type `%s` must be a backed enum or class with properties.', 67 | $this->type instanceof Type ? $this->type->value : $this->type, 68 | )); 69 | } 70 | 71 | $rf = new \ReflectionEnum($this->type); 72 | 73 | /** @var \ReflectionEnum $rf */ 74 | if (!$rf->isBacked()) { 75 | throw new DefinitionException(\sprintf( 76 | 'Type `%s` is not a backed enum.', 77 | $this->type instanceof Type ? $this->type->value : $this->type, 78 | )); 79 | } 80 | 81 | /** 82 | * @var \ReflectionNamedType $type 83 | */ 84 | $type = $rf->getBackingType(); 85 | 86 | // mapping to json schema type 87 | $schema['type'] = match ($type->getName()) { 88 | 'int', 'float' => 'number', 89 | 'string' => 'string', 90 | 'bool' => 'boolean', 91 | default => throw new DefinitionException(\sprintf( 92 | 'Type `%s` is not a backed enum.', 93 | $this->type instanceof Type ? $this->type->value : $this->type, 94 | )), 95 | }; 96 | 97 | // options are scalar values at this point 98 | $schema['enum'] = $this->options; 99 | 100 | return $schema; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Schema/Format.php: -------------------------------------------------------------------------------- 1 | $options 16 | */ 17 | public function __construct( 18 | public readonly Type|string $type, 19 | array $options = [], 20 | public readonly string $title = '', 21 | public readonly string $description = '', 22 | public readonly bool $required = false, 23 | public readonly mixed $default = null, 24 | public readonly ?Format $format = null, 25 | ) { 26 | if (\is_string($this->type) && !\class_exists($this->type)) { 27 | throw new InvalidTypeException('Invalid type definition.'); 28 | } 29 | 30 | $this->options = new PropertyOptions($options); 31 | } 32 | 33 | public function jsonSerialize(): array 34 | { 35 | $property = []; 36 | if ($this->title !== '') { 37 | $property['title'] = $this->title; 38 | } 39 | 40 | if ($this->description !== '') { 41 | $property['description'] = $this->description; 42 | } 43 | 44 | if ($this->default !== null) { 45 | $property['default'] = $this->default; 46 | } 47 | 48 | if ($this->format instanceof Format) { 49 | $property['format'] = $this->format->value; 50 | } 51 | 52 | 53 | if ($this->type === Type::Union) { 54 | $property['anyOf'] = $this->options->jsonSerialize(); 55 | return $property; 56 | } 57 | 58 | if (\is_string($this->type)) { 59 | // this is nested class 60 | $property['allOf'][] = ['$ref' => (new Reference($this->type))->jsonSerialize()]; 61 | return $property; 62 | } 63 | 64 | $property['type'] = $this->type->value; 65 | 66 | if ($this->type === Type::Array) { 67 | if (\count($this->options) === 1) { 68 | if (\is_string($this->options[0]->value)) { 69 | // reference to class 70 | $property['items']['$ref'] = (new Reference($this->options[0]->value))->jsonSerialize(); 71 | return $property; 72 | } 73 | 74 | $property['items']['type'] = $this->options[0]->value->value; 75 | } else { 76 | $property['items']['anyOf'] = $this->options->jsonSerialize(); 77 | } 78 | } 79 | 80 | return $property; 81 | } 82 | 83 | public function getDependencies(): array 84 | { 85 | $dependencies = []; 86 | foreach ($this->options->getOptions() as $option) { 87 | if (\is_string($option->value)) { 88 | $dependencies[] = $option->value; 89 | } 90 | } 91 | 92 | if (\is_string($this->type)) { 93 | $dependencies[] = $this->type; 94 | } 95 | 96 | return $dependencies; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Schema/PropertyOption.php: -------------------------------------------------------------------------------- 1 | value) && !\class_exists($this->value)) { 21 | throw new InvalidTypeException('Invalid property option definition.'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Schema/PropertyOptions.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class PropertyOptions implements \Countable, \ArrayAccess, \JsonSerializable 12 | { 13 | /** 14 | * @var array 15 | */ 16 | private array $options = []; 17 | 18 | /** 19 | * @param array $options 20 | */ 21 | public function __construct(array $options = []) 22 | { 23 | foreach ($options as $option) { 24 | $this->options[] = new PropertyOption($option); 25 | } 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getOptions(): array 32 | { 33 | return $this->options; 34 | } 35 | 36 | public function count(): int 37 | { 38 | return \count($this->options); 39 | } 40 | 41 | public function offsetExists(mixed $offset): bool 42 | { 43 | return isset($this->options[$offset]); 44 | } 45 | 46 | public function offsetGet(mixed $offset): PropertyOption 47 | { 48 | return $this->options[$offset]; 49 | } 50 | 51 | /** 52 | * @param int $offset 53 | * @param PropertyOption|class-string|Type $value 54 | */ 55 | public function offsetSet(mixed $offset, mixed $value): void 56 | { 57 | $this->options[$offset] = $value instanceof PropertyOption ? $value : new PropertyOption($value); 58 | } 59 | 60 | public function offsetUnset(mixed $offset): void 61 | { 62 | unset($this->options[$offset]); 63 | } 64 | 65 | public function jsonSerialize(): array 66 | { 67 | $types = []; 68 | foreach ($this->options as $option) { 69 | if (\is_string($option->value)) { 70 | // reference to class 71 | $types[] = ['$ref' => (new Reference($option->value))->jsonSerialize()]; 72 | continue; 73 | } 74 | 75 | $types[] = ['type' => $option->value->value]; 76 | } 77 | return $types; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Schema/Reference.php: -------------------------------------------------------------------------------- 1 | className, '\\'); 22 | 23 | return '#/definitions/' . ($pos === false ? $this->className : \substr($this->className, $pos + 1)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Schema/Type.php: -------------------------------------------------------------------------------- 1 | self::String, 23 | 'integer', 'int' => self::Integer, 24 | 'float', 'double', 'number' => self::Number, 25 | 'boolean', 'bool' => self::Boolean, 26 | 'object' => self::Object, 27 | 'array' => self::Array, 28 | 'null' => self::Null, 29 | default => throw new \InvalidArgumentException(\sprintf('Invalid type `%s`.', $type)), 30 | }; 31 | } 32 | } 33 | --------------------------------------------------------------------------------