Merge branch '3.x'

This commit is contained in:
Guilhem Niot 2020-12-10 22:28:55 +01:00
commit 363fd26f7c
24 changed files with 313 additions and 59 deletions

View File

@ -0,0 +1,64 @@
# from doctrine/instantiator:
# https://github.com/doctrine/instantiator/blob/97aa11bb71ad6259a8c5a1161b4de2d6cdcc5501/.github/workflows/continuous-integration.yml
name: "CI"
on:
pull_request:
branches:
- "*.x"
push:
branches:
- "*.x"
env:
fail-fast: true
COMPOSER_ROOT_VERSION: "1.4"
jobs:
phpunit:
name: "PHPUnit"
runs-on: "ubuntu-20.04"
strategy:
matrix:
include:
- php-version: 7.1
composer-flags: "--prefer-lowest"
- php-version: 7.2
symfony-require: "3.4.*"
- php-version: 7.3
symfony-require: "4.4.*"
- php-version: 7.3
symfony-require: "^5.0"
- php-version: 8.0
composer-flags: "--ignore-platform-reqs"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
with:
fetch-depth: 2
- name: "Install PHP without coverage"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
coverage: "none"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v2"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Install dependencies with composer"
env:
SYMFONY_REQUIRE: "${{ matrix.symfony-require }}"
run: |
composer global require --no-progress --no-scripts --no-plugins symfony/flex
composer update --no-interaction --no-progress ${{ matrix.composer-flags }}
- name: "Run PHPUnit"
run: "./phpunit"

1
.gitignore vendored
View File

@ -6,5 +6,6 @@
/.php_cs
/phpunit.xml
/.phpunit
/.phpunit.result.cache
/Tests/Functional/cache
/Tests/Functional/logs

View File

@ -1,36 +0,0 @@
language: php
php:
- 7.1
- 7.2
- 7.3
- 7.4
sudo: false
cache:
directories:
- .phpunit
- $HOME/.composer/cache
matrix:
fast_finish: true
include:
- php: 7.1
env: COMPOSER_FLAGS="--prefer-lowest"
- php: 7.3
env: SYMFONY_VERSION=^4.0
- php: 7.3
env: SYMFONY_VERSION=^5.0
- php: 7.4
env: SYMFONY_VERSION=^4.0
- php: 7.4
env: SYMFONY_VERSION=^5.0
before_install:
- phpenv config-rm xdebug.ini || true
- if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --dev --no-update; fi;
install: composer update --no-interaction $COMPOSER_FLAGS
script: ./phpunit

View File

@ -11,7 +11,7 @@ You MUST follow the [PSR-1](http://www.php-fig.org/psr/psr-1/) and
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).
You MUST run the test suite.
You MUST run the test suite (run `composer update`, and then execute `vendor/bin/simple-phpunit`).
You MUST write (or update) unit tests.

View File

@ -61,5 +61,7 @@ final class SwaggerUiController
Response::HTTP_OK,
['Content-Type' => 'text/html']
);
return $response->setCharset('UTF-8');
}
}

View File

@ -29,7 +29,11 @@ final class ConfigurationPass implements CompilerPassInterface
$container->register('nelmio_api_doc.model_describers.form', FormModelDescriber::class)
->setPublic(false)
->addArgument(new Reference('form.factory'))
->addArgument(new Reference('annotation_reader'))
->addArgument($container->getParameter('nelmio_api_doc.media_types'))
->addTag('nelmio_api_doc.model_describer', ['priority' => 100]);
}
$container->getParameterBag()->remove('nelmio_api_doc.media_types');
}
}

View File

