mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Heavily refactored the property metadata attribute logic
The new method is much more universal and fixes issue #862
This commit is contained in:
parent
b38ef8ecea
commit
c4ba28e3a0
5 changed files with 202 additions and 123 deletions
|
@ -1,116 +0,0 @@
|
||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\ApiPlatform;
|
|
||||||
|
|
||||||
use ApiPlatform\JsonSchema\Schema;
|
|
||||||
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
|
|
||||||
use ApiPlatform\Metadata\Operation;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This decorator adds the properties given by DocumentedAPIProperty attributes on the classes to the schema.
|
|
||||||
*/
|
|
||||||
#[AsDecorator('api_platform.json_schema.schema_factory')]
|
|
||||||
class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterface
|
|
||||||
{
|
|
||||||
|
|
||||||
public function __construct(private readonly SchemaFactoryInterface $decorated)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildSchema(
|
|
||||||
string $className,
|
|
||||||
string $format = 'json',
|
|
||||||
string $type = Schema::TYPE_OUTPUT,
|
|
||||||
?Operation $operation = null,
|
|
||||||
?Schema $schema = null,
|
|
||||||
?array $serializerContext = null,
|
|
||||||
bool $forceCollection = false
|
|
||||||
): Schema {
|
|
||||||
|
|
||||||
|
|
||||||
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
|
|
||||||
|
|
||||||
//Check if there is are DocumentedAPIProperty attributes on the class
|
|
||||||
$reflectionClass = new \ReflectionClass($className);
|
|
||||||
$attributes = $reflectionClass->getAttributes(DocumentedAPIProperty::class);
|
|
||||||
foreach ($attributes as $attribute) {
|
|
||||||
/** @var DocumentedAPIProperty $api_property */
|
|
||||||
$api_property = $attribute->newInstance();
|
|
||||||
$this->addPropertyToSchema($schema, $api_property->schemaName, $api_property->property,
|
|
||||||
$api_property, $serializerContext ?? [], $format);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addPropertyToSchema(Schema $schema, string $definitionName, string $normalizedPropertyName, DocumentedAPIProperty $propertyMetadata, array $serializerContext, string $format): void
|
|
||||||
{
|
|
||||||
$version = $schema->getVersion();
|
|
||||||
$swagger = Schema::VERSION_SWAGGER === $version;
|
|
||||||
|
|
||||||
$propertySchema = [];
|
|
||||||
|
|
||||||
if (false === $propertyMetadata->writeable) {
|
|
||||||
$propertySchema['readOnly'] = true;
|
|
||||||
}
|
|
||||||
if (!$swagger && false === $propertyMetadata->readable) {
|
|
||||||
$propertySchema['writeOnly'] = true;
|
|
||||||
}
|
|
||||||
if (null !== $description = $propertyMetadata->description) {
|
|
||||||
$propertySchema['description'] = $description;
|
|
||||||
}
|
|
||||||
|
|
||||||
$deprecationReason = $propertyMetadata->deprecationReason;
|
|
||||||
|
|
||||||
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
|
||||||
if (!$swagger && null !== $deprecationReason) {
|
|
||||||
$propertySchema['deprecated'] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($default = $propertyMetadata->default)) {
|
|
||||||
if ($default instanceof \BackedEnum) {
|
|
||||||
$default = $default->value;
|
|
||||||
}
|
|
||||||
$propertySchema['default'] = $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($example = $propertyMetadata->example)) {
|
|
||||||
$propertySchema['example'] = $example;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($propertySchema['example']) && isset($propertySchema['default'])) {
|
|
||||||
$propertySchema['example'] = $propertySchema['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$propertySchema['type'] = $propertyMetadata->type;
|
|
||||||
$propertySchema['nullable'] = $propertyMetadata->nullable;
|
|
||||||
|
|
||||||
$propertySchema = new \ArrayObject($propertySchema);
|
|
||||||
|
|
||||||
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -21,7 +21,9 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
namespace App\ApiPlatform;
|
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When this attribute is applied to a class, an property will be added to the API documentation using the given parameters.
|
* When this attribute is applied to a class, an property will be added to the API documentation using the given parameters.
|
||||||
|
@ -64,4 +66,55 @@ final class DocumentedAPIProperty
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toAPIProperty(bool $use_swagger = false): ApiProperty
|
||||||
|
{
|
||||||
|
$openApiContext = [];
|
||||||
|
|
||||||
|
if (false === $this->writeable) {
|
||||||
|
$openApiContext['readOnly'] = true;
|
||||||
|
}
|
||||||
|
if (!$use_swagger && false === $this->readable) {
|
||||||
|
$openApiContext['writeOnly'] = true;
|
||||||
|
}
|
||||||
|
if (null !== $description = $this->description) {
|
||||||
|
$openApiContext['description'] = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deprecationReason = $this->deprecationReason;
|
||||||
|
|
||||||
|
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
||||||
|
if (!$use_swagger && null !== $deprecationReason) {
|
||||||
|
$openApiContext['deprecated'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($default = $this->default)) {
|
||||||
|
if ($default instanceof \BackedEnum) {
|
||||||
|
$default = $default->value;
|
||||||
|
}
|
||||||
|
$openApiContext['default'] = $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($example = $this->example)) {
|
||||||
|
$openApiContext['example'] = $example;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($openApiContext['example']) && isset($openApiContext['default'])) {
|
||||||
|
$openApiContext['example'] = $openApiContext['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$openApiContext['type'] = $this->type;
|
||||||
|
$openApiContext['nullable'] = $this->nullable;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return new ApiProperty(
|
||||||
|
description: $this->description,
|
||||||
|
readable: $this->readable,
|
||||||
|
writable: $this->writeable,
|
||||||
|
openapiContext: $openApiContext,
|
||||||
|
types: $this->type,
|
||||||
|
property: $this->property
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
|
||||||
|
use ReflectionClass;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata
|
||||||
|
* which then get picked up by the openapi schema generator
|
||||||
|
*/
|
||||||
|
#[AsDecorator('api_platform.metadata.property.metadata_factory')]
|
||||||
|
class PropertyMetadataFactory implements PropertyMetadataFactoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(private PropertyMetadataFactoryInterface $decorated)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
|
||||||
|
{
|
||||||
|
$metadata = $this->decorated->create($resourceClass, $property, $options);
|
||||||
|
|
||||||
|
//Only become active in the context of the openapi schema generation
|
||||||
|
if (!isset($options['schema_type'])) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists($resourceClass)) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refClass = new ReflectionClass($resourceClass);
|
||||||
|
$attributes = $refClass->getAttributes(DocumentedAPIProperty::class);
|
||||||
|
|
||||||
|
//Look for the DocumentedAPIProperty attribute with the given property name
|
||||||
|
foreach ($attributes as $attribute) {
|
||||||
|
/** @var DocumentedAPIProperty $api_property */
|
||||||
|
$api_property = $attribute->newInstance();
|
||||||
|
//If attribute not matches the property name, skip it
|
||||||
|
if ($api_property->property !== $property) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return the virtual property
|
||||||
|
return $api_property->toAPIProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
||||||
|
use ApiPlatform\Metadata\Property\PropertyNameCollection;
|
||||||
|
use ReflectionClass;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection
|
||||||
|
* which then get picked up by the openapi schema generator
|
||||||
|
*/
|
||||||
|
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
|
||||||
|
class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $resourceClass, array $options = []): PropertyNameCollection
|
||||||
|
{
|
||||||
|
// Get the default properties from the decorated service
|
||||||
|
$propertyNames = $this->decorated->create($resourceClass, $options);
|
||||||
|
|
||||||
|
//Only become active in the context of the openapi schema generation
|
||||||
|
if (!isset($options['schema_type'])) {
|
||||||
|
return $propertyNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists($resourceClass)) {
|
||||||
|
return $propertyNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
$properties = iterator_to_array($propertyNames);
|
||||||
|
|
||||||
|
$refClass = new ReflectionClass($resourceClass);
|
||||||
|
|
||||||
|
foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) {
|
||||||
|
/** @var DocumentedAPIProperty $instance */
|
||||||
|
$instance = $attribute->newInstance();
|
||||||
|
$properties[] = $instance->property;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PropertyNameCollection($properties);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,23 +33,24 @@ use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
use ApiPlatform\Metadata\Patch;
|
use ApiPlatform\Metadata\Patch;
|
||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use App\ApiPlatform\DocumentedAPIProperty;
|
use App\ApiPlatform\DocumentedAPIProperties\DocumentedAPIProperty;
|
||||||
use App\ApiPlatform\Filter\EntityFilter;
|
use App\ApiPlatform\Filter\EntityFilter;
|
||||||
use App\ApiPlatform\Filter\LikeFilter;
|
use App\ApiPlatform\Filter\LikeFilter;
|
||||||
use App\ApiPlatform\HandleAttachmentsUploadsProcessor;
|
use App\ApiPlatform\HandleAttachmentsUploadsProcessor;
|
||||||
use App\Repository\AttachmentRepository;
|
|
||||||
use App\EntityListeners\AttachmentDeleteListener;
|
|
||||||
use Doctrine\DBAL\Types\Types;
|
|
||||||
use App\Entity\Base\AbstractNamedDBElement;
|
use App\Entity\Base\AbstractNamedDBElement;
|
||||||
|
use App\EntityListeners\AttachmentDeleteListener;
|
||||||
|
use App\Repository\AttachmentRepository;
|
||||||
use App\Validator\Constraints\Selectable;
|
use App\Validator\Constraints\Selectable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use LogicException;
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||||
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
|
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
use function in_array;
|
use function in_array;
|
||||||
use InvalidArgumentException;
|
|
||||||
use LogicException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Attachment.
|
* Class Attachment.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue