diff --git a/ApiDocGenerator.php b/ApiDocGenerator.php index 73f583d..eceb2fa 100644 --- a/ApiDocGenerator.php +++ b/ApiDocGenerator.php @@ -20,9 +20,12 @@ use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister; use OpenApi\Analysis; use OpenApi\Annotations\OpenApi; use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerAwareTrait; final class ApiDocGenerator { + use LoggerAwareTrait; + /** @var OpenApi */ private $openApi; @@ -81,6 +84,9 @@ final class ApiDocGenerator $this->openApi = new OpenApi([]); $modelRegistry = new ModelRegistry($this->modelDescribers, $this->openApi, $this->alternativeNames); + if (null !== $this->logger) { + $modelRegistry->setLogger($this->logger); + } foreach ($this->describers as $describer) { if ($describer instanceof ModelRegistryAwareInterface) { $describer->setModelRegistry($modelRegistry); diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 16a6583..2b868b4 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -66,11 +66,12 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI $container->setParameter('nelmio_api_doc.media_types', $config['media_types']); foreach ($config['areas'] as $area => $areaConfig) { $nameAliases = $this->findNameAliases($config['models']['names'], $area); - $container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class) ->setPublic(true) ->addMethodCall('setAlternativeNames', [$nameAliases]) ->addMethodCall('setMediaTypes', [$config['media_types']]) + ->addMethodCall('setLogger', [new Reference('logger')]) + ->addTag('monolog.logger', ['channel' => 'nelmio_api_doc']) ->setArguments([ new TaggedIteratorArgument(sprintf('nelmio_api_doc.describer.%s', $area)), new TaggedIteratorArgument('nelmio_api_doc.model_describer'), diff --git a/Model/ModelRegistry.php b/Model/ModelRegistry.php index 48bc4ba..51aabc8 100644 --- a/Model/ModelRegistry.php +++ b/Model/ModelRegistry.php @@ -15,10 +15,16 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; use Symfony\Component\PropertyInfo\Type; final class ModelRegistry { + use LoggerAwareTrait; + + private $registeredModelNames = []; + private $alternativeNames = []; private $unregistered = []; @@ -40,10 +46,11 @@ final class ModelRegistry { $this->modelDescribers = $modelDescribers; $this->api = $api; - + $this->logger = new NullLogger(); foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) { $this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']); $this->names[$model->getHash()] = $alternativeName; + $this->registeredModelNames[$alternativeName] = $model; Util::getSchema($this->api, $alternativeName); } } @@ -57,6 +64,7 @@ final class ModelRegistry } if (!isset($this->names[$hash])) { $this->names[$hash] = $this->generateModelName($model); + $this->registeredModelNames[$this->names[$hash]] = $model; } // Reserve the name @@ -115,6 +123,12 @@ final class ModelRegistry ); $i = 1; while (\in_array($name, $names, true)) { + if (isset($this->registeredModelNames[$name])) { + $this->logger->info(sprintf('Can not assign a name for the model, the name "%s" has already been taken.', $name), [ + 'model' => $this->modelToArray($model), + 'taken_by' => $this->modelToArray($this->registeredModelNames[$name]), + ]); + } ++$i; $name = $base.$i; } @@ -122,6 +136,26 @@ final class ModelRegistry return $name; } + private function modelToArray(Model $model): array + { + $getType = function (Type $type) use (&$getType) { + return [ + 'class' => $type->getClassName(), + 'built_in_type' => $type->getBuiltinType(), + 'nullable' => $type->isNullable(), + 'collection' => $type->isCollection(), + 'collection_key_types' => $type->isCollection() ? array_map($getType, $type->getCollectionKeyTypes()) : null, + 'collection_value_types' => $type->isCollection() ? array_map($getType, $type->getCollectionValueTypes()) : null, + ]; + }; + + return [ + 'type' => $getType($model->getType()), + 'options' => $model->getOptions(), + 'groups' => $model->getGroups(), + ]; + } + private function getTypeShortName(Type $type): string { if (null !== $type->getCollectionValueType()) { diff --git a/Tests/Model/ModelRegistryTest.php b/Tests/Model/ModelRegistryTest.php index 1ce1f8c..66eb16c 100644 --- a/Tests/Model/ModelRegistryTest.php +++ b/Tests/Model/ModelRegistryTest.php @@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\ModelRegistry; use OpenApi\Annotations as OA; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\PropertyInfo\Type; class ModelRegistryTest extends TestCase @@ -33,6 +34,96 @@ class ModelRegistryTest extends TestCase $this->assertEquals('#/components/schemas/array', $registry->register(new Model($type, ['group1']))); } + public function testNameCollisionsAreLogged() + { + $logger = $this->createMock(LoggerInterface::class); + $logger + ->expects(self::once()) + ->method('info') + ->with( + 'Can not assign a name for the model, the name "ModelRegistryTest" has already been taken.', [ + 'model' => [ + 'type' => [ + 'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest', + 'built_in_type' => 'object', + 'nullable' => false, + 'collection' => false, + 'collection_key_types' => null, + 'collection_value_types' => null, + ], + 'options' => null, + 'groups' => ['group2'], + ], + 'taken_by' => [ + 'type' => [ + 'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest', + 'built_in_type' => 'object', + 'nullable' => false, + 'collection' => false, + 'collection_key_types' => null, + 'collection_value_types' => null, + ], + 'options' => null, + 'groups' => ['group1'], + ], + ]); + + $registry = new ModelRegistry([], new OA\OpenApi([]), []); + $registry->setLogger($logger); + + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class); + $registry->register(new Model($type, ['group1'])); + $registry->register(new Model($type, ['group2'])); + } + + public function testNameCollisionsAreLoggedWithAlternativeNames() + { + $ref = new \ReflectionClass(self::class); + $alternativeNames = [ + $ref->getShortName() => [ + 'type' => $ref->getName(), + 'groups' => ['group1'], + ], + ]; + $logger = $this->createMock(LoggerInterface::class); + $logger + ->expects(self::once()) + ->method('info') + ->with( + 'Can not assign a name for the model, the name "ModelRegistryTest" has already been taken.', [ + 'model' => [ + 'type' => [ + 'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest', + 'built_in_type' => 'object', + 'nullable' => false, + 'collection' => false, + 'collection_key_types' => null, + 'collection_value_types' => null, + ], + 'options' => null, + 'groups' => ['group2'], + ], + 'taken_by' => [ + 'type' => [ + 'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest', + 'built_in_type' => 'object', + 'nullable' => false, + 'collection' => false, + 'collection_key_types' => null, + 'collection_value_types' => null, + ], + 'options' => null, + 'groups' => ['group1'], + ], + ]); + + $registry = new ModelRegistry([], new OA\OpenApi([]), $alternativeNames); + $registry->setLogger($logger); + + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class); + $registry->register(new Model($type, ['group2'])); + } + /** * @dataProvider getNameAlternatives *