@ -62,6 +62,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
->setFactory([new Reference('router'), 'getRouteCollection']);
$container->setParameter('nelmio_api_doc.areas', array_keys($config['areas']));
$container->setParameter('nelmio_api_doc.media_types', $config['media_types']);
foreach ($config['areas'] as $area => $areaConfig) {
$nameAliases = $this->findNameAliases($config['models']['names'], $area);

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Exception;
class UndocumentedArrayItemsException extends \LogicException
{
private $class;
private $path;
public function __construct(string $class = null, string $path = '')
{
$this->class = $class;
$this->path = $path;
$propertyName = '';
if (null !== $class) {
$propertyName = $class.'::';
}
$propertyName .= $path;
parent::__construct(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@SWG\Property(type="array", @SWG\Items(type="string"))`.', $propertyName));
}
public function getClass()
{
return $this->class;
}
public function getPath()
{
return $this->path;
}
}

View File

@ -29,7 +29,7 @@ class DocumentationExtension extends AbstractTypeExtension
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['documentation' => []])
->setAllowedTypes('documentation', ['array']);
->setAllowedTypes('documentation', ['array', 'bool']);
}
public function getExtendedType()

View File

@ -11,9 +11,11 @@
namespace Nelmio\ApiDocBundle\ModelDescriber;
use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\Form\AbstractType;
@ -33,10 +35,22 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
use ModelRegistryAwareTrait;
private $formFactory;
private $doctrineReader;
private $mediaTypes;
public function __construct(FormFactoryInterface $formFactory = null)
public function __construct(FormFactoryInterface $formFactory = null, Reader $reader = null, array $mediaTypes = null)
{
$this->formFactory = $formFactory;
$this->doctrineReader = $reader;
if (null === $reader) {
@trigger_error(sprintf('Not passing a doctrine reader to the constructor of %s is deprecated since version 3.8 and won\'t be allowed in version 5.', self::class), E_USER_DEPRECATED);
}
if (null === $mediaTypes) {
$mediaTypes = ['json'];
@trigger_error(sprintf('Not passing media types to the constructor of %s is deprecated since version 4.1 and won\'t be allowed in version 5.', self::class), E_USER_DEPRECATED);
}
$this->mediaTypes = $mediaTypes;
}
public function describe(Model $model, OA\Schema $schema)
@ -52,6 +66,9 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$class = $model->getType()->getClassName();
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
$annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
$form = $this->formFactory->create($class, null, $model->getOptions() ?? []);
$this->parseForm($schema, $form);
}
@ -65,6 +82,11 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
{
foreach ($form as $name => $child) {
$config = $child->getConfig();
// This field must not be documented
if ($config->hasOption('documentation') && false === $config->getOption('documentation')) {
continue;
}
$property = Util::getProperty($schema, $name);
if ($config->getRequired()) {

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber;
use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
@ -148,7 +149,15 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
$propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($propertyDescriber->supports($types)) {
$propertyDescriber->describe($types, $property, $model->getGroups());
try {
$propertyDescriber->describe($types, $property, $model->getGroups());
} catch (UndocumentedArrayItemsException $e) {
if (null !== $e->getClass()) {
throw $e; // This exception is already complete
}
throw new UndocumentedArrayItemsException($model->getType()->getClassName(), sprintf('%s%s', $propertyName, $e->getPath()));
}
return;
}

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\PropertyDescriber;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
@ -33,7 +34,7 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
{
$type = $types[0]->getCollectionValueType();
if (null === $type) {
throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@OA\Property(type="array", @OA\Items(type="string"))`.', $property->property));
throw new UndocumentedArrayItemsException();
}
$property->type = 'array';
@ -45,7 +46,15 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
$propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($propertyDescriber->supports([$type])) {
$propertyDescriber->describe([$type], $property, $groups);
try {
$propertyDescriber->describe([$type], $property, $groups);
} catch (UndocumentedArrayItemsException $e) {
if (null !== $e->getClass()) {
throw $e; // This exception is already complete
}
throw new UndocumentedArrayItemsException(null, sprintf('%s[]', $e->getPath()));
}
break;
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional;
use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException;
class ArrayItemsErrorTest extends WebTestCase
{
protected function setUp(): void
{
parent::setUp();
static::createClient([], ['HTTP_HOST' => 'api.example.com']);
}
public function testModelPictureDocumentation()
{
$this->expectException(UndocumentedArrayItemsException::class);
$this->expectExceptionMessage('Property "Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItemsError\Bar::things[]" is an array, but its items type isn\'t specified.');
$this->getOpenApiDefinition();
}
protected static function createKernel(array $options = [])
{
return new TestKernel(TestKernel::ERROR_ARRAY_ITEMS);
}
}

View File

@ -125,6 +125,6 @@ class BazingaFunctionalTest extends WebTestCase
protected static function createKernel(array $options = [])
{
return new TestKernel(true, true);
return new TestKernel(TestKernel::USE_JMS | TestKernel::USE_BAZINGA);
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItemsError\Foo;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(host="api.example.com")
*/
class ArrayItemsErrorController
{
/**
* @Route("/api/error", methods={"GET"})
* @OA\Response(
* response=200,
* description="Success",
* @Model(type=Foo::class)
* )
*/
public function errorAction()
{
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItemsError;
/**
* @author Guilhem N. <guilhem@gniot.fr>
*/
class Bar
{
public $things;
public function addThing(array $thing) { }
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItemsError;
/**
* @author Guilhem N. <guilhem@gniot.fr>
*/
class Foo
{
/**
* @var string
*/
public $articles;
/**
* @var Bar[]
*/
public $bars;
}

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Form;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use OpenApi\Annotations as OA;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -19,6 +20,11 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @OA\Schema(
* description="this is the description of an user"
* )
*/
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
@ -42,6 +48,7 @@ class UserType extends AbstractType
->add('document', DocumentType::class, ['class' => 'Document'])
->add('documents', DocumentType::class, ['class' => 'Document', 'multiple' => true])
->add('extended_builtin', ExtendedBuiltinType::class, ['required_option' => 'foo'])
->add('hidden', DummyType::class, ['documentation' => false])
->add('save', SubmitType::class);
}

View File

@ -224,6 +224,7 @@ class FunctionalTest extends WebTestCase
{
$this->assertEquals([
'type' => 'object',
'description' => 'this is the description of an user',
'properties' => [
'strings' => [
'items' => ['type' => 'string'],

View File

@ -324,6 +324,6 @@ class JMSFunctionalTest extends WebTestCase
protected static function createKernel(array $options = [])
{
return new TestKernel(true);
return new TestKernel(TestKernel::USE_JMS);
}
}

View File

@ -33,6 +33,7 @@ class SwaggerUiTest extends WebTestCase
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('UTF-8', $response->getCharset());
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
$expected = json_decode($this->getOpenApiDefinition()->toJson(), true);

View File

@ -33,17 +33,19 @@ use Symfony\Component\Serializer\Annotation\SerializedName;
class TestKernel extends Kernel
{
const USE_JMS = 1;
const USE_BAZINGA = 2;
const ERROR_ARRAY_ITEMS = 4;
use MicroKernelTrait;
private $useJMS;
private $useBazinga;
private $flags;
public function __construct(bool $useJMS = false, bool $useBazinga = false)
public function __construct(int $flags = 0)
{
parent::__construct('test'.(int) $useJMS.(int) $useBazinga, true);
parent::__construct('test'.$flags, true);
$this->useJMS = $useJMS;
$this->useBazinga = $useBazinga;
$this->flags = $flags;
}
/**
@ -61,10 +63,10 @@ class TestKernel extends Kernel
new FOSRestBundle(),
];
if ($this->useJMS) {
if ($this->flags & self::USE_JMS) {
$bundles[] = new JMSSerializerBundle();
if ($this->useBazinga) {
if ($this->flags & self::USE_BAZINGA) {
$bundles[] = new BazingaHateoasBundle();
}
}
@ -91,11 +93,11 @@ class TestKernel extends Kernel
$routes->import(__DIR__.'/Controller/SerializedNameController.php', '/', 'annotation');
}
if ($this->useJMS) {
if ($this->flags & self::USE_JMS) {
$routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation');
}
if ($this->useBazinga) {
if ($this->flags & self::USE_BAZINGA) {
$routes->import(__DIR__.'/Controller/BazingaController.php', '/', 'annotation');
try {
@ -104,6 +106,10 @@ class TestKernel extends Kernel
} catch (\ReflectionException $e) {
}
}
if ($this->flags & self::ERROR_ARRAY_ITEMS) {
$routes->import(__DIR__.'/Controller/ArrayItemsErrorController.php', '/', 'annotation');
}
}
/**
@ -250,7 +256,7 @@ class TestKernel extends Kernel
*/
public function getCacheDir()
{
return parent::getCacheDir().'/'.(int) $this->useJMS;
return parent::getCacheDir().'/'.$this->flags;
}
/**
@ -258,7 +264,7 @@ class TestKernel extends Kernel
*/
public function getLogDir()
{
return parent::getLogDir().'/'.(int) $this->useJMS;
return parent::getLogDir().'/'.$this->flags;
}
public function serialize()

View File

@ -15,7 +15,7 @@
}
],
"require": {
"php": "^7.1",
"php": ">=7.1.3",
"ext-json": "*",
"symfony/framework-bundle": "^4.0|^5.0",
"symfony/options-resolver": "^4.0|^5.0",
@ -43,7 +43,7 @@
"doctrine/common": "^2.4",
"api-platform/core": "^2.4",
"friendsofsymfony/rest-bundle": "^2.0|^3.0@beta",
"friendsofsymfony/rest-bundle": "^2.0|^3.0@dev",
"willdurand/hateoas-bundle": "^1.0|^2.0",
"jms/serializer-bundle": "^2.3|^3.0",
"jms/serializer": "^1.14|^3.0"

View File

@ -5,5 +5,4 @@ if (!file_exists(__DIR__.'/vendor/symfony/phpunit-bridge/bin/simple-phpunit')) {
exit(1);
}
putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');
putenv('SYMFONY_PHPUNIT_VERSION=7.5');
require __DIR__.'/vendor/symfony/phpunit-bridge/bin/simple-phpunit';