diff --git a/.circleci/config.yml b/.circleci/config.yml index a0ff5e33f77..a2f3d78b5e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ commands: disable-xdebug-php-extension: steps: - run: - name: Disable Xdebug PHP extension + name: Disable xdebug PHP extension command: sudo rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini install-mongodb-php-extension: steps: @@ -46,6 +46,13 @@ commands: command: | sudo pecl install pcov-1.0.6 sudo docker-php-ext-enable pcov + install-symfony-flex: + steps: + - run: + name: Install Symfony Flex + command: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex merge-code-coverage-reports: parameters: dir: @@ -88,6 +95,7 @@ commands: restore-composer-cache: steps: - restore_cache: + name: Restore Composer cache keys: - composer-cache-{{ .Revision }} - composer-cache-{{ .Branch }} @@ -95,6 +103,7 @@ commands: restore-npm-cache: steps: - restore_cache: + name: Restore npm cache keys: - npm-cache-{{ .Revision }} - npm-cache-{{ .Branch }} @@ -102,6 +111,7 @@ commands: restore-php-cs-fixer-cache: steps: - restore_cache: + name: Restore PHP-CS-Fixer cache keys: - php-cs-fixer-cache-{{ .Revision }} - php-cs-fixer-cache-{{ .Branch }} @@ -109,6 +119,7 @@ commands: restore-phpstan-cache: steps: - restore_cache: + name: Restore PHPStan cache keys: - phpstan-cache-{{ .Revision }} - phpstan-cache-{{ .Branch }} @@ -116,40 +127,48 @@ commands: save-composer-cache: steps: - save_cache: + name: Save Composer cache paths: - - ~/.composer/cache + - ~/.composer/cache/files key: composer-cache-{{ .Branch }}-{{ .BuildNum }} - save_cache: + name: Save Composer cache paths: - - ~/.composer/cache + - ~/.composer/cache/files key: composer-cache-{{ .Revision }}-{{ .BuildNum }} save-npm-cache: steps: - save_cache: + name: Save npm cache paths: - ~/.npm key: npm-cache-{{ .Branch }}-{{ .BuildNum }} - save_cache: + name: Save npm cache paths: - ~/.npm key: npm-cache-{{ .Revision }}-{{ .BuildNum }} save-php-cs-fixer-cache: steps: - save_cache: + name: Save PHP-CS-Fixer cache paths: - .php_cs.cache key: php-cs-fixer-cache-{{ .Branch }}-{{ .BuildNum }} - save_cache: + name: Save PHP-CS-Fixer cache paths: - .php_cs.cache key: php-cs-fixer-cache-{{ .Revision }}-{{ .BuildNum }} save-phpstan-cache: steps: - save_cache: + name: Save PHPStan cache paths: - /tmp/phpstan/cache key: phpstan-cache-{{ .Branch }}-{{ .BuildNum }} - save_cache: + name: Save PHPStan cache paths: - /tmp/phpstan/cache key: phpstan-cache-{{ .Revision }}-{{ .BuildNum }} @@ -201,27 +220,34 @@ executors: php: docker: - image: circleci/php:7.3-node + environment: + SYMFONY_REQUIRE: ^3.4 || ^4.0 php-and-elasticsearch: docker: - image: circleci/php:7.3-node - # https://github.com/elastic/elasticsearch-docker/issues/84 - - image: docker.elastic.co/elasticsearch/elasticsearch:6.7.2 + environment: + SYMFONY_REQUIRE: ^3.4 || ^4.0 + - image: docker.elastic.co/elasticsearch/elasticsearch:6.8.3 # https://github.com/elastic/elasticsearch/issues/43627 php-and-mongodb: docker: - image: circleci/php:7.3-node + environment: + SYMFONY_REQUIRE: ^3.4 || ^4.0 - image: circleci/mongo:4 jobs: php-cs-fixer: + description: PHP-CS-Fixer executor: php environment: - PHP_CS_FIXER_FUTURE_MODE: 1 + PHP_CS_FIXER_FUTURE_MODE: '1' working_directory: ~/api-platform/core steps: - checkout - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - run: name: Install PHP-CS-Fixer command: | @@ -237,10 +263,10 @@ jobs: - save-php-cs-fixer-cache phpstan: + description: PHPStan executor: php environment: - # https://github.com/phpstan/phpstan-symfony/issues/37 - APP_DEBUG: 1 + APP_DEBUG: '1' # https://github.com/phpstan/phpstan-symfony/issues/37 working_directory: ~/api-platform/core steps: - checkout @@ -248,6 +274,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache @@ -258,6 +285,7 @@ jobs: - save-phpstan-cache phpunit-coverage: + description: PHPUnit (code coverage) executor: php parallelism: 2 working_directory: ~/api-platform/core @@ -268,6 +296,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache @@ -305,6 +334,7 @@ jobs: - save-npm-cache behat-coverage: + description: Behat (code coverage) executor: php parallelism: 2 working_directory: ~/api-platform/core @@ -315,6 +345,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache @@ -349,6 +380,7 @@ jobs: - save-npm-cache phpunit-mongodb-coverage: + description: PHPUnit (MongoDB) (code coverage) executor: php-and-mongodb environment: APP_ENV: mongodb @@ -360,6 +392,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache @@ -386,6 +419,7 @@ jobs: - save-npm-cache behat-mongodb-coverage: + description: Behat (MongoDB) (code coverage) executor: php-and-mongodb environment: APP_ENV: mongodb @@ -397,6 +431,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache @@ -429,6 +464,7 @@ jobs: - save-npm-cache behat-elasticsearch-coverage: + description: Behat (Elasticsearch) (code coverage) executor: php-and-elasticsearch environment: APP_ENV: elasticsearch @@ -440,6 +476,7 @@ jobs: - disable-xdebug-php-extension - disable-php-memory-limit - restore-composer-cache + - install-symfony-flex - update-project-dependencies - save-composer-cache - clear-test-app-cache diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..03c62aff951 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +/.circleci export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php_cs.dist export-ignore +/.travis.yml export-ignore +/appveyor.yml export-ignore +/behat.yml.dist export-ignore +/features export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/phpunit_mongodb.xml export-ignore +/tests export-ignore +/update-js.sh export-ignore +/yarn.lock export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index eb455a2e2a8..00000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ - -| Q | A -| ------------- | --- -| Bug fix? | yes/no -| New feature? | yes/no -| BC breaks? | no -| Deprecations? | no -| Tests pass? | yes -| Fixed tickets | #1234, #5678 -| License | MIT -| Doc PR | api-platform/doc#1234 - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 678ed93a24e..6e627c533c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,22 +2,24 @@ name: CI on: push: - # branches: - # - master - # - '[0-9]*.[0-9]*' - # - '[0-9]*.x' - # tags: - # - 'v[0-9]*.[0-9]*' pull_request: +env: + COMPOSER_ALLOW_SUPERUSER: '1' # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser + COMPOSER_INSTALLER_COMMIT: fb22b78362d31c0d2bf516d1f8cdfd2745caa431 + EXT_MONGODB_VERSION: '1.5.5' + LEGACY: '0' + SYMFONY_REQUIRE: ^3.4 || ^4.0 + jobs: phpunit: name: PHPUnit (PHP ${{ matrix.php }}) runs-on: ubuntu-latest container: - image: php:${{ matrix.php }} + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec strategy: - # https://github.community/t5/GitHub-Actions/GitHub-Actions-Matrix-options-dont-work-as-documented/m-p/29558 matrix: php: - '7.1' @@ -30,40 +32,53 @@ jobs: uses: actions/checkout@v1 - name: Install system packages run: | - apt-get update - apt-get install -y --no-install-recommends \ - unzip + apk add \ + unzip \ - name: Install mongodb PHP extension - if: matrix.php != '7.1' + if: (!startsWith(matrix.php, '7.1')) run: | - pecl install mongodb-1.5.5 + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION docker-php-ext-enable mongodb - name: Disable PHP memory limit run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini - name: Install Composer - run: curl -sS -o - https://raw.githubusercontent.com/composer/getcomposer.org/ba13e3fc70f1c66250d1ea7ea4911d593aa1dba5/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex - name: Remove Doctrine MongoDB ODM - if: matrix.php == '7.1' + if: startsWith(matrix.php, '7.1') run: | composer remove --dev --no-progress --no-update --ansi \ doctrine/mongodb-odm \ - doctrine/mongodb-odm-bundle + doctrine/mongodb-odm-bundle \ - name: Update project dependencies - run: composer update --no-progress --no-suggest --ansi + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests run: vendor/bin/phpunit --colors=always - phpunit-lowest-deps: - name: PHPUnit (PHP ${{ matrix.php }}) (lowest dependencies) + behat: + name: Behat (PHP ${{ matrix.php }}) runs-on: ubuntu-latest container: - image: php:${{ matrix.php }} + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec strategy: - # https://github.community/t5/GitHub-Actions/GitHub-Actions-Matrix-options-dont-work-as-documented/m-p/29558 matrix: php: + - '7.1' + - '7.2' - '7.3' - '7.4-rc' fail-fast: false @@ -72,27 +87,733 @@ jobs: uses: actions/checkout@v1 - name: Install system packages run: | - apt-get update - apt-get install -y --no-install-recommends \ - unzip + apk add \ + unzip \ - name: Install mongodb PHP extension - if: matrix.php != '7.1' + if: (!startsWith(matrix.php, '7.1')) run: | - pecl install mongodb-1.5.5 + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION docker-php-ext-enable mongodb - name: Disable PHP memory limit run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini - name: Install Composer - run: curl -sS -o - https://raw.githubusercontent.com/composer/getcomposer.org/ba13e3fc70f1c66250d1ea7ea4911d593aa1dba5/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex - name: Remove Doctrine MongoDB ODM - if: matrix.php == '7.1' + if: startsWith(matrix.php, '7.1') run: | composer remove --dev --no-progress --no-update --ansi \ doctrine/mongodb-odm \ - doctrine/mongodb-odm-bundle + doctrine/mongodb-odm-bundle \ + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.1') || startsWith(matrix.php, '7.2') || startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=default-no-legacy --no-interaction --colors + fi + + phpunit-lowest-deps: + name: PHPUnit (PHP ${{ matrix.php }}) (lowest dependencies) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + strategy: + matrix: + php: + - '7.3' + - '7.4-rc' + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always + + behat-lowest-deps: + name: Behat (PHP ${{ matrix.php }}) (lowest dependencies) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + strategy: + matrix: + php: + - '7.3' + - '7.4-rc' + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=default-no-legacy --no-interaction --colors + fi + + phpunit-postgresql: + name: PHPUnit (PHP ${{ matrix.php }}) (PostgreSQL) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + postgres: + image: postgres:10-alpine + env: + POSTGRES_DB: api_platform_test + POSTGRES_PASSWORD: hk7dFAByeQVVxpLtmZ6GVUzP + POSTGRES_USER: api-platform + options: >- + --mount type=tmpfs,destination=/var/lib/postgresql/data + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: postgres + DATABASE_URL: postgres://api-platform:hk7dFAByeQVVxpLtmZ6GVUzP@postgres/api_platform_test + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install PHP extensions + run: | + apk add \ + $PHPIZE_DEPS \ + postgresql-dev \ + ; + docker-php-ext-install -j$(nproc) \ + pdo_pgsql \ + ; + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always + + behat-postgresql: + name: Behat (PHP ${{ matrix.php }}) (PostgreSQL) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + postgres: + image: postgres:10-alpine + env: + POSTGRES_DB: api_platform_test + POSTGRES_PASSWORD: hk7dFAByeQVVxpLtmZ6GVUzP + POSTGRES_USER: api-platform + options: >- + --mount type=tmpfs,destination=/var/lib/postgresql/data + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: postgres + DATABASE_URL: postgres://api-platform:hk7dFAByeQVVxpLtmZ6GVUzP@postgres/api_platform_test + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install PHP extensions + run: | + apk add \ + $PHPIZE_DEPS \ + postgresql-dev \ + ; + docker-php-ext-install -j$(nproc) \ + pdo_pgsql \ + ; + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --profile=postgres --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=postgres-no-legacy --no-interaction --colors + fi + + phpunit-mysql: + name: PHPUnit (PHP ${{ matrix.php }}) (MySQL) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + mysql: + image: mysql:5.7 + env: + MYSQL_DATABASE: api_platform_test + MYSQL_PASSWORD: LUhGR5tJ7WA2gbGumknCYBcB + MYSQL_RANDOM_ROOT_PASSWORD: 'yes' + MYSQL_USER: api-platform + options: >- + --mount type=tmpfs,destination=/var/lib/mysql + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: mysql + DATABASE_URL: mysql://api-platform:LUhGR5tJ7WA2gbGumknCYBcB@mysql/api_platform_test + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install PHP extensions + run: | + apk add \ + $PHPIZE_DEPS \ + ; + docker-php-ext-install -j$(nproc) \ + pdo_mysql \ + ; + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always + + behat-mysql: + name: Behat (PHP ${{ matrix.php }}) (MySQL) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + mysql: + image: mysql:5.7 + env: + MYSQL_DATABASE: api_platform_test + MYSQL_PASSWORD: LUhGR5tJ7WA2gbGumknCYBcB + MYSQL_RANDOM_ROOT_PASSWORD: 'yes' + MYSQL_USER: api-platform + options: >- + --mount type=tmpfs,destination=/var/lib/mysql + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: mysql + DATABASE_URL: mysql://api-platform:LUhGR5tJ7WA2gbGumknCYBcB@mysql/api_platform_test + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install PHP extensions + run: | + apk add \ + $PHPIZE_DEPS \ + ; + docker-php-ext-install -j$(nproc) \ + pdo_mysql \ + ; + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex - name: Update project dependencies - run: composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=default-no-legacy --no-interaction --colors + fi + + phpunit-mongodb: + name: PHPUnit (PHP ${{ matrix.php }}) (MongoDB) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + mongo: + image: mongo:4 + options: >- + --health-cmd "mongo --quiet --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: mongodb + MONGODB_URL: mongodb://mongo:27017 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always --configuration phpunit_mongodb.xml + + behat-mongodb: + name: Behat (PHP ${{ matrix.php }}) (MongoDB) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + mongo: + image: mongo:4 + options: >- + --health-cmd "mongo --quiet --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: mongodb + MONGODB_URL: mongodb://mongo:27017 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --profile=mongodb --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=mongodb-no-legacy --no-interaction --colors + fi + + phpunit-elasticsearch: + name: PHPUnit (PHP ${{ matrix.php }}) (Elasticsearch) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:6.8.3 # https://github.com/elastic/elasticsearch/issues/43627 + env: + discovery.type: single-node + options: >- + --health-cmd "curl -fsSL http://127.0.0.1:9200/_cluster/health | grep -q '\"status\":[[:space:]]*\"green\"'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: elasticsearch + ELASTICSEARCH_URL: http://elasticsearch:9200 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always + + behat-elasticsearch: + name: Behat (PHP ${{ matrix.php }}) (Elasticsearch) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:6.8.3 # https://github.com/elastic/elasticsearch/issues/43627 + env: + discovery.type: single-node + options: >- + --health-cmd "curl -fsSL http://127.0.0.1:9200/_cluster/health | grep -q '\"status\":[[:space:]]*\"green\"'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + APP_ENV: elasticsearch + ELASTICSEARCH_URL: http://elasticsearch:9200 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Enable legacy integrations + if: startsWith(matrix.php, '7.3') + run: echo '::set-env name=LEGACY::1' + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi + - name: Run Behat tests + run: | + if [ "$LEGACY" = '1' ]; then + vendor/bin/behat --format=progress --profile=elasticsearch --no-interaction --colors + else + vendor/bin/behat --format=progress --profile=elasticsearch-no-legacy --no-interaction --colors + fi + + phpunit-no-deprecations: + name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations) + runs-on: ubuntu-latest + container: + image: php:${{ matrix.php }}-alpine + options: >- + --tmpfs /tmp:exec + strategy: + matrix: + php: + - '7.3' + fail-fast: false + env: + SYMFONY_DEPRECATIONS_HELPER: max[total]=5 # 5 deprecation notices from FOSUserBundle + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install system packages + run: | + apk add \ + unzip \ + - name: Install mongodb PHP extension + run: | + apk add $PHPIZE_DEPS + pecl install mongodb-$EXT_MONGODB_VERSION + docker-php-ext-enable mongodb + - name: Disable PHP memory limit + run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini + - name: Install Composer + run: wget -qO - https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_INSTALLER_COMMIT/web/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet + - name: Install Symfony Flex + run: | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex + - name: Update project dependencies + run: | + mkdir -p /tmp/api-platform/core/vendor + ln -s /tmp/api-platform/core/vendor vendor + composer update --no-progress --no-suggest --ansi + - name: Clear test app cache + run: | + mkdir -p /tmp/api-platform/core/var + ln -s /tmp/api-platform/core/var tests/Fixtures/app/var + tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests run: vendor/bin/phpunit --colors=always + continue-on-error: true diff --git a/.travis.yml b/.travis.yml index 4176e93d174..b284e3d06a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,45 +1,47 @@ language: php +env: + global: + - SYMFONY_REQUIRE='^3.4 || ^4.0' + cache: directories: - - $HOME/.composer/cache + - $HOME/.composer/cache/files - $HOME/.npm .steps: - - &add-composer-bin-dir-to-path | - export PATH="$PATH:$HOME/.composer/vendor/bin" - &clear-test-app-cache | tests/Fixtures/app/console cache:clear + - &create-test-database | + tests/Fixtures/app/console doctrine:database:create - &disable-php-memory-limit | echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - &disable-xdebug-php-extension | phpenv config-rm xdebug.ini || echo "xdebug not available" - &enable-mongodb-php-extension | echo "extension=mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/mongodb.ini - - &run-behat-tests | - vendor/bin/behat --format=progress --no-interaction --colors - - &run-phpunit-tests | - vendor/bin/phpunit --colors=always + - &install-symfony-flex | + composer global require --prefer-dist --no-progress --no-suggest --ansi \ + symfony/flex - &update-project-dependencies | composer update --no-progress --no-suggest --ansi - &validate-openapi-v2-json | - tests/Fixtures/app/console api:swagger:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json + npx swagger-cli validate <(tests/Fixtures/app/console api:swagger:export) - &validate-openapi-v2-yaml | - tests/Fixtures/app/console api:swagger:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml + npx swagger-cli validate <(tests/Fixtures/app/console api:swagger:export --yaml) - &validate-openapi-v3-json | - tests/Fixtures/app/console api:openapi:export --spec-version 3 > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json + npx swagger-cli validate <(tests/Fixtures/app/console api:openapi:export --spec-version 3) - &validate-openapi-v3-yaml | - tests/Fixtures/app/console api:openapi:export --spec-version 3 --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml + npx swagger-cli validate <(tests/Fixtures/app/console api:openapi:export --spec-version 3 --yaml) jobs: include: - name: '(PHP 7.1)' php: '7.1' before_install: - - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - | composer remove --dev --no-progress --no-update --ansi \ @@ -49,9 +51,6 @@ jobs: before_script: - *clear-test-app-cache script: - - *run-phpunit-tests - - *clear-test-app-cache - - *run-behat-tests - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json @@ -63,15 +62,12 @@ jobs: - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache script: - - *run-phpunit-tests - - *clear-test-app-cache - - *run-behat-tests - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json @@ -83,15 +79,30 @@ jobs: - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache script: - - *run-phpunit-tests + - *validate-openapi-v2-json + - *validate-openapi-v2-yaml + - *validate-openapi-v3-json + - *validate-openapi-v3-yaml + + - name: '(PHP 7.4-rc)' + php: '7.4snapshot' + env: LEGACY=0 + before_install: + - *enable-mongodb-php-extension + - *disable-xdebug-php-extension + - *disable-php-memory-limit + - *install-symfony-flex + install: + - *update-project-dependencies + before_script: - *clear-test-app-cache - - *run-behat-tests + script: - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json @@ -103,134 +114,134 @@ jobs: - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi before_script: - *clear-test-app-cache script: - - *run-phpunit-tests - - *clear-test-app-cache - - *run-behat-tests - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - - php: '7.3' - env: APP_ENV=postgres - services: - - postgresql + - name: '(PHP 7.4-rc) (lowest dependencies)' + php: '7.4snapshot' + env: LEGACY=0 before_install: - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - - *update-project-dependencies + - composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi before_script: - *clear-test-app-cache - - psql --command 'CREATE DATABASE api_platform_test;' --username postgres script: - - *run-phpunit-tests - - *clear-test-app-cache - - vendor/bin/behat --format=progress --profile=postgres --no-interaction - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - - php: '7.3' - env: APP_ENV=mysql + - name: '(PHP 7.3) (PostgreSQL)' + php: '7.3' + env: >- + APP_ENV=postgres + DATABASE_URL=postgres://postgres:@localhost/api_platform_test + addons: + postgresql: '10' services: - - mysql + - postgresql before_install: - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache - - mysql --execute 'CREATE DATABASE api_platform_test;' + - *create-test-database script: - - *run-phpunit-tests - - *clear-test-app-cache - - *run-behat-tests - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - - php: '7.3' - env: APP_ENV=mongodb + - name: '(PHP 7.3) (MySQL)' + php: '7.3' + env: >- + APP_ENV=mysql + DATABASE_URL=mysql://travis:@localhost/api_platform_test services: - - mongodb + - mysql before_install: + - sudo systemctl stop mysql + - sudo mv /var/lib/mysql /var/ramfs/mysql + - sudo ln -s /var/ramfs/mysql /var/lib/mysql + - | + { + echo '[mysqld]'; + echo 'innodb_flush_log_at_trx_commit=0'; + } | sudo tee /etc/mysql/conf.d/performance.cnf + - sudo systemctl start mysql - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache + - *create-test-database script: - - vendor/bin/phpunit --configuration phpunit_mongodb.xml - - *clear-test-app-cache - - vendor/bin/behat --format=progress --profile=mongodb --no-interaction - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - - php: '7.3' - env: APP_ENV=elasticsearch + - name: '(PHP 7.3) (MongoDB)' + php: '7.3' + env: APP_ENV=mongodb + services: + - mongodb before_install: - - sudo apt-get purge --auto-remove elasticsearch - - wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - - - echo 'deb https://artifacts.elastic.co/packages/6.x/apt stable main' | sudo tee -a /etc/apt/sources.list.d/elastic-6.x.list - - sudo apt-get update - - sudo apt-get install elasticsearch - - sudo service elasticsearch start - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache script: - - *run-phpunit-tests - - *clear-test-app-cache - - vendor/bin/behat --format=progress --profile=elasticsearch --no-interaction - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - - php: '7.3' - env: SYMFONY_DEPRECATIONS_HELPER=max[total]=5 # 5 deprecation notices from FOSUserBundle + - name: '(PHP 7.3) (Elasticsearch)' + php: '7.3' + env: APP_ENV=elasticsearch before_install: + - sudo apt-get purge --auto-remove elasticsearch + - wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - + - echo 'deb https://artifacts.elastic.co/packages/6.x/apt stable main' | sudo tee -a /etc/apt/sources.list.d/elastic-6.x.list + - sudo apt-get update + - sudo apt-get install elasticsearch + - sudo systemctl start elasticsearch - *enable-mongodb-php-extension - *disable-xdebug-php-extension - *disable-php-memory-limit - - *add-composer-bin-dir-to-path + - *install-symfony-flex install: - *update-project-dependencies before_script: - *clear-test-app-cache script: - - *run-phpunit-tests - - *clear-test-app-cache - - *run-behat-tests - *validate-openapi-v2-json - *validate-openapi-v2-yaml - *validate-openapi-v3-json - *validate-openapi-v3-yaml - allow_failures: - - env: SYMFONY_DEPRECATIONS_HELPER=max[total]=5 # 5 deprecation notices from FOSUserBundle fast_finish: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a1fa17ab58..84fcbb9c6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.6.0 + +* MongoDB: Possibility to add execute options (aggregate command fields) for a resource, like `allowDiskUse` (#3144) + ## 2.5.0 * Fix BC-break when using short-syntax notation for `access_control` @@ -32,7 +36,7 @@ * Add basic infrastructure for cursor-based pagination (#2532) * Change ExistsFilter syntax to `exists[property]`, old syntax still supported see #2243, fixes it's behavior on GraphQL (also related #2640). * Pagination with subresources (#2698) -* Improve search filter id's managment (#1844) +* Improve search filter id's management (#1844) * Add support of name converter in filters (#2751, #2897), filter signature in abstract methods has changed see b42dfd198b1644904fd6a684ab2cedaf530254e3 * Ability to change the Vary header via `cacheHeaders` attributes of a resource (#2758) * Ability to use the Query object in a paginator (#2493) @@ -149,7 +153,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 2.4.2 -* Fix a dependency injection injection problem in `FilterEagerLoadingExtension` +* Fix a dependency injection problem in `FilterEagerLoadingExtension` * Improve performance by adding a `NoOpScalarNormalizer` handling scalar values ## 2.4.1 @@ -420,7 +424,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Persist data in Doctrine DataPersister only if needed * Fix identifiers handling in GraphQL mutations * Fix client-side ID creation or update when using GraphQL mutations -* Fix an error that was occuring when the Expression Language component wasn't installed +* Fix an error that was occurring when the Expression Language component wasn't installed * Update the `ChainSubresourceDataProvider` class to take into account `RestrictedDataProviderInterface` ## 2.2.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d491efdebe..dfdabf24917 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,11 +18,6 @@ Then, if it appears that it's a real bug, you may report it using GitHub by foll > _NOTE:_ Don't hesitate giving as much information as you can (OS, PHP version extensions...) -### Security Issues - -If you find a security issue, send a mail to Kévin Dunglas . **Please do not report security problems -publicly**. We will disclose details of the issue and credit you after having released a new version including a fix. - ## Pull Requests ### Writing a Pull Request @@ -33,7 +28,7 @@ See [the dedicated documentation entry](https://api-platform.com/docs/extra/rele ### Matching Coding Standards The API Platform project follows [Symfony coding standards](https://symfony.com/doc/current/contributing/code/standards.html). -But don't worry, you can fix CS issues automatically using the [PHP CS Fixer](http://cs.sensiolabs.org/) tool: +But don't worry, you can fix CS issues automatically using the [PHP CS Fixer](https://cs.sensiolabs.org/) tool: ```shell php-cs-fixer.phar fix diff --git a/README.md b/README.md index 99249dc9701..44238cdd08d 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ API Platform Core is an easy to use and powerful system to create [hypermedia-dr It is a component of the [API Platform framework](https://api-platform.com) and it can be integrated with [the Symfony framework](https://symfony.com) using the bundle distributed with the library. -It natively supports popular open formats including [JSON for Linked Data (JSON-LD)](http://json-ld.org), [Hydra Core Vocabulary](http://www.hydra-cg.com), [OpenAPI v2 (formerly Swagger) and v3](https://www.openapis.org), [HAL](http://stateless.co/hal_specification.html) and [HTTP Problem](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-03). +It natively supports popular open formats including [JSON for Linked Data (JSON-LD)](https://json-ld.org), [Hydra Core Vocabulary](https://www.hydra-cg.com), [OpenAPI v2 (formerly Swagger) and v3](https://www.openapis.org), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08) and [Problem Details](https://tools.ietf.org/html/rfc7807). Build a working and fully-featured CRUD API in minutes. Leverage the awesome features of the tool to develop complex and high performance API-first projects. Extend or override everything you want. +[![GitHub Actions](https://github.com/api-platform/core/workflows/CI/badge.svg)](https://github.com/api-platform/core/actions?workflow=CI) [![Travis CI](https://travis-ci.org/api-platform/core.svg?branch=master)](https://travis-ci.org/api-platform/core) [![CircleCI](https://circleci.com/gh/api-platform/core/tree/master.svg?style=shield)](https://circleci.com/gh/api-platform/core/tree/master) [![AppVeyor](https://ci.appveyor.com/api/projects/status/grwuyprts3wdqx5l/branch/master?svg=true)](https://ci.appveyor.com/project/dunglas/dunglasapibundle/branch/master) diff --git a/behat.yml.dist b/behat.yml.dist index da7ee687d09..6a5a36f2649 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -36,10 +36,16 @@ default: symfony2: ~ 'Behatch\Extension': ~ +default-no-legacy: + suites: + default: + filters: + tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@legacy' + postgres: suites: default: false - postgres: + postgres: &postgres-suite contexts: - 'CommandContext' - 'DoctrineContext': @@ -61,10 +67,18 @@ postgres: filters: tags: '~@sqlite&&~@mongodb&&~@elasticsearch' +postgres-no-legacy: + suites: + default: false + postgres: + <<: *postgres-suite + filters: + tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@legacy' + mongodb: suites: default: false - mongodb: + mongodb: &mongodb-suite contexts: - 'CommandContext' - 'DoctrineContext': @@ -86,10 +100,18 @@ mongodb: filters: tags: '~@sqlite&&~@elasticsearch&&~@!mongodb' +mongodb-no-legacy: + suites: + default: false + mongodb: + <<: *mongodb-suite + filters: + tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@legacy' + elasticsearch: suites: default: false - elasticsearch: + elasticsearch: &elasticsearch-suite paths: - '%paths.base%/features/elasticsearch' contexts: @@ -105,6 +127,14 @@ elasticsearch: filters: tags: '@elasticsearch' +elasticsearch-no-legacy: + suites: + default: false + elasticsearch: + <<: *elasticsearch-suite + filters: + tags: '@elasticsearch&&~@legacy' + default-coverage: suites: default: @@ -127,13 +157,12 @@ default-coverage: - 'CoverageContext' - 'Behat\MinkExtension\Context\MinkContext' - 'Behatch\Context\RestContext' - filters: - tags: '~@postgres&&~@mongodb&&~@elasticsearch' mongodb-coverage: suites: default: false mongodb: + <<: *mongodb-suite contexts: - 'CommandContext' - 'DoctrineContext': @@ -153,15 +182,12 @@ mongodb-coverage: - 'CoverageContext' - 'Behat\MinkExtension\Context\MinkContext' - 'Behatch\Context\RestContext' - filters: - tags: '~@sqlite&&~@elasticsearch&&~@!mongodb' elasticsearch-coverage: suites: default: false elasticsearch: - paths: - - '%paths.base%/features/elasticsearch' + <<: *elasticsearch-suite contexts: - 'CommandContext' - 'ElasticsearchContext': @@ -173,5 +199,3 @@ elasticsearch-coverage: - 'CoverageContext' - 'Behatch\Context\RestContext' - 'Behat\MinkExtension\Context\MinkContext' - filters: - tags: '@elasticsearch' diff --git a/composer.json b/composer.json index 374f51cca23..8c55e696b37 100644 --- a/composer.json +++ b/composer.json @@ -33,12 +33,13 @@ "behat/symfony2-extension": "^2.1.1", "behatch/contexts": "3.1.0", "doctrine/annotations": "^1.7", + "doctrine/common": "^2.11", "doctrine/data-fixtures": "^1.2.2", "doctrine/doctrine-bundle": "^1.8", "doctrine/doctrine-cache-bundle": "^1.3.5", - "doctrine/mongodb-odm": "^2.0@rc", - "doctrine/mongodb-odm-bundle": "^4.0@rc", - "doctrine/orm": "^2.6.3", + "doctrine/mongodb-odm": "^2.0", + "doctrine/mongodb-odm-bundle": "^4.0", + "doctrine/orm": "^2.6.4", "elasticsearch/elasticsearch": "^6.0", "friendsofsymfony/user-bundle": "^2.2@dev", "guzzlehttp/guzzle": "^6.0", @@ -49,7 +50,7 @@ "phpdocumentor/type-resolver": "^0.3 || ^0.4", "phpspec/prophecy": "^1.8", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.11.10", + "phpstan/phpstan": "^0.11.10 <0.11.17", "phpstan/phpstan-doctrine": "^0.11.5", "phpstan/phpstan-phpunit": "^0.11.2", "phpstan/phpstan-symfony": "^0.11.6", @@ -77,20 +78,19 @@ "symfony/mercure-bundle": "*", "symfony/messenger": "^4.3", "symfony/phpunit-bridge": "^4.3@dev", - "symfony/routing": "^3.4 || ^4.0", + "symfony/routing": "^3.4 || ^4.3", "symfony/security-bundle": "^3.4 || ^4.0", "symfony/security-core": "^4.3", "symfony/twig-bundle": "^3.4 || ^4.0", "symfony/validator": "^3.4 || ^4.0", "symfony/web-profiler-bundle": "^4.2", "symfony/yaml": "^3.4 || ^4.0", - "twig/twig": "^1.41 || ^2.10", + "twig/twig": "^1.42.3 || ^2.12", "webonyx/graphql-php": ">=0.13.1 <1.0" }, "conflict": { "doctrine/common": "<2.7", - "doctrine/mongodb-odm": "<2.0", - "doctrine/mongodb-odm-bundle": "4.0.0-RC2" + "doctrine/mongodb-odm": "<2.0" }, "suggest": { "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index 093a19015a9..ac8a8f36520 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -31,14 +31,353 @@ Feature: GraphQL mutation support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "data.__type.fields[2].name" should contain "delete" - And the JSON node "data.__type.fields[2].description" should match '/^Deletes a [A-z0-9]+.$/' - And the JSON node "data.__type.fields[2].type.name" should match "/^delete[A-z0-9]+Payload$/" - And the JSON node "data.__type.fields[2].type.kind" should be equal to "OBJECT" - And the JSON node "data.__type.fields[2].args[0].name" should be equal to "input" - And the JSON node "data.__type.fields[2].args[0].type.kind" should be equal to "NON_NULL" - And the JSON node "data.__type.fields[2].args[0].type.ofType.name" should match "/^delete[A-z0-9]+Input$/" - And the JSON node "data.__type.fields[2].args[0].type.ofType.kind" should be equal to "INPUT_OBJECT" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "__type" + ], + "properties": { + "__type": { + "type": "object", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "object", + "required": [ + "name", + "description", + "type", + "args" + ], + "properties": { + "name": { + "pattern": "^create[A-z0-9]+$" + }, + "description": { + "pattern": "^Creates a [A-z0-9]+.$" + }, + "type": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^create[A-z0-9]+Payload$" + }, + "kind": { + "enum": ["OBJECT"] + } + } + }, + "args": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "enum": ["input"] + }, + "type": { + "type": "object", + "required": [ + "kind", + "ofType" + ], + "properties": { + "kind": { + "enum": ["NON_NULL"] + }, + "ofType": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^create[A-z0-9]+Input$" + }, + "kind": { + "enum": ["INPUT_OBJECT"] + } + } + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "required": [ + "name", + "description", + "type", + "args" + ], + "properties": { + "name": { + "pattern": "^update[A-z0-9]+$" + }, + "description": { + "pattern": "^Updates a [A-z0-9]+.$" + }, + "type": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^update[A-z0-9]+Payload$" + }, + "kind": { + "enum": ["OBJECT"] + } + } + }, + "args": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "enum": ["input"] + }, + "type": { + "type": "object", + "required": [ + "kind", + "ofType" + ], + "properties": { + "kind": { + "enum": ["NON_NULL"] + }, + "ofType": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^update[A-z0-9]+Input$" + }, + "kind": { + "enum": ["INPUT_OBJECT"] + } + } + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "required": [ + "name", + "description", + "type", + "args" + ], + "properties": { + "name": { + "pattern": "^delete[A-z0-9]+$" + }, + "description": { + "pattern": "^Deletes a [A-z0-9]+.$" + }, + "type": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^delete[A-z0-9]+Payload$" + }, + "kind": { + "enum": ["OBJECT"] + } + } + }, + "args": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "enum": ["input"] + }, + "type": { + "type": "object", + "required": [ + "kind", + "ofType" + ], + "properties": { + "kind": { + "enum": ["NON_NULL"] + }, + "ofType": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^delete[A-z0-9]+Input$" + }, + "kind": { + "enum": ["INPUT_OBJECT"] + } + } + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "required": [ + "name", + "description", + "type", + "args" + ], + "properties": { + "name": { + "pattern": "^(?!create|update|delete)[A-z0-9]+$" + }, + "description": { + "pattern": "^(?!Create|Update|Delete)[A-z0-9]+s a [A-z0-9]+.$" + }, + "type": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^(?!create|update|delete)[A-z0-9]+Payload$" + }, + "kind": { + "enum": ["OBJECT"] + } + } + }, + "args": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": [ + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "enum": ["input"] + }, + "type": { + "type": "object", + "required": [ + "kind", + "ofType" + ], + "properties": { + "kind": { + "enum": ["NON_NULL"] + }, + "ofType": { + "type": "object", + "required": [ + "name", + "kind" + ], + "properties": { + "name": { + "pattern": "^(?!create|update|delete)[A-z0-9]+Input$" + }, + "kind": { + "enum": ["INPUT_OBJECT"] + } + } + } + } + } + } + } + ] + } + } + } + ] + } + } + } + } + } + } + } + } + """ Scenario: Create an item When I send the following GraphQL request: diff --git a/features/json/relation.feature b/features/json/relation.feature new file mode 100644 index 00000000000..7c39dc75f71 --- /dev/null +++ b/features/json/relation.feature @@ -0,0 +1,228 @@ +Feature: JSON relations support + In order to use a hypermedia API + As a client software developer + I need to be able to update relations between resources + + @createSchema + Scenario: Create a third level + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/third_levels" with body: + """ + { + "level": 3 + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ThirdLevel", + "@id": "/third_levels/1", + "@type": "ThirdLevel", + "fourthLevel": null, + "badFourthLevel": null, + "id": 1, + "level": 3, + "test": true + } + """ + + Scenario: Create a new relation + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/relation_embedders" with body: + """ + { + "anotherRelated": { + "symfony": "laravel" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelationEmbedder", + "@id": "/relation_embedders/1", + "@type": "RelationEmbedder", + "krondstadt": "Krondstadt", + "anotherRelated": { + "@id": "/related_dummies/1", + "@type": "https://schema.org/Product", + "symfony": "laravel", + "thirdLevel": null + }, + "related": null + } + """ + + Scenario: Update the relation with a new one + When I add "Content-Type" header equal to "application/json" + And I send a "PUT" request to "/relation_embedders/1" with body: + """ + { + "anotherRelated": { + "symfony": "laravel2" + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelationEmbedder", + "@id": "/relation_embedders/1", + "@type": "RelationEmbedder", + "krondstadt": "Krondstadt", + "anotherRelated": { + "@id": "/related_dummies/2", + "@type": "https://schema.org/Product", + "symfony": "laravel2", + "thirdLevel": null + }, + "related": null + } + """ + + Scenario: Update an embedded relation using an IRI + When I add "Content-Type" header equal to "application/json" + And I send a "PUT" request to "/relation_embedders/1" with body: + """ + { + "anotherRelated": { + "id": "/related_dummies/1", + "symfony": "API Platform" + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelationEmbedder", + "@id": "/relation_embedders/1", + "@type": "RelationEmbedder", + "krondstadt": "Krondstadt", + "anotherRelated": { + "@id": "/related_dummies/1", + "@type": "https://schema.org/Product", + "symfony": "API Platform", + "thirdLevel": null + }, + "related": null + } + """ + + Scenario: Update an embedded relation using plain identifiers + When I add "Content-Type" header equal to "application/json" + And I send a "PUT" request to "/relation_embedders/1" with body: + """ + { + "anotherRelated": { + "id": 1, + "symfony": "API Platform 2" + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelationEmbedder", + "@id": "/relation_embedders/1", + "@type": "RelationEmbedder", + "krondstadt": "Krondstadt", + "anotherRelated": { + "@id": "/related_dummies/1", + "@type": "https://schema.org/Product", + "symfony": "API Platform 2", + "thirdLevel": null + }, + "related": null + } + """ + + Scenario: Create a related dummy with a relation + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/related_dummies" with body: + """ + { + "thirdLevel": "1" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/RelatedDummy", + "@id": "/related_dummies/3", + "@type": "https://schema.org/Product", + "id": 3, + "name": null, + "symfony": "symfony", + "dummyDate": null, + "thirdLevel": { + "@id": "/third_levels/1", + "@type": "ThirdLevel", + "fourthLevel": null + }, + "relatedToDummyFriend": [], + "dummyBoolean": null, + "embeddedDummy": [], + "age": null + } + """ + + Scenario: Passing a (valid) plain identifier on a relation + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/dummies" with body: + """ + { + "relatedDummy": "1", + "relatedDummies": [ + "1" + ], + "name": "Dummy with plain relations" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/Dummy", + "@id": "/dummies/1", + "@type": "Dummy", + "description": null, + "dummy": null, + "dummyBoolean": null, + "dummyDate": null, + "dummyFloat": null, + "dummyPrice": null, + "relatedDummy": "/related_dummies/1", + "relatedDummies": [ + "/related_dummies/1" + ], + "jsonData": [], + "arrayData": [], + "name_converted": null, + "relatedOwnedDummy": null, + "relatedOwningDummy": null, + "id": 1, + "name": "Dummy with plain relations", + "alias": null, + "foo": null + } + """ diff --git a/features/jsonapi/filtering.feature b/features/jsonapi/filtering.feature index fd00f6a35fe..ce8f209caa5 100644 --- a/features/jsonapi/filtering.feature +++ b/features/jsonapi/filtering.feature @@ -23,6 +23,14 @@ Feature: JSON API filter handling And the JSON should be valid according to the JSON API schema And the JSON node "data" should have 0 elements + Scenario: Apply filters and pagination at the same time + When I send a "GET" request to "/dummies?filter[name]=foo&page[page]=2" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should be valid according to the JSON API schema + Then the JSON node "meta.currentPage" should be a number + Then the JSON node "meta.currentPage" should be equal to "2" + Scenario: Apply property filter based on the 'fields' Given there are 2 dummy property objects When I send a "GET" request to "/dummy_properties?fields[DummyProperty]=id,foo,bar" diff --git a/features/integration/fos_user.feature b/features/legacy/fos_user.feature similarity index 99% rename from features/integration/fos_user.feature rename to features/legacy/fos_user.feature index 82dc25441d8..c665839303c 100644 --- a/features/integration/fos_user.feature +++ b/features/legacy/fos_user.feature @@ -1,3 +1,4 @@ +@legacy Feature: FOSUser integration In order to use FOSUserBundle As an API software developer diff --git a/features/integration/nelmio_api_doc.feature b/features/legacy/nelmio_api_doc.feature similarity index 98% rename from features/integration/nelmio_api_doc.feature rename to features/legacy/nelmio_api_doc.feature index 385474eaf4f..846547c2169 100644 --- a/features/integration/nelmio_api_doc.feature +++ b/features/legacy/nelmio_api_doc.feature @@ -1,3 +1,4 @@ +@legacy Feature: NelmioApiDoc integration In order to use NelmioApiDocBundle As an API software developer diff --git a/features/main/relation.feature b/features/main/relation.feature index 8ad186cf050..fa3fe30c50a 100644 --- a/features/main/relation.feature +++ b/features/main/relation.feature @@ -365,66 +365,6 @@ Feature: Relations support And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - Scenario: Create a new relation (json) - When I add "Content-Type" header equal to "application/json" - And I send a "POST" request to "/relation_embedders" with body: - """ - { - "anotherRelated": { - "symfony": "laravel" - } - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/RelationEmbedder", - "@id": "/relation_embedders/3", - "@type": "RelationEmbedder", - "krondstadt": "Krondstadt", - "anotherRelated": { - "@id": "/related_dummies/4", - "@type": "https://schema.org/Product", - "symfony": "laravel", - "thirdLevel": null - }, - "related": null - } - """ - - Scenario: Update the relation with a new one (json) - When I add "Content-Type" header equal to "application/json" - And I send a "PUT" request to "/relation_embedders/3" with body: - """ - { - "anotherRelated": { - "symfony": "laravel2" - } - } - """ - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/RelationEmbedder", - "@id": "/relation_embedders/3", - "@type": "RelationEmbedder", - "krondstadt": "Krondstadt", - "anotherRelated": { - "@id": "/related_dummies/5", - "@type": "https://schema.org/Product", - "symfony": "laravel2", - "thirdLevel": null - }, - "related": null - } - """ - Scenario: Update an embedded relation When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/relation_embedders/2" with body: @@ -456,37 +396,6 @@ Feature: Relations support } """ - Scenario: Create a related dummy with a relation (json) - When I add "Content-Type" header equal to "application/json" - And I send a "POST" request to "/related_dummies" with body: - """ - {"thirdLevel": "1"} - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/RelatedDummy", - "@id": "/related_dummies/6", - "@type": "https://schema.org/Product", - "id": 6, - "name": null, - "symfony": "symfony", - "dummyDate": null, - "thirdLevel": { - "@id": "/third_levels/1", - "@type": "ThirdLevel", - "fourthLevel": null - }, - "relatedToDummyFriend": [], - "dummyBoolean": null, - "embeddedDummy": [], - "age": null - } - """ - Scenario: Issue #1222 Given there are people having pets When I add "Content-Type" header equal to "application/ld+json" @@ -519,45 +428,6 @@ Feature: Relations support } """ - Scenario: Passing a (valid) plain identifier on a relation - When I add "Content-Type" header equal to "application/json" - And I send a "POST" request to "/dummies" with body: - """ - { - "relatedDummy": "1", - "relatedDummies": ["1"], - "name": "Dummy with plain relations" - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context":"/contexts/Dummy", - "@id":"/dummies/2", - "@type":"Dummy", - "description":null, - "dummy":null, - "dummyBoolean":null, - "dummyDate":null, - "dummyFloat":null, - "dummyPrice":null, - "relatedDummy":"/related_dummies/1", - "relatedDummies":["/related_dummies/1"], - "jsonData":[], - "arrayData":[], - "name_converted":null, - "relatedOwnedDummy": null, - "relatedOwningDummy": null, - "id":2, - "name":"Dummy with plain relations", - "alias":null, - "foo":null - } - """ - Scenario: Eager load relations should not be duplicated Given there is an order with same customer and recipient When I add "Content-Type" header equal to "application/ld+json" diff --git a/features/main/subresource.feature b/features/main/subresource.feature index 16fa448cb03..909fd5398ac 100644 --- a/features/main/subresource.feature +++ b/features/main/subresource.feature @@ -7,7 +7,7 @@ Feature: Subresource support Scenario: Get subresource one to one relation Given there is an answer "42" to the question "What's the answer to the Ultimate Question of Life, the Universe and Everything?" When I send a "GET" request to "/questions/1/answer" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the JSON should be equal to: """ @@ -24,10 +24,10 @@ Feature: Subresource support } """ - Scenario: Get a non existant subresource + Scenario: Get a non existent subresource Given there is an answer "42" to the question "What's the answer to the Ultimate Question of Life, the Universe and Everything?" When I send a "GET" request to "/questions/999999/answer" - And the response status code should be 404 + Then the response status code should be 404 And the response should be in JSON Scenario: Get recursive subresource one to many relation @@ -56,7 +56,7 @@ Feature: Subresource support Scenario: Get the subresource relation collection Given there is a dummy object with a fourth level relation When I send a "GET" request to "/dummies/1/related_dummies" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -132,7 +132,7 @@ Feature: Subresource support Scenario: Get filtered embedded relation subresource collection When I send a "GET" request to "/dummies/1/related_dummies?name=Hello" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -195,7 +195,7 @@ Feature: Subresource support Scenario: Get the subresource relation item When I send a "GET" request to "/dummies/1/related_dummies/2" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -254,7 +254,7 @@ Feature: Subresource support Scenario: Get the embedded relation subresource item at the fourth level When I send a "GET" request to "/dummies/1/related_dummies/1/third_level/fourth_level" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -272,7 +272,7 @@ Feature: Subresource support Scenario: Get offers subresource from aggregate offers subresource Given I have a product with offers When I send a "GET" request to "/dummy_products/2/offers/1/offers" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -296,7 +296,7 @@ Feature: Subresource support Scenario: Get offers subresource from aggregate offers subresource When I send a "GET" request to "/dummy_aggregate_offers/1/offers" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -321,7 +321,7 @@ Feature: Subresource support Scenario: The recipient of the person's greetings should be empty Given there is a person named "Alice" greeting with a "hello" message When I send a "GET" request to "/people/1/sent_greetings" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: @@ -411,7 +411,7 @@ Feature: Subresource support Scenario: Recursive resource When I send a "GET" request to "/dummy_products/2" - And the response status code should be 200 + Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be equal to: diff --git a/features/push_relations/push.feature b/features/push_relations/push.feature index 6c14442a0a9..fe4a7d1de75 100644 --- a/features/push_relations/push.feature +++ b/features/push_relations/push.feature @@ -9,9 +9,9 @@ Feature: Push relations using HTTP/2 Given there are 2 dummy objects with relatedDummy When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/dummies" - Then the header "Link" should be equal to '; rel="preload",; rel="preload",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"' + Then the header "Link" should be equal to '; rel="preload"; as="fetch",; rel="preload"; as="fetch",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"' Scenario: Push the relations of an item When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/dummies/1" - Then the header "Link" should be equal to '; rel="preload",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"' + Then the header "Link" should be equal to '; rel="preload"; as="fetch",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2f0a298995d..03838853d3b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ [ diff --git a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php index ffc67528d4a..b50337c8baf 100644 --- a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php +++ b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php @@ -13,8 +13,10 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Common\Util; +use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\PropertyNotFoundException; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -34,6 +36,7 @@ trait IdentifierManagerTrait * @param int|string $id * * @throws PropertyNotFoundException + * @throws InvalidIdentifierException */ private function normalizeIdentifiers($id, ObjectManager $manager, string $resourceClass): array { @@ -76,11 +79,15 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou $doctrineTypeName = $doctrineClassMetadata->getTypeOfField($propertyName); - if ($isOrm && null !== $doctrineTypeName && DBALType::hasType($doctrineTypeName)) { - $identifier = DBALType::getType($doctrineTypeName)->convertToPHPValue($identifier, $platform); - } - if ($isOdm && null !== $doctrineTypeName && MongoDbType::hasType($doctrineTypeName)) { - $identifier = MongoDbType::getType($doctrineTypeName)->convertToPHPValue($identifier); + try { + if ($isOrm && null !== $doctrineTypeName && DBALType::hasType($doctrineTypeName)) { + $identifier = DBALType::getType($doctrineTypeName)->convertToPHPValue($identifier, $platform); + } + if ($isOdm && null !== $doctrineTypeName && MongoDbType::hasType($doctrineTypeName)) { + $identifier = MongoDbType::getType($doctrineTypeName)->convertToPHPValue($identifier); + } + } catch (ConversionException $e) { + throw new InvalidIdentifierException(sprintf('Invalid value "%s" provided for an identifier.', $propertyName), $e->getCode(), $e); } $identifiers[$propertyName] = $identifier; diff --git a/src/Bridge/Doctrine/MongoDbOdm/CollectionDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/CollectionDataProvider.php index da4dac68ba5..b8820439d8a 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/CollectionDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/CollectionDataProvider.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; @@ -32,14 +33,16 @@ final class CollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface { private $managerRegistry; + private $resourceMetadataFactory; private $collectionExtensions; /** * @param AggregationCollectionExtensionInterface[] $collectionExtensions */ - public function __construct(ManagerRegistry $managerRegistry, iterable $collectionExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, ResourceMetadataFactoryInterface $resourceMetadataFactory, iterable $collectionExtensions = []) { $this->managerRegistry = $managerRegistry; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->collectionExtensions = $collectionExtensions; } @@ -72,6 +75,10 @@ public function getCollection(string $resourceClass, string $operationName = nul } } - return $aggregationBuilder->hydrate($resourceClass)->execute(); + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $attribute = $resourceMetadata->getCollectionOperationAttribute($operationName, 'doctrine_mongodb', [], true); + $executeOptions = $attribute['execute_options'] ?? []; + + return $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions); } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php b/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php index 75215bbeb61..80399ffbc20 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Paginator; use ApiPlatform\Core\DataProvider\Pagination; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\DocumentManager; @@ -33,11 +34,13 @@ final class PaginationExtension implements AggregationResultCollectionExtensionInterface { private $managerRegistry; + private $resourceMetadataFactory; private $pagination; - public function __construct(ManagerRegistry $managerRegistry, Pagination $pagination) + public function __construct(ManagerRegistry $managerRegistry, ResourceMetadataFactoryInterface $resourceMetadataFactory, Pagination $pagination) { $this->managerRegistry = $managerRegistry; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->pagination = $pagination; } @@ -113,7 +116,11 @@ public function getResult(Builder $aggregationBuilder, string $resourceClass, st throw new RuntimeException(sprintf('The manager for "%s" must be an instance of "%s".', $resourceClass, DocumentManager::class)); } - return new Paginator($aggregationBuilder->execute(), $manager->getUnitOfWork(), $resourceClass, $aggregationBuilder->getPipeline()); + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $attribute = $resourceMetadata->getCollectionOperationAttribute($operationName, 'doctrine_mongodb', [], true); + $executeOptions = $attribute['execute_options'] ?? []; + + return new Paginator($aggregationBuilder->execute($executeOptions), $manager->getUnitOfWork(), $resourceClass, $aggregationBuilder->getPipeline()); } private function addCountToContext(Builder $aggregationBuilder, array $context): array diff --git a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php index 9dcc53f35e1..12db2dda163 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php @@ -22,6 +22,7 @@ use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; @@ -38,14 +39,16 @@ final class ItemDataProvider implements DenormalizedIdentifiersAwareItemDataProv use IdentifierManagerTrait; private $managerRegistry; + private $resourceMetadataFactory; private $itemExtensions; /** * @param AggregationItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = []) { $this->managerRegistry = $managerRegistry; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->itemExtensions = $itemExtensions; @@ -95,6 +98,10 @@ public function getItem(string $resourceClass, $id, string $operationName = null } } - return $aggregationBuilder->hydrate($resourceClass)->execute()->current() ?: null; + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $attribute = $resourceMetadata->getItemOperationAttribute($operationName, 'doctrine_mongodb', [], true); + $executeOptions = $attribute['execute_options'] ?? []; + + return $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions)->current() ?: null; } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php index 2c490deb456..beceaf0a22d 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/SubresourceDataProvider.php @@ -24,6 +24,7 @@ use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\DocumentManager; @@ -43,6 +44,7 @@ final class SubresourceDataProvider implements SubresourceDataProviderInterface use IdentifierManagerTrait; private $managerRegistry; + private $resourceMetadataFactory; private $collectionExtensions; private $itemExtensions; @@ -50,9 +52,10 @@ final class SubresourceDataProvider implements SubresourceDataProviderInterface * @param AggregationCollectionExtensionInterface[] $collectionExtensions * @param AggregationItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = []) { $this->managerRegistry = $managerRegistry; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->collectionExtensions = $collectionExtensions; @@ -80,7 +83,11 @@ public function getSubresource(string $resourceClass, array $identifiers, array throw new ResourceClassNotSupportedException('The given resource class is not a subresource.'); } - $aggregationBuilder = $this->buildAggregation($identifiers, $context, $repository->createAggregationBuilder(), \count($context['identifiers'])); + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $attribute = $resourceMetadata->getSubresourceOperationAttribute($operationName, 'doctrine_mongodb', [], true); + $executeOptions = $attribute['execute_options'] ?? []; + + $aggregationBuilder = $this->buildAggregation($identifiers, $context, $executeOptions, $repository->createAggregationBuilder(), \count($context['identifiers'])); if (true === $context['collection']) { foreach ($this->collectionExtensions as $extension) { @@ -98,7 +105,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array } } - $iterator = $aggregationBuilder->hydrate($resourceClass)->execute(); + $iterator = $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions); return $context['collection'] ? $iterator->toArray() : ($iterator->current() ?: null); } @@ -106,7 +113,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array /** * @throws RuntimeException */ - private function buildAggregation(array $identifiers, array $context, Builder $previousAggregationBuilder, int $remainingIdentifiers, Builder $topAggregationBuilder = null): Builder + private function buildAggregation(array $identifiers, array $context, array $executeOptions, Builder $previousAggregationBuilder, int $remainingIdentifiers, Builder $topAggregationBuilder = null): Builder { if ($remainingIdentifiers <= 0) { return $previousAggregationBuilder; @@ -154,9 +161,9 @@ private function buildAggregation(array $identifiers, array $context, Builder $p } // Recurse aggregations - $aggregation = $this->buildAggregation($identifiers, $context, $aggregation, --$remainingIdentifiers, $topAggregationBuilder); + $aggregation = $this->buildAggregation($identifiers, $context, $executeOptions, $aggregation, --$remainingIdentifiers, $topAggregationBuilder); - $results = $aggregation->execute()->toArray(); + $results = $aggregation->execute($executeOptions)->toArray(); $in = array_reduce($results, function ($in, $result) use ($previousAssociationProperty) { return $in + array_map(function ($result) { return $result['_id']; diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php index e12ee385d76..fe6f2bc185e 100644 --- a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php +++ b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php @@ -83,7 +83,7 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati return $clauseBody; } - return array_merge($clauseBody, [ + return array_merge_recursive($clauseBody, [ 'bool' => [ 'must' => $searches, ], diff --git a/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php b/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php index 2a44b68c106..71722613f13 100644 --- a/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php +++ b/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php @@ -100,5 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } else { $output->writeln($content); } + + return 0; } } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index cf39090b833..5090dcf3ecf 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -191,6 +191,29 @@ private function registerCommonConfiguration(ContainerBuilder $container, array if ($config['name_converter']) { $container->setAlias('api_platform.name_converter', $config['name_converter']); } + $container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? [])); + } + + private function normalizeDefaults(array $defaults): array + { + $normalizedDefaults = ['attributes' => []]; + $rootLevelOptions = [ + 'description', + 'iri', + 'item_operations', + 'collection_operations', + 'graphql', + ]; + + foreach ($defaults as $option => $value) { + if (\in_array($option, $rootLevelOptions, true)) { + $normalizedDefaults[$option] = $value; + } else { + $normalizedDefaults['attributes'][$option] = $value; + } + } + + return $normalizedDefaults; } private function registerMetadataConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void @@ -312,6 +335,8 @@ private function registerOAuthConfiguration(ContainerBuilder $container, array $ */ private function registerSwaggerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void { + $container->setParameter('api_platform.swagger.versions', $config['swagger']['versions']); + if (empty($config['swagger']['versions'])) { return; } @@ -325,7 +350,6 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']); } - $container->setParameter('api_platform.swagger.versions', $config['swagger']['versions']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); } @@ -347,6 +371,10 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a $loader->load('jsonld.xml'); $loader->load('hydra.xml'); + if (!$container->has('api_platform.json_schema.schema_factory')) { + $container->removeDefinition('api_platform.hydra.json_schema.schema_factory'); + } + if (!$docEnabled) { $container->removeDefinition('api_platform.hydra.listener.response.add_link_header'); } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPass.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPass.php index 3d0d929d52b..d6b3806ecc2 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPass.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPass.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler; +use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Util\AnnotationFilterExtractorTrait; use ApiPlatform\Core\Util\ReflectionClassRecursiveIterator; use Doctrine\Common\Annotations\Reader; @@ -23,7 +24,7 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; /** - * Injects filters. + * Registers filter services from {@see ApiFilter} annotations. * * @internal * @@ -43,7 +44,7 @@ final class AnnotationFilterPass implements CompilerPassInterface /** * {@inheritdoc} */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $resourceClassDirectories = $container->getParameter('api_platform.resource_class_directories'); @@ -55,28 +56,41 @@ public function process(ContainerBuilder $container) /** * @throws InvalidArgumentException */ - private function createFilterDefinitions(\ReflectionClass $reflectionClass, ContainerBuilder $container): void + private function createFilterDefinitions(\ReflectionClass $resourceReflectionClass, ContainerBuilder $container): void { - $reader = $this->reader ?? $this->reader = $container->get('annotation_reader'); + $this->reader ?? $this->reader = $container->get('annotation_reader'); - foreach ($this->readFilterAnnotations($reflectionClass, $reader) as $id => [$arguments, $filterClass]) { + foreach ($this->readFilterAnnotations($resourceReflectionClass, $this->reader) as $id => [$arguments, $filterClass]) { if ($container->has($id)) { continue; } - if ($container->has($filterClass) && ($definition = $container->findDefinition($filterClass))->isAbstract()) { - $definition = new ChildDefinition($definition->getClass()); - } elseif ($reflectionClass = $container->getReflectionClass($filterClass, false)) { - $definition = new Definition($reflectionClass->getName()); - $definition->setAutoconfigured(true); - } else { + if (null === $filterReflectionClass = $container->getReflectionClass($filterClass, false)) { throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $filterClass, $id)); } + if ($container->has($filterClass) && ($parentDefinition = $container->findDefinition($filterClass))->isAbstract()) { + $definition = new ChildDefinition($parentDefinition->getClass()); + } else { + $definition = new Definition($filterReflectionClass->getName()); + $definition->setAutoconfigured(true); + } + $definition->addTag(self::TAG_FILTER_NAME); $definition->setAutowired(true); + $parameterNames = []; + if (null !== $constructorReflectionMethod = $filterReflectionClass->getConstructor()) { + foreach ($constructorReflectionMethod->getParameters() as $reflectionParameter) { + $parameterNames[$reflectionParameter->name] = true; + } + } + foreach ($arguments as $key => $value) { + if (!isset($parameterNames[$key])) { + throw new InvalidArgumentException(sprintf('Class "%s" does not have argument "$%s".', $filterClass, $key)); + } + $definition->setArgument("$$key", $value); } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index b49c43b28ed..ca8a5b0d4f0 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection; +use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata; use ApiPlatform\Core\Exception\FilterValidationException; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -33,6 +34,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; /** * The configuration of the bundle. @@ -128,13 +130,41 @@ public function getConfigTreeBuilder() ->canBeDisabled() ->addDefaultsIfNotSet() ->children() - ->booleanNode('enabled')->defaultTrue()->info('To enable or disable pagination for all resource collections by default.')->end() - ->booleanNode('partial')->defaultFalse()->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.')->end() - ->booleanNode('client_enabled')->defaultFalse()->info('To allow the client to enable or disable the pagination.')->end() - ->booleanNode('client_items_per_page')->defaultFalse()->info('To allow the client to set the number of items per page.')->end() - ->booleanNode('client_partial')->defaultFalse()->info('To allow the client to enable or disable partial pagination.')->end() - ->integerNode('items_per_page')->defaultValue(30)->info('The default number of items per page.')->end() - ->integerNode('maximum_items_per_page')->defaultNull()->info('The maximum number of items per page.')->end() + ->booleanNode('enabled') + ->setDeprecated('The use of the `collection.pagination.enabled` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_enabled` instead.') + ->defaultTrue() + ->info('To enable or disable pagination for all resource collections by default.') + ->end() + ->booleanNode('partial') + ->setDeprecated('The use of the `collection.pagination.partial` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_partial` instead.') + ->defaultFalse() + ->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.') + ->end() + ->booleanNode('client_enabled') + ->setDeprecated('The use of the `collection.pagination.client_enabled` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_enabled` instead.') + ->defaultFalse() + ->info('To allow the client to enable or disable the pagination.') + ->end() + ->booleanNode('client_items_per_page') + ->setDeprecated('The use of the `collection.pagination.client_items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_items_per_page` instead.') + ->defaultFalse() + ->info('To allow the client to set the number of items per page.') + ->end() + ->booleanNode('client_partial') + ->setDeprecated('The use of the `collection.pagination.client_partial` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_partial` instead.') + ->defaultFalse() + ->info('To allow the client to enable or disable partial pagination.') + ->end() + ->integerNode('items_per_page') + ->setDeprecated('The use of the `collection.pagination.items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_items_per_page` instead.') + ->defaultValue(30) + ->info('The default number of items per page.') + ->end() + ->integerNode('maximum_items_per_page') + ->setDeprecated('The use of the `collection.pagination.maximum_items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_maximum_items_per_page` instead.') + ->defaultNull() + ->info('The maximum number of items per page.') + ->end() ->scalarNode('page_parameter_name')->defaultValue('page')->cannotBeEmpty()->info('The default name of the parameter handling the page number.')->end() ->scalarNode('enabled_parameter_name')->defaultValue('pagination')->cannotBeEmpty()->info('The name of the query parameter to enable or disable pagination.')->end() ->scalarNode('items_per_page_parameter_name')->defaultValue('itemsPerPage')->cannotBeEmpty()->info('The name of the query parameter to set the number of items per page.')->end() @@ -179,6 +209,8 @@ public function getConfigTreeBuilder() 'jsonld' => ['mime_types' => ['application/ld+json']], ]); + $this->addDefaultsSection($rootNode); + return $treeBuilder; } @@ -311,16 +343,30 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void ->arrayNode('http_cache') ->addDefaultsIfNotSet() ->children() - ->booleanNode('etag')->defaultTrue()->info('Automatically generate etags for API responses.')->end() - ->integerNode('max_age')->defaultNull()->info('Default value for the response max age.')->end() - ->integerNode('shared_max_age')->defaultNull()->info('Default value for the response shared (proxy) max age.')->end() + ->booleanNode('etag') + ->setDeprecated('The use of the `http_cache.etag` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.etag` instead.') + ->defaultTrue() + ->info('Automatically generate etags for API responses.') + ->end() + ->integerNode('max_age') + ->setDeprecated('The use of the `http_cache.max_age` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.max_age` instead.') + ->defaultNull() + ->info('Default value for the response max age.') + ->end() + ->integerNode('shared_max_age') + ->setDeprecated('The use of the `http_cache.shared_max_age` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.shared_max_age` instead.') + ->defaultNull() + ->info('Default value for the response shared (proxy) max age.') + ->end() ->arrayNode('vary') + ->setDeprecated('The use of the `http_cache.vary` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.vary` instead.') ->defaultValue(['Accept']) ->prototype('scalar')->end() ->info('Default values of the "Vary" HTTP header.') ->end() ->booleanNode('public')->defaultNull()->info('To make all responses public by default.')->end() ->arrayNode('invalidation') + ->setDeprecated('The use of the `http_cache.invalidation` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.invalidation` instead.') ->info('Enable the tags-based cache invalidation system.') ->canBeEnabled() ->children() @@ -494,4 +540,28 @@ private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, ar ->end() ->end(); } + + private function addDefaultsSection(ArrayNodeDefinition $rootNode): void + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); + $defaultsNode = $rootNode->children()->arrayNode('defaults'); + + $defaultsNode + ->ignoreExtraKeys() + ->beforeNormalization() + ->always(function (array $defaults) use ($nameConverter) { + $normalizedDefaults = []; + foreach ($defaults as $option => $value) { + $option = $nameConverter->normalize($option); + $normalizedDefaults[$option] = $value; + } + + return $normalizedDefaults; + }); + + foreach (ApiResource::CONFIGURABLE_DEFAULTS as $attribute) { + $snakeCased = $nameConverter->normalize($attribute); + $defaultsNode->children()->variableNode($snakeCased); + } + } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index fa300e4a700..d129fd7019b 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -24,11 +24,13 @@ + + @@ -36,6 +38,7 @@ + @@ -46,7 +49,6 @@ parent="api_platform.doctrine_mongodb.odm.collection_data_provider" class="ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\CollectionDataProvider"> - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index 701a7ac9878..07617218bf5 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -53,12 +53,12 @@ - + - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/metadata/annotation.xml b/src/Bridge/Symfony/Bundle/Resources/config/metadata/annotation.xml index 70556d4f8bc..6440d8c921e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/metadata/annotation.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/metadata/annotation.xml @@ -14,6 +14,7 @@ + %api_platform.defaults% diff --git a/src/Bridge/Symfony/Bundle/Resources/config/metadata/xml.xml b/src/Bridge/Symfony/Bundle/Resources/config/metadata/xml.xml index 9836ed19d6a..157a764d913 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/metadata/xml.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/metadata/xml.xml @@ -26,7 +26,7 @@ - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml b/src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml index 1657eb99b86..7344a04a22e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml @@ -18,6 +18,7 @@ + %api_platform.defaults% diff --git a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js index 1600643d778..95f2b63ed5e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js +++ b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js @@ -45,6 +45,7 @@ window.onload = function() { spec: data.spec, dom_id: '#swagger-ui', validatorUrl: null, + oauth2RedirectUrl: data.oauth.redirectUrl, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset, diff --git a/src/Bridge/Symfony/Bundle/Resources/public/swagger-ui/oauth2-redirect.html b/src/Bridge/Symfony/Bundle/Resources/public/swagger-ui/oauth2-redirect.html new file mode 100644 index 00000000000..fb68399d264 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Resources/public/swagger-ui/oauth2-redirect.html @@ -0,0 +1,67 @@ + + + + + + diff --git a/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig b/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig index 8e2e6925f77..c516c4df42c 100644 --- a/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig +++ b/src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig @@ -10,8 +10,9 @@ {% endblock %} + {% set oauth_data = {'oauth': swagger_data.oauth|merge({'redirectUrl' : absolute_url(asset('bundles/apiplatform/swagger-ui/oauth2-redirect.html')) })} %} {# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #} - + diff --git a/src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index e52cade819c..4283b7336d1 100644 --- a/src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -59,7 +59,7 @@ public static function assertJsonContains($subset, bool $checkForObjectIdentity /** * Asserts that the retrieved JSON is equal to $json. * - * Both values are canonicalized before the comparision. + * Both values are canonicalized before the comparison. * * @param array|string $json */ diff --git a/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php b/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php index e1269ddee57..caef5608d2c 100644 --- a/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php +++ b/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php @@ -71,7 +71,7 @@ protected static function createClient(array $kernelOptions = [], array $default } /** - * Finds the IRI of a resource item macthing the resource class and the specified criteria. + * Finds the IRI of a resource item matching the resource class and the specified criteria. */ protected function findIriBy(string $resourceClass, array $criteria): ?string { diff --git a/src/Bridge/Symfony/Bundle/Test/Response.php b/src/Bridge/Symfony/Bundle/Test/Response.php index 36d1dcbec1e..663bfbcc833 100644 --- a/src/Bridge/Symfony/Bundle/Test/Response.php +++ b/src/Bridge/Symfony/Bundle/Test/Response.php @@ -170,7 +170,7 @@ public function getKernelResponse(): HttpFoundationResponse } /** - * Returns the internal BrowserKit reponse. + * Returns the internal BrowserKit response. */ public function getBrowserKitResponse(): BrowserKitResponse { diff --git a/src/Bridge/Symfony/Messenger/ContextStamp.php b/src/Bridge/Symfony/Messenger/ContextStamp.php new file mode 100644 index 00000000000..40afe1b1384 --- /dev/null +++ b/src/Bridge/Symfony/Messenger/ContextStamp.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Messenger; + +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * An envelope stamp with context which related to a message. + * + * @experimental + * + * @author Sergii Pavlenko + */ +final class ContextStamp implements StampInterface +{ + private $context; + + public function __construct(array $context = []) + { + $this->context = $context; + } + + /** + * Get the context related to a message. + */ + public function getContext(): array + { + return $this->context; + } +} diff --git a/src/Bridge/Symfony/Messenger/DataPersister.php b/src/Bridge/Symfony/Messenger/DataPersister.php index 525cd71b304..faebcad48e8 100644 --- a/src/Bridge/Symfony/Messenger/DataPersister.php +++ b/src/Bridge/Symfony/Messenger/DataPersister.php @@ -75,7 +75,10 @@ public function supports($data, array $context = []): bool */ public function persist($data, array $context = []) { - $envelope = $this->dispatch($data); + $envelope = $this->dispatch( + (new Envelope($data)) + ->with(new ContextStamp($context)) + ); $handledStamp = $envelope->last(HandledStamp::class); if (!$handledStamp instanceof HandledStamp) { diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php index 48698755754..6d97441197f 100644 --- a/src/EventListener/ReadListener.php +++ b/src/EventListener/ReadListener.php @@ -109,7 +109,7 @@ public function onKernelRequest(GetResponseEvent $event): void $data = $this->getSubresourceData($identifiers, $attributes, $context); } } catch (InvalidIdentifierException $e) { - throw new NotFoundHttpException('Not found, because of an invalid identifier configuration', $e); + throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); } if (null === $data) { diff --git a/src/EventListener/SerializeListener.php b/src/EventListener/SerializeListener.php index 54731fc678f..c0669484056 100644 --- a/src/EventListener/SerializeListener.php +++ b/src/EventListener/SerializeListener.php @@ -101,7 +101,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); foreach ($resourcesToPush as $resourceToPush) { - $linkProvider = $linkProvider->withLink(new Link('preload', $resourceToPush)); + $linkProvider = $linkProvider->withLink((new Link('preload', $resourceToPush))->withAttribute('as', 'fetch')); } $request->attributes->set('_links', $linkProvider); } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 604c654605b..c2fd9eb4990 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -18,6 +18,6 @@ * * @author Kévin Dunglas */ -interface ExceptionInterface +interface ExceptionInterface extends \Throwable { } diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php index 18a1e2ad18a..d6ab052d085 100644 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php +++ b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php @@ -30,7 +30,7 @@ final class SecurityPostDenormalizeStage implements SecurityPostDenormalizeStage private $resourceMetadataFactory; private $resourceAccessChecker; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ?ResourceAccessCheckerInterface $resourceAccessChecker) { $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceAccessChecker = $resourceAccessChecker; @@ -44,6 +44,7 @@ public function __invoke(string $resourceClass, string $operationName, array $co $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); $isGranted = $resourceMetadata->getGraphqlAttribute($operationName, 'security_post_denormalize', null, true); + if (null === $isGranted) { // Backward compatibility $isGranted = $resourceMetadata->getGraphqlAttribute($operationName, 'access_control', null, true); @@ -52,6 +53,10 @@ public function __invoke(string $resourceClass, string $operationName, array $co } } + if (null !== $isGranted && null === $this->resourceAccessChecker) { + throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); + } + if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { return; } diff --git a/src/GraphQl/Resolver/Stage/SecurityStage.php b/src/GraphQl/Resolver/Stage/SecurityStage.php index 36fa2e37619..b3afa035618 100644 --- a/src/GraphQl/Resolver/Stage/SecurityStage.php +++ b/src/GraphQl/Resolver/Stage/SecurityStage.php @@ -30,7 +30,7 @@ final class SecurityStage implements SecurityStageInterface private $resourceMetadataFactory; private $resourceAccessChecker; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ?ResourceAccessCheckerInterface $resourceAccessChecker) { $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceAccessChecker = $resourceAccessChecker; @@ -44,6 +44,11 @@ public function __invoke(string $resourceClass, string $operationName, array $co $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); $isGranted = $resourceMetadata->getGraphqlAttribute($operationName, 'security', null, true); + + if (null !== $isGranted && null === $this->resourceAccessChecker) { + throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); + } + if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { return; } diff --git a/src/JsonApi/EventListener/TransformFilteringParametersListener.php b/src/JsonApi/EventListener/TransformFilteringParametersListener.php index d5d11433eac..6aba979ba61 100644 --- a/src/JsonApi/EventListener/TransformFilteringParametersListener.php +++ b/src/JsonApi/EventListener/TransformFilteringParametersListener.php @@ -29,12 +29,12 @@ public function onKernelRequest(GetResponseEvent $event): void $request = $event->getRequest(); if ( 'jsonapi' !== $request->getRequestFormat() || - null === ($filters = $request->query->get('filter')) || - !\is_array($filters) + null === ($filterParameter = $request->query->get('filter')) || + !\is_array($filterParameter) ) { return; } - - $request->attributes->set('_api_filters', $filters); + $filters = $request->attributes->get('_api_filters', []); + $request->attributes->set('_api_filters', array_merge($filterParameter, $filters)); } } diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 1c5e300e87a..dc70791dccc 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -206,7 +206,9 @@ private function buildDefinitionName(string $resourceClass, string $format = 'js $prefix = $resourceMetadata->getShortName(); if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) { - $prefix .= ':'.md5($inputOrOutputClass); + $parts = explode('\\', $inputOrOutputClass); + $shortName = end($parts); + $prefix .= ':'.$shortName; } if (isset($this->distinctFormats[$format])) { diff --git a/src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php b/src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php index d941e0e4918..c04364b5b9b 100644 --- a/src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php +++ b/src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php @@ -27,11 +27,13 @@ final class AnnotationResourceMetadataFactory implements ResourceMetadataFactory { private $reader; private $decorated; + private $defaults; - public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null) + public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null, array $defaults = []) { $this->reader = $reader; $this->decorated = $decorated; + $this->defaults = $defaults + ['attributes' => []]; } /** @@ -78,16 +80,18 @@ private function handleNotFound(?ResourceMetadata $parentPropertyMetadata, strin private function createMetadata(ApiResource $annotation, ResourceMetadata $parentResourceMetadata = null): ResourceMetadata { + $attributes = (null === $annotation->attributes && [] === $this->defaults['attributes']) ? null : (array) $annotation->attributes + $this->defaults['attributes']; + if (!$parentResourceMetadata) { return new ResourceMetadata( $annotation->shortName, - $annotation->description, - $annotation->iri, - $annotation->itemOperations, - $annotation->collectionOperations, - $annotation->attributes, + $annotation->description ?? $this->defaults['description'] ?? null, + $annotation->iri ?? $this->defaults['iri'] ?? null, + $annotation->itemOperations ?? $this->defaults['item_operations'] ?? null, + $annotation->collectionOperations ?? $this->defaults['collection_operations'] ?? null, + $attributes, $annotation->subresourceOperations, - $annotation->graphql + $annotation->graphql ?? $this->defaults['graphql'] ?? null ); } diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php index 369380b4364..fee663cf97c 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php @@ -27,11 +27,13 @@ final class ExtractorResourceMetadataFactory implements ResourceMetadataFactoryI { private $extractor; private $decorated; + private $defaults; - public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null) + public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null, array $defaults = []) { $this->extractor = $extractor; $this->decorated = $decorated; + $this->defaults = $defaults + ['attributes' => []]; } /** @@ -52,6 +54,13 @@ public function create(string $resourceClass): ResourceMetadata return $this->handleNotFound($parentResourceMetadata, $resourceClass); } + $resource['description'] = $resource['description'] ?? $this->defaults['description'] ?? null; + $resource['iri'] = $resource['iri'] ?? $this->defaults['iri'] ?? null; + $resource['itemOperations'] = $resource['itemOperations'] ?? $this->defaults['item_operations'] ?? null; + $resource['collectionOperations'] = $resource['collectionOperations'] ?? $this->defaults['collection_operations'] ?? null; + $resource['graphql'] = $resource['graphql'] ?? $this->defaults['graphql'] ?? null; + $resource['attributes'] = (null === $resource['attributes'] && [] === $this->defaults['attributes']) ? null : (array) $resource['attributes'] + $this->defaults['attributes']; + return $this->update($parentResourceMetadata ?: new ResourceMetadata(), $resource); } diff --git a/src/Operation/DashPathSegmentNameGenerator.php b/src/Operation/DashPathSegmentNameGenerator.php index 418bfdee154..07acc591ecc 100644 --- a/src/Operation/DashPathSegmentNameGenerator.php +++ b/src/Operation/DashPathSegmentNameGenerator.php @@ -27,9 +27,7 @@ final class DashPathSegmentNameGenerator implements PathSegmentNameGeneratorInte */ public function getSegmentName(string $name, bool $collection = true): string { - $name = $this->dashize($name); - - return $collection ? Inflector::pluralize($name) : $name; + return $collection ? $this->dashize(Inflector::pluralize($name)) : $this->dashize($name); } private function dashize(string $string): string diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index c6cfca00105..7c067184f88 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -472,7 +472,7 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $ } /** - * Gets a valid context for property name collection / property metadata factories. + * Gets the options for the property name collection / property metadata factories. */ protected function getFactoryOptions(array $context): array { diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index 73ed3bef63e..a38a69eaccd 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -94,7 +94,7 @@ public function createFromRequest(Request $request, bool $normalization, array $ } foreach ($resourceMetadata->getItemOperations() as $operation) { - if ('PATCH' === $operation['method'] && \in_array('application/merge-patch+json', $operation['input_formats']['json'] ?? [], true)) { + if ('PATCH' === ($operation['method'] ?? '') && \in_array('application/merge-patch+json', $operation['input_formats']['json'] ?? [], true)) { $context['skip_null_values'] = true; break; diff --git a/src/Swagger/Serializer/ApiGatewayNormalizer.php b/src/Swagger/Serializer/ApiGatewayNormalizer.php index 6ac6a699665..f726881982d 100644 --- a/src/Swagger/Serializer/ApiGatewayNormalizer.php +++ b/src/Swagger/Serializer/ApiGatewayNormalizer.php @@ -31,7 +31,9 @@ final class ApiGatewayNormalizer implements NormalizerInterface, CacheableSuppor public const API_GATEWAY = 'api_gateway'; private $documentationNormalizer; - private $defaultContext = [self::API_GATEWAY => false]; + private $defaultContext = [ + self::API_GATEWAY => false, + ]; public function __construct(NormalizerInterface $documentationNormalizer, $defaultContext = []) { @@ -66,19 +68,19 @@ public function normalize($object, $format = null, array $context = []) if (!preg_match('/^[a-zA-Z0-9._$-]+$/', $parameter['name'])) { unset($data['paths'][$path][$operation]['parameters'][$key]); } - if (isset($parameter['schema']['$ref']) && !preg_match('/^#\/definitions\/[A-z]+$/', $parameter['schema']['$ref'])) { - $data['paths'][$path][$operation]['parameters'][$key]['schema']['$ref'] = str_replace(['-', '_'], '', $parameter['schema']['$ref']); + if (isset($parameter['schema']['$ref']) && $this->isLocalRef($parameter['schema']['$ref'])) { + $data['paths'][$path][$operation]['parameters'][$key]['schema']['$ref'] = $this->normalizeRef($parameter['schema']['$ref']); } } $data['paths'][$path][$operation]['parameters'] = array_values($data['paths'][$path][$operation]['parameters']); } if (isset($options['responses'])) { foreach ($options['responses'] as $statusCode => $response) { - if (isset($response['schema']['items']['$ref']) && !preg_match('/^#\/definitions\/[A-z]+$/', $response['schema']['items']['$ref'])) { - $data['paths'][$path][$operation]['responses'][$statusCode]['schema']['items']['$ref'] = str_replace(['-', '_'], '', $response['schema']['items']['$ref']); + if (isset($response['schema']['items']['$ref']) && $this->isLocalRef($response['schema']['items']['$ref'])) { + $data['paths'][$path][$operation]['responses'][$statusCode]['schema']['items']['$ref'] = $this->normalizeRef($response['schema']['items']['$ref']); } - if (isset($response['schema']['$ref']) && !preg_match('/^#\/definitions\/[A-z]+$/', $response['schema']['$ref'])) { - $data['paths'][$path][$operation]['responses'][$statusCode]['schema']['$ref'] = str_replace(['-', '_'], '', $response['schema']['$ref']); + if (isset($response['schema']['$ref']) && $this->isLocalRef($response['schema']['$ref'])) { + $data['paths'][$path][$operation]['responses'][$statusCode]['schema']['$ref'] = $this->normalizeRef($response['schema']['$ref']); } } } @@ -93,19 +95,19 @@ public function normalize($object, $format = null, array $context = []) if (isset($propertyOptions['readOnly'])) { unset($data['definitions'][$definition]['properties'][$property]['readOnly']); } - if (isset($propertyOptions['$ref']) && !preg_match('/^#\/definitions\/[A-z]+$/', $propertyOptions['$ref'])) { - $data['definitions'][$definition]['properties'][$property]['$ref'] = str_replace(['-', '_'], '', $propertyOptions['$ref']); + if (isset($propertyOptions['$ref']) && $this->isLocalRef($propertyOptions['$ref'])) { + $data['definitions'][$definition]['properties'][$property]['$ref'] = $this->normalizeRef($propertyOptions['$ref']); } - if (isset($propertyOptions['items']['$ref']) && !preg_match('/^#\/definitions\/[A-z]+$/', $propertyOptions['items']['$ref'])) { - $data['definitions'][$definition]['properties'][$property]['items']['$ref'] = str_replace(['-', '_'], '', $propertyOptions['items']['$ref']); + if (isset($propertyOptions['items']['$ref']) && $this->isLocalRef($propertyOptions['items']['$ref'])) { + $data['definitions'][$definition]['properties'][$property]['items']['$ref'] = $this->normalizeRef($propertyOptions['items']['$ref']); } } } // $data['definitions'] is an instance of \ArrayObject foreach (array_keys($data['definitions']->getArrayCopy()) as $definition) { - if (!preg_match('/^[A-z]+$/', (string) $definition)) { - $data['definitions'][str_replace(['-', '_'], '', (string) $definition)] = $data['definitions'][$definition]; + if (!preg_match('/^[0-9A-Za-z]+$/', (string) $definition)) { + $data['definitions'][preg_replace('/[^0-9A-Za-z]/', '', (string) $definition)] = $data['definitions'][$definition]; unset($data['definitions'][$definition]); } } @@ -128,4 +130,20 @@ public function hasCacheableSupportsMethod(): bool { return $this->documentationNormalizer instanceof CacheableSupportsMethodInterface && $this->documentationNormalizer->hasCacheableSupportsMethod(); } + + private function isLocalRef(string $ref): bool + { + return '#/' === substr($ref, 0, 2); + } + + private function normalizeRef(string $ref): string + { + $refParts = explode('/', $ref); + + $schemaName = array_pop($refParts); + $schemaName = preg_replace('/[^0-9A-Za-z]/', '', $schemaName); + $refParts[] = $schemaName; + + return implode('/', $refParts); + } } diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index 475a9750169..7fdc800b180 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -543,15 +543,28 @@ private function addRequestBody(bool $v3, \ArrayObject $pathOperation, \ArrayObj return $pathOperation; } - $pathOperation['parameters'][] = [ - 'name' => lcfirst($resourceShortName), - 'in' => 'body', - 'description' => $description, - ] + $message; + if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) { + $pathOperation['parameters'][] = [ + 'name' => lcfirst($resourceShortName), + 'in' => 'body', + 'description' => $description, + ] + $message; + } return $pathOperation; } + private function hasBodyParameter(array $parameters): bool + { + foreach ($parameters as $parameter) { + if (\array_key_exists('in', $parameter) && 'body' === $parameter['in']) { + return true; + } + } + + return false; + } + private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata): \ArrayObject { $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName); diff --git a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php index ce0f1f11e83..8a7269b6f5c 100644 --- a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php +++ b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Util; use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait; +use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\PropertyNotFoundException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -31,6 +32,7 @@ use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; +use Ramsey\Uuid\Doctrine\UuidType; class IdentifierManagerTraitTest extends TestCase { @@ -204,4 +206,28 @@ private function getDocumentManager(string $resourceClass, array $identifierFiel return $managerProphecy->reveal(); } + + /** + * @group legacy + */ + public function testInvalidIdentifierConversion() + { + DBALType::addType('uuid', UuidType::class); + + $this->expectException(InvalidIdentifierException::class); + $this->expectExceptionMessage('Invalid value "ida" provided for an identifier.'); + + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + 'ida', + ]); + $objectManager = $this->getEntityManager(Dummy::class, [ + 'ida' => [ + 'type' => 'uuid', + ], + ]); + + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + + $identifierManager->normalizeIdentifiers('notanuuid', $objectManager, Dummy::class); + } } diff --git a/tests/Bridge/Doctrine/MongoDbOdm/CollectionDataProviderTest.php b/tests/Bridge/Doctrine/MongoDbOdm/CollectionDataProviderTest.php index 001e4d997a8..2b972521c1a 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/CollectionDataProviderTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/CollectionDataProviderTest.php @@ -17,6 +17,8 @@ use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultCollectionExtensionInterface; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectRepository; @@ -33,13 +35,27 @@ */ class CollectionDataProviderTest extends TestCase { + private $managerRegistryProphecy; + private $resourceMetadataFactoryProphecy; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + parent::setUp(); + + $this->managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + } + public function testGetCollection() { $iterator = $this->prophesize(Iterator::class)->reveal(); $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->hydrate(Dummy::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute()->willReturn($iterator)->shouldBeCalled(); + $aggregationBuilderProphecy->execute([])->willReturn($iterator)->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $repositoryProphecy = $this->prophesize(DocumentRepository::class); @@ -48,13 +64,46 @@ public function testGetCollection() $managerProphecy = $this->prophesize(DocumentManager::class); $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata()); + + $extensionProphecy = $this->prophesize(AggregationCollectionExtensionInterface::class); + $extensionProphecy->applyToCollection($aggregationBuilder, Dummy::class, 'foo', [])->shouldBeCalled(); + + $dataProvider = new CollectionDataProvider($this->managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); + $this->assertEquals($iterator, $dataProvider->getCollection(Dummy::class, 'foo')); + } + + public function testGetCollectionWithExecuteOptions() + { + $iterator = $this->prophesize(Iterator::class)->reveal(); + + $aggregationBuilderProphecy = $this->prophesize(Builder::class); + $aggregationBuilderProphecy->hydrate(Dummy::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); + $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iterator)->shouldBeCalled(); + $aggregationBuilder = $aggregationBuilderProphecy->reveal(); + + $repositoryProphecy = $this->prophesize(DocumentRepository::class); + $repositoryProphecy->createAggregationBuilder()->willReturn($aggregationBuilder)->shouldBeCalled(); + + $managerProphecy = $this->prophesize(DocumentManager::class); + $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); + + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata( + 'Dummy', + null, + null, + null, + ['foo' => ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]]] + )); $extensionProphecy = $this->prophesize(AggregationCollectionExtensionInterface::class); $extensionProphecy->applyToCollection($aggregationBuilder, Dummy::class, 'foo', [])->shouldBeCalled(); - $dataProvider = new CollectionDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new CollectionDataProvider($this->managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals($iterator, $dataProvider->getCollection(Dummy::class, 'foo')); } @@ -69,15 +118,14 @@ public function testAggregationResultExtension() $managerProphecy = $this->prophesize(DocumentManager::class); $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); $extensionProphecy = $this->prophesize(AggregationResultCollectionExtensionInterface::class); $extensionProphecy->applyToCollection($aggregationBuilder, Dummy::class, 'foo', [])->shouldBeCalled(); $extensionProphecy->supportsResult(Dummy::class, 'foo', [])->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($aggregationBuilder, Dummy::class, 'foo', [])->willReturn([])->shouldBeCalled(); - $dataProvider = new CollectionDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new CollectionDataProvider($this->managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getCollection(Dummy::class, 'foo')); } @@ -91,21 +139,19 @@ public function testCannotCreateAggregationBuilder() $managerProphecy = $this->prophesize(DocumentManager::class); $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); - $dataProvider = new CollectionDataProvider($managerRegistryProphecy->reveal()); + $dataProvider = new CollectionDataProvider($this->managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal()); $this->assertEquals([], $dataProvider->getCollection(Dummy::class, 'foo')); } public function testUnsupportedClass() { - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled(); + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled(); $extensionProphecy = $this->prophesize(AggregationResultCollectionExtensionInterface::class); - $dataProvider = new CollectionDataProvider($managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new CollectionDataProvider($this->managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertFalse($dataProvider->supports(Dummy::class, 'foo')); } } diff --git a/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php b/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php index f6b4f383628..9532392f42a 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php @@ -42,24 +42,28 @@ class PaginationExtensionTest extends TestCase { private $managerRegistryProphecy; + private $resourceMetadataFactoryProphecy; + /** + * {@inheritdoc} + */ protected function setUp(): void { parent::setUp(); $this->managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); } public function testApplyToCollection() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_items_per_page' => 40, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'page_parameter_name' => '_page', @@ -71,6 +75,7 @@ public function testApplyToCollection() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -78,14 +83,13 @@ public function testApplyToCollection() public function testApplyToCollectionWithItemPerPageZero() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_items_per_page' => 0, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'items_per_page' => 0, @@ -98,6 +102,7 @@ public function testApplyToCollectionWithItemPerPageZero() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -108,14 +113,13 @@ public function testApplyToCollectionWithItemPerPageZeroAndPage2() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Page should not be greater than 1 if limit is equal to 0'); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_items_per_page' => 0, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'items_per_page' => 0, @@ -129,6 +133,7 @@ public function testApplyToCollectionWithItemPerPageZeroAndPage2() $extension = new PaginationExtension( $this->prophesize(ManagerRegistry::class)->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -139,14 +144,13 @@ public function testApplyToCollectionWithItemPerPageLessThan0() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Limit should not be less than 0'); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_items_per_page' => -20, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'items_per_page' => -20, @@ -160,6 +164,7 @@ public function testApplyToCollectionWithItemPerPageLessThan0() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -167,14 +172,13 @@ public function testApplyToCollectionWithItemPerPageLessThan0() public function testApplyToCollectionWithItemPerPageTooHigh() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => true, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'page_parameter_name' => '_page', @@ -187,6 +191,7 @@ public function testApplyToCollectionWithItemPerPageTooHigh() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -194,14 +199,13 @@ public function testApplyToCollectionWithItemPerPageTooHigh() public function testApplyToCollectionWithGraphql() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => 20, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory); @@ -211,6 +215,7 @@ public function testApplyToCollectionWithGraphql() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -218,14 +223,13 @@ public function testApplyToCollectionWithGraphql() public function testApplyToCollectionWithGraphqlAndCountContext() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => 20, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory); @@ -244,6 +248,7 @@ public function testApplyToCollectionWithGraphqlAndCountContext() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -251,9 +256,8 @@ public function testApplyToCollectionWithGraphqlAndCountContext() public function testApplyToCollectionNoFilters() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory); @@ -263,6 +267,7 @@ public function testApplyToCollectionNoFilters() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -270,9 +275,8 @@ public function testApplyToCollectionNoFilters() public function testApplyToCollectionPaginationDisabled() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'enabled' => false, @@ -285,6 +289,7 @@ public function testApplyToCollectionPaginationDisabled() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -292,9 +297,8 @@ public function testApplyToCollectionPaginationDisabled() public function testApplyToCollectionGraphQlPaginationDisabled() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [], [ 'enabled' => false, @@ -307,6 +311,7 @@ public function testApplyToCollectionGraphQlPaginationDisabled() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -314,14 +319,13 @@ public function testApplyToCollectionGraphQlPaginationDisabled() public function testApplyToCollectionWithMaximumItemsPerPage() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $attributes = [ 'pagination_enabled' => true, 'pagination_client_enabled' => true, 'pagination_maximum_items_per_page' => 80, ]; - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], $attributes)); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'client_enabled' => true, @@ -335,6 +339,7 @@ public function testApplyToCollectionWithMaximumItemsPerPage() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', 'op', $context); @@ -342,14 +347,14 @@ public function testApplyToCollectionWithMaximumItemsPerPage() public function testSupportsResult() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory); $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $this->assertTrue($extension->supportsResult('Foo', 'op')); @@ -357,9 +362,8 @@ public function testSupportsResult() public function testSupportsResultClientNotAllowedToPaginate() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'enabled' => false, @@ -368,6 +372,7 @@ public function testSupportsResultClientNotAllowedToPaginate() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $this->assertFalse($extension->supportsResult('Foo', 'op', ['filters' => ['pagination' => true]])); @@ -375,9 +380,8 @@ public function testSupportsResultClientNotAllowedToPaginate() public function testSupportsResultPaginationDisabled() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [ 'enabled' => false, @@ -385,6 +389,7 @@ public function testSupportsResultPaginationDisabled() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $this->assertFalse($extension->supportsResult('Foo', 'op', ['filters' => ['enabled' => false]])); @@ -392,9 +397,8 @@ public function testSupportsResultPaginationDisabled() public function testSupportsResultGraphQlPaginationDisabled() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $this->resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [])); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory, [], [ 'enabled' => false, @@ -402,6 +406,7 @@ public function testSupportsResultGraphQlPaginationDisabled() $extension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); $this->assertFalse($extension->supportsResult('Foo', 'op', ['filters' => ['enabled' => false], 'graphql_operation_name' => 'op'])); @@ -409,8 +414,7 @@ public function testSupportsResultGraphQlPaginationDisabled() public function testGetResult() { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); $pagination = new Pagination($resourceMetadataFactory); @@ -420,6 +424,8 @@ public function testGetResult() $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($documentManager); + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata()); + $iteratorProphecy = $this->prophesize(Iterator::class); $iteratorProphecy->toArray()->willReturn([ [ @@ -432,7 +438,7 @@ public function testGetResult() ]); $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->execute()->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy->execute([])->willReturn($iteratorProphecy->reveal()); $aggregationBuilderProphecy->getPipeline()->willReturn([ [ '$facet' => [ @@ -449,6 +455,7 @@ public function testGetResult() $paginationExtension = new PaginationExtension( $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, $pagination ); @@ -458,6 +465,65 @@ public function testGetResult() $this->assertInstanceOf(PaginatorInterface::class, $result); } + public function testGetResultWithExecuteOptions() + { + $resourceMetadataFactory = $this->resourceMetadataFactoryProphecy->reveal(); + + $pagination = new Pagination($resourceMetadataFactory); + + $fixturesPath = \dirname((string) (new \ReflectionClass(Dummy::class))->getFileName()); + $config = DoctrineMongoDbOdmSetup::createAnnotationMetadataConfiguration([$fixturesPath], true); + $documentManager = DocumentManager::create(null, $config); + + $this->managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($documentManager); + + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata( + 'Dummy', + null, + null, + null, + ['foo' => ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]]] + )); + + $iteratorProphecy = $this->prophesize(Iterator::class); + $iteratorProphecy->toArray()->willReturn([ + [ + 'count' => [ + [ + 'count' => 9, + ], + ], + ], + ]); + + $aggregationBuilderProphecy = $this->prophesize(Builder::class); + $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy->getPipeline()->willReturn([ + [ + '$facet' => [ + 'results' => [ + ['$skip' => 3], + ['$limit' => 6], + ], + 'count' => [ + ['$count' => 'count'], + ], + ], + ], + ]); + + $paginationExtension = new PaginationExtension( + $this->managerRegistryProphecy->reveal(), + $resourceMetadataFactory, + $pagination + ); + + $result = $paginationExtension->getResult($aggregationBuilderProphecy->reveal(), Dummy::class, 'foo'); + + $this->assertInstanceOf(PartialPaginatorInterface::class, $result); + $this->assertInstanceOf(PaginatorInterface::class, $result); + } + private function mockAggregationBuilder($expectedOffset, $expectedLimit) { $skipProphecy = $this->prophesize(Skip::class); diff --git a/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php index 2f5cee9f086..bba7ade6290 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/ItemDataProviderTest.php @@ -23,6 +23,8 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; @@ -42,6 +44,18 @@ */ class ItemDataProviderTest extends TestCase { + private $resourceMetadataFactoryProphecy; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + parent::setUp(); + + $this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + } + public function testGetItemSingleIdentifier() { $context = ['foo' => 'bar', 'fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -57,7 +71,40 @@ public function testGetItemSingleIdentifier() $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); $aggregationBuilderProphecy->hydrate(Dummy::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute()->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->execute([])->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilder = $aggregationBuilderProphecy->reveal(); + + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + $managerRegistry = $this->getManagerRegistry(Dummy::class, $aggregationBuilder); + + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata()); + + $extensionProphecy = $this->prophesize(AggregationItemExtensionInterface::class); + $extensionProphecy->applyToItem($aggregationBuilder, Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); + + $dataProvider = new ItemDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + + $this->assertEquals($result, $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); + } + + public function testGetItemWithExecuteOptions() + { + $context = ['foo' => 'bar', 'fetch_data' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + + $matchProphecy = $this->prophesize(Match::class); + $matchProphecy->field('id')->willReturn($matchProphecy)->shouldBeCalled(); + $matchProphecy->equals(1)->shouldBeCalled(); + + $iterator = $this->prophesize(Iterator::class); + $result = new \stdClass(); + $iterator->current()->willReturn($result)->shouldBeCalled(); + + $aggregationBuilderProphecy = $this->prophesize(Builder::class); + $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->hydrate(Dummy::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); + $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iterator->reveal())->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ @@ -65,10 +112,17 @@ public function testGetItemSingleIdentifier() ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, $aggregationBuilder); + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata( + 'Dummy', + null, + null, + ['foo' => ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]]] + )); + $extensionProphecy = $this->prophesize(AggregationItemExtensionInterface::class); $extensionProphecy->applyToItem($aggregationBuilder, Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals($result, $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); } @@ -88,7 +142,7 @@ public function testGetItemDoubleIdentifier() $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); $aggregationBuilderProphecy->hydrate(Dummy::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute()->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->execute([])->willReturn($iterator->reveal())->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ @@ -97,11 +151,13 @@ public function testGetItemDoubleIdentifier() ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, $aggregationBuilder); + $this->resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata()); + $context = [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; $extensionProphecy = $this->prophesize(AggregationItemExtensionInterface::class); $extensionProphecy->applyToItem($aggregationBuilder, Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals($result, $dataProvider->getItem(Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)); } @@ -126,7 +182,7 @@ public function testGetItemWrongCompositeIdentifier() ], ]); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new ItemDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $dataProvider->getItem(Dummy::class, 'ida=1;', 'foo'); } @@ -151,7 +207,7 @@ public function testAggregationResultExtension() $extensionProphecy->supportsResult(Dummy::class, 'foo', $context)->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($aggregationBuilder, Dummy::class, 'foo', $context)->willReturn([])->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); } @@ -167,7 +223,7 @@ public function testUnsupportedClass() 'id', ]); - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertFalse($dataProvider->supports(Dummy::class, 'foo')); } @@ -190,7 +246,7 @@ public function testCannotCreateAggregationBuilder() 'id', ]); - (new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]))->getItem(Dummy::class, 'foo', null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]); + (new ItemDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]))->getItem(Dummy::class, 'foo', null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]); } /** diff --git a/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php index 301843efa60..5745364d34f 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/SubresourceDataProviderTest.php @@ -22,6 +22,8 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedOwningDummy; @@ -45,6 +47,18 @@ */ class SubresourceDataProviderTest extends TestCase { + private $resourceMetadataFactoryProphecy; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + parent::setUp(); + + $this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + } + private function getMetadataProphecies(array $resourceClassesIdentifiers) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); @@ -94,7 +108,7 @@ public function testNotASubresource() $aggregationBuilder = $this->prophesize(Builder::class)->reveal(); $managerRegistry = $this->getManagerRegistryProphecy($aggregationBuilder, $identifiers, Dummy::class); - $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, []); + $dataProvider = new SubresourceDataProvider($managerRegistry, $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, []); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -126,7 +140,7 @@ public function testGetSubresource() $dummyIterator = $this->prophesize(Iterator::class); $dummyIterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'relatedDummies' => [['_id' => 2]]]]); - $dummyAggregationBuilder->execute()->shouldBeCalled()->willReturn($dummyIterator->reveal()); + $dummyAggregationBuilder->execute([])->shouldBeCalled()->willReturn($dummyIterator->reveal()); $managerProphecy->createAggregationBuilder(Dummy::class)->shouldBeCalled()->willReturn($dummyAggregationBuilder->reveal()); @@ -137,16 +151,18 @@ public function testGetSubresource() $iterator = $this->prophesize(Iterator::class); $iterator->toArray()->shouldBeCalled()->willReturn([]); - $aggregationBuilder->execute()->shouldBeCalled()->willReturn($iterator->reveal()); + $aggregationBuilder->execute([])->shouldBeCalled()->willReturn($iterator->reveal()); $aggregationBuilder->hydrate(RelatedDummy::class)->shouldBeCalled()->willReturn($aggregationBuilder); $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); + $this->resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => ['id']]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -171,7 +187,7 @@ public function testGetSubSubresourceItem() $dummyIterator = $this->prophesize(Iterator::class); $dummyIterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'relatedDummies' => [['_id' => 2]]]]); - $dummyAggregationBuilder->execute()->shouldBeCalled()->willReturn($dummyIterator->reveal()); + $dummyAggregationBuilder->execute([])->shouldBeCalled()->willReturn($dummyIterator->reveal()); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->hasAssociation('relatedDummies')->willReturn(true)->shouldBeCalled(); @@ -198,7 +214,7 @@ public function testGetSubSubresourceItem() $rIterator = $this->prophesize(Iterator::class); $rIterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'thirdLevel' => [['_id' => 3]]]]); - $rAggregationBuilder->execute()->shouldBeCalled()->willReturn($rIterator->reveal()); + $rAggregationBuilder->execute([])->shouldBeCalled()->willReturn($rIterator->reveal()); $rClassMetadataProphecy = $this->prophesize(ClassMetadata::class); $rClassMetadataProphecy->hasAssociation('thirdLevel')->shouldBeCalled()->willReturn(true); @@ -220,7 +236,7 @@ public function testGetSubSubresourceItem() $iterator = $this->prophesize(Iterator::class); $iterator->current()->shouldBeCalled()->willReturn($result); - $aggregationBuilder->execute()->shouldBeCalled()->willReturn($iterator->reveal()); + $aggregationBuilder->execute([])->shouldBeCalled()->willReturn($iterator->reveal()); $aggregationBuilder->hydrate(ThirdLevel::class)->shouldBeCalled()->willReturn($aggregationBuilder); $repositoryProphecy = $this->prophesize(DocumentRepository::class); @@ -231,15 +247,114 @@ public function testGetSubSubresourceItem() $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); + $this->resourceMetadataFactoryProphecy->create(ThirdLevel::class)->willReturn(new ResourceMetadata()); + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context)); } + public function testGetSubSubresourceItemWithExecuteOptions() + { + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $identifiers = ['id']; + + // First manager (Dummy) + $dummyAggregationBuilder = $this->prophesize(Builder::class); + $dummyLookup = $this->prophesize(Lookup::class); + $dummyLookup->alias('relatedDummies')->shouldBeCalled(); + $dummyAggregationBuilder->lookup('relatedDummies')->shouldBeCalled()->willReturn($dummyLookup->reveal()); + + $dummyMatch = $this->prophesize(Match::class); + $dummyMatch->equals(1)->shouldBeCalled(); + $dummyMatch->field('id')->shouldBeCalled()->willReturn($dummyMatch); + $dummyAggregationBuilder->match()->shouldBeCalled()->willReturn($dummyMatch->reveal()); + + $dummyIterator = $this->prophesize(Iterator::class); + $dummyIterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'relatedDummies' => [['_id' => 2]]]]); + $dummyAggregationBuilder->execute(['allowDiskUse' => true])->shouldBeCalled()->willReturn($dummyIterator->reveal()); + + $classMetadataProphecy = $this->prophesize(ClassMetadata::class); + $classMetadataProphecy->hasAssociation('relatedDummies')->willReturn(true)->shouldBeCalled(); + + $dummyManagerProphecy = $this->prophesize(DocumentManager::class); + $dummyManagerProphecy->createAggregationBuilder(Dummy::class)->shouldBeCalled()->willReturn($dummyAggregationBuilder->reveal()); + $dummyManagerProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); + + $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($dummyManagerProphecy->reveal()); + + // Second manager (RelatedDummy) + $rAggregationBuilder = $this->prophesize(Builder::class); + $rLookup = $this->prophesize(Lookup::class); + $rLookup->alias('thirdLevel')->shouldBeCalled(); + $rAggregationBuilder->lookup('thirdLevel')->shouldBeCalled()->willReturn($rLookup->reveal()); + + $rMatch = $this->prophesize(Match::class); + $rMatch->equals(1)->shouldBeCalled(); + $rMatch->field('id')->shouldBeCalled()->willReturn($rMatch); + $previousRMatch = $this->prophesize(Match::class); + $previousRMatch->in([2])->shouldBeCalled(); + $previousRMatch->field('_id')->shouldBeCalled()->willReturn($previousRMatch); + $rAggregationBuilder->match()->shouldBeCalled()->willReturn($rMatch->reveal(), $previousRMatch->reveal()); + + $rIterator = $this->prophesize(Iterator::class); + $rIterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'thirdLevel' => [['_id' => 3]]]]); + $rAggregationBuilder->execute(['allowDiskUse' => true])->shouldBeCalled()->willReturn($rIterator->reveal()); + + $rClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $rClassMetadataProphecy->hasAssociation('thirdLevel')->shouldBeCalled()->willReturn(true); + + $rDummyManagerProphecy = $this->prophesize(DocumentManager::class); + $rDummyManagerProphecy->createAggregationBuilder(RelatedDummy::class)->shouldBeCalled()->willReturn($rAggregationBuilder->reveal()); + $rDummyManagerProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($rClassMetadataProphecy->reveal()); + + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($rDummyManagerProphecy->reveal()); + + $result = new \stdClass(); + // Origin manager (ThirdLevel) + $aggregationBuilder = $this->prophesize(Builder::class); + + $match = $this->prophesize(Match::class); + $match->in([3])->shouldBeCalled(); + $match->field('_id')->shouldBeCalled()->willReturn($match); + $aggregationBuilder->match()->shouldBeCalled()->willReturn($match); + + $iterator = $this->prophesize(Iterator::class); + $iterator->current()->shouldBeCalled()->willReturn($result); + $aggregationBuilder->execute(['allowDiskUse' => true])->shouldBeCalled()->willReturn($iterator->reveal()); + $aggregationBuilder->hydrate(ThirdLevel::class)->shouldBeCalled()->willReturn($aggregationBuilder); + + $repositoryProphecy = $this->prophesize(DocumentRepository::class); + $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn($aggregationBuilder->reveal()); + + $managerProphecy = $this->prophesize(DocumentManager::class); + $managerProphecy->getRepository(ThirdLevel::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); + + $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); + + $this->resourceMetadataFactoryProphecy->create(ThirdLevel::class)->willReturn(new ResourceMetadata( + 'ThirdLevel', + null, + null, + null, + null, + null, + ['third_level_operation_name' => ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]]] + )); + + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); + + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + + $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; + + $this->assertEquals($result, $dataProvider->getSubresource(ThirdLevel::class, ['id' => ['id' => 1], 'relatedDummies' => ['id' => 1]], $context, 'third_level_operation_name')); + } + public function testGetSubresourceOneToOneOwningRelation() { // RelatedOwningDummy OneToOne Dummy @@ -274,16 +389,18 @@ public function testGetSubresourceOneToOneOwningRelation() $iterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'ownedDummy' => [['_id' => 3]]]]); $result = new \stdClass(); $iterator->current()->shouldBeCalled()->willReturn($result); - $aggregationBuilder->execute()->shouldBeCalled()->willReturn($iterator->reveal()); + $aggregationBuilder->execute([])->shouldBeCalled()->willReturn($iterator->reveal()); $aggregationBuilder->hydrate(RelatedOwningDummy::class)->shouldBeCalled()->willReturn($aggregationBuilder); $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(RelatedOwningDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); + $this->resourceMetadataFactoryProphecy->create(RelatedOwningDummy::class)->willReturn(new ResourceMetadata()); + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $context = ['property' => 'ownedDummy', 'identifiers' => [['id', Dummy::class]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -321,12 +438,14 @@ public function testAggregationResultExtension() $iterator = $this->prophesize(Iterator::class); $iterator->toArray()->shouldBeCalled()->willReturn([['_id' => 1, 'relatedDummies' => [['_id' => 3]]]]); - $aggregationBuilder->execute()->shouldBeCalled()->willReturn($iterator->reveal()); + $aggregationBuilder->execute([])->shouldBeCalled()->willReturn($iterator->reveal()); $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); + $this->resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); $extensionProphecy = $this->prophesize(AggregationResultCollectionExtensionInterface::class); @@ -334,7 +453,7 @@ public function testAggregationResultExtension() $extensionProphecy->supportsResult(RelatedDummy::class, null, Argument::type('array'))->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($aggregationBuilder, RelatedDummy::class, null, Argument::type('array'))->willReturn([])->shouldBeCalled(); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $context = ['property' => 'relatedDummies', 'identifiers' => [['id', Dummy::class]], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -357,7 +476,7 @@ public function testCannotCreateQueryBuilder() [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -371,7 +490,7 @@ public function testThrowResourceClassNotSupportedException() [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -401,7 +520,7 @@ public function testGetSubresourceCollectionItem() $rIterator = $this->prophesize(Iterator::class); $rIterator->current()->shouldBeCalled()->willReturn($result); - $rAggregationBuilder->execute()->shouldBeCalled()->willReturn($rIterator->reveal()); + $rAggregationBuilder->execute([])->shouldBeCalled()->willReturn($rIterator->reveal()); $rAggregationBuilder->hydrate(RelatedDummy::class)->shouldBeCalled()->willReturn($rAggregationBuilder); $aggregationBuilder = $this->prophesize(Builder::class); @@ -411,9 +530,11 @@ public function testGetSubresourceCollectionItem() $rDummyManagerProphecy->getRepository(RelatedDummy::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); + $this->resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); $context = ['property' => 'id', 'identifiers' => [['id', Dummy::class, true], ['relatedDummies', RelatedDummy::class, true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; diff --git a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php index eaf83bbf406..3a74e6cfb55 100644 --- a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php +++ b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Bridge\RamseyUuid\Normalizer; use ApiPlatform\Core\Bridge\RamseyUuid\Identifier\Normalizer\UuidNormalizer; +use ApiPlatform\Core\Exception\InvalidIdentifierException; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\Uuid; @@ -36,7 +37,7 @@ public function testNoSupportDenormalizeUuid() public function testFailDenormalizeUuid() { - $this->expectException(\ApiPlatform\Core\Exception\InvalidIdentifierException::class); + $this->expectException(InvalidIdentifierException::class); $uuid = 'notanuuid'; $normalizer = new UuidNormalizer(); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 11f23d41b3f..d03332f1818 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -144,6 +144,9 @@ class ApiPlatformExtensionTest extends TestCase 'doctrine_mongodb_odm' => [ 'enabled' => false, ], + 'defaults' => [ + 'attributes' => [], + ], ]]; private $extension; @@ -694,6 +697,7 @@ public function testEnableElasticsearch() $containerBuilderProphecy->registerForAutoconfiguration(RequestBodySearchCollectionExtensionInterface::class)->willReturn($this->childDefinitionProphecy)->shouldBeCalled(); $containerBuilderProphecy->setParameter('api_platform.elasticsearch.hosts', ['http://elasticsearch:9200'])->shouldBeCalled(); $containerBuilderProphecy->setParameter('api_platform.elasticsearch.mapping', [])->shouldBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.defaults', ['attributes' => []])->shouldBeCalled(); $config = self::DEFAULT_CONFIG; $config['api_platform']['elasticsearch'] = [ @@ -757,7 +761,46 @@ public function testKeepCachePoolClearerCacheWarmerWithDebug() $this->extension->load(self::DEFAULT_CONFIG, $containerBuilder); } - private function getPartialContainerBuilderProphecy() + public function testDisabledSwaggerAndHydra() + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['enable_swagger'] = false; + $config['api_platform']['enable_swagger_ui'] = false; + $config['api_platform']['formats'] = [ + 'jsonhal' => ['mime_types' => ['application/hal+json']], + ]; + + $containerBuilderProphecy = $this->getBaseContainerBuilderProphecy(['orm'], $config); + $containerBuilderProphecy->setDefinition('api_platform.swagger.action.ui', Argument::type(Definition::class))->shouldNotBeCalled(); + $containerBuilderProphecy->setDefinition('api_platform.swagger.listener.ui', Argument::type(Definition::class))->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', false)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_re_doc', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_re_doc', false)->shouldNotBeCalled(); + $containerBuilder = $containerBuilderProphecy->reveal(); + $this->extension->load($config, $containerBuilder); + } + + public function testDisabledSwagger() + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['enable_swagger'] = false; + $config['api_platform']['enable_swagger_ui'] = false; + + $containerBuilderProphecy = $this->getBaseContainerBuilderProphecy(['orm'], $config); + $containerBuilderProphecy->setDefinition('api_platform.swagger.action.ui', Argument::type(Definition::class))->shouldNotBeCalled(); + $containerBuilderProphecy->setDefinition('api_platform.swagger.listener.ui', Argument::type(Definition::class))->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', false)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_re_doc', true)->shouldNotBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.enable_re_doc', false)->shouldNotBeCalled(); + $containerBuilder = $containerBuilderProphecy->reveal(); + $this->extension->load($config, $containerBuilder); + } + + private function getPartialContainerBuilderProphecy($configuration = null) { $parameterBag = new EnvPlaceholderParameterBag(); @@ -783,7 +826,7 @@ private function getPartialContainerBuilderProphecy() 'api_platform.collection.order_parameter_name' => 'order', 'api_platform.description' => 'description', 'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']], - 'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']], + 'api_platform.formats' => null === $configuration ? ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']] : $this->getFormatsFromConfiguration($configuration['api_platform']['formats']) ?? [], 'api_platform.patch_formats' => [], 'api_platform.exception_to_status' => [ ExceptionInterface::class => Response::HTTP_BAD_REQUEST, @@ -804,6 +847,7 @@ private function getPartialContainerBuilderProphecy() 'api_platform.http_cache.shared_max_age' => null, 'api_platform.http_cache.vary' => ['Accept'], 'api_platform.http_cache.public' => null, + 'api_platform.defaults' => ['attributes' => []], 'api_platform.enable_entrypoint' => true, 'api_platform.enable_docs' => true, ]; @@ -977,9 +1021,12 @@ private function getPartialContainerBuilderProphecy() return $containerBuilderProphecy; } - private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLoad = ['orm']) + private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLoad = ['orm'], $configuration = null) { - $containerBuilderProphecy = $this->getPartialContainerBuilderProphecy(); + $hasSwagger = null === $configuration || true === $configuration['api_platform']['enable_swagger'] ?? false; + $hasHydra = null === $configuration || isset($configuration['api_platform']['formats']['jsonld']); + + $containerBuilderProphecy = $this->getPartialContainerBuilderProphecy($configuration); $containerBuilderProphecy->hasParameter('kernel.debug')->willReturn(true); $containerBuilderProphecy->getParameter('kernel.debug')->willReturn(false); @@ -1063,8 +1110,6 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.oauth.tokenUrl' => '/oauth/v2/token', 'api_platform.oauth.authorizationUrl' => '/oauth/v2/auth', 'api_platform.oauth.scopes' => [], - 'api_platform.swagger.versions' => [2, 3], - 'api_platform.swagger.api_keys' => [], 'api_platform.enable_swagger_ui' => true, 'api_platform.enable_re_doc' => true, 'api_platform.graphql.enabled' => true, @@ -1076,8 +1121,16 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.resource_class_directories' => Argument::type('array'), 'api_platform.validator.serialize_payload_fields' => [], 'api_platform.elasticsearch.enabled' => false, + 'api_platform.defaults' => ['attributes' => []], ]; + if ($hasSwagger) { + $parameters['api_platform.swagger.versions'] = [2, 3]; + $parameters['api_platform.swagger.api_keys'] = []; + } else { + $parameters['api_platform.swagger.versions'] = []; + } + foreach ($parameters as $key => $value) { $containerBuilderProphecy->setParameter($key, $value)->shouldBeCalled(); } @@ -1153,20 +1206,6 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.http_cache.listener.response.configure', 'api_platform.http_cache.purger.varnish_client', 'api_platform.http_cache.purger.varnish', - 'api_platform.hydra.listener.response.add_link_header', - 'api_platform.hydra.normalizer.collection', - 'api_platform.hydra.normalizer.collection_filters', - 'api_platform.hydra.normalizer.constraint_violation_list', - 'api_platform.hydra.normalizer.documentation', - 'api_platform.hydra.normalizer.entrypoint', - 'api_platform.hydra.normalizer.error', - 'api_platform.hydra.normalizer.partial_collection_view', - 'api_platform.hydra.json_schema.schema_factory', - 'api_platform.jsonld.action.context', - 'api_platform.jsonld.context_builder', - 'api_platform.jsonld.encoder', - 'api_platform.jsonld.normalizer.item', - 'api_platform.jsonld.normalizer.object', 'api_platform.listener.view.validate', 'api_platform.listener.view.validate_query_parameters', 'api_platform.mercure.listener.response.add_link_header', @@ -1190,13 +1229,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.problem.normalizer.constraint_violation_list', 'api_platform.problem.normalizer.error', 'api_platform.swagger.action.ui', - 'api_platform.swagger.command.swagger_command', 'api_platform.swagger.listener.ui', - 'api_platform.swagger.normalizer.api_gateway', - 'api_platform.swagger.normalizer.documentation', - 'api_platform.json_schema.type_factory', - 'api_platform.json_schema.schema_factory', - 'api_platform.json_schema.json_schema_generate_command', 'api_platform.validator', 'test.api_platform.client', ]; @@ -1229,6 +1262,41 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo $definitions[] = 'api_platform.doctrine.metadata_factory'; } + // Only when swagger is enabled + if ($hasSwagger) { + $definitions[] = 'api_platform.swagger.command.swagger_command'; + $definitions[] = 'api_platform.swagger.normalizer.api_gateway'; + $definitions[] = 'api_platform.swagger.normalizer.documentation'; + $definitions[] = 'api_platform.json_schema.type_factory'; + $definitions[] = 'api_platform.json_schema.schema_factory'; + $definitions[] = 'api_platform.json_schema.json_schema_generate_command'; + } + + // has jsonld + if ($hasHydra) { + $containerBuilderProphecy->has('api_platform.json_schema.schema_factory')->shouldBeCalled()->willReturn($hasSwagger); + + if (!$hasSwagger) { + $containerBuilderProphecy->has('api_platform.json_schema.schema_factory')->shouldBeCalled()->willReturn(false); + $containerBuilderProphecy->removeDefinition('api_platform.hydra.json_schema.schema_factory')->shouldBeCalled(); + } + + $definitions[] = 'api_platform.hydra.json_schema.schema_factory'; + $definitions[] = 'api_platform.hydra.listener.response.add_link_header'; + $definitions[] = 'api_platform.hydra.normalizer.collection'; + $definitions[] = 'api_platform.hydra.normalizer.collection_filters'; + $definitions[] = 'api_platform.hydra.normalizer.constraint_violation_list'; + $definitions[] = 'api_platform.hydra.normalizer.documentation'; + $definitions[] = 'api_platform.hydra.normalizer.entrypoint'; + $definitions[] = 'api_platform.hydra.normalizer.error'; + $definitions[] = 'api_platform.hydra.normalizer.partial_collection_view'; + $definitions[] = 'api_platform.jsonld.action.context'; + $definitions[] = 'api_platform.jsonld.context_builder'; + $definitions[] = 'api_platform.jsonld.encoder'; + $definitions[] = 'api_platform.jsonld.normalizer.item'; + $definitions[] = 'api_platform.jsonld.normalizer.object'; + } + foreach ($definitions as $definition) { $containerBuilderProphecy->setDefinition($definition, Argument::type(Definition::class))->shouldBeCalled(); } @@ -1250,8 +1318,6 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo NumericFilter::class => 'api_platform.doctrine.orm.numeric_filter', ExistsFilter::class => 'api_platform.doctrine.orm.exists_filter', GraphQlSerializerContextBuilderInterface::class => 'api_platform.graphql.serializer.context_builder', - TypeFactoryInterface::class => 'api_platform.json_schema.type_factory', - SchemaFactoryInterface::class => 'api_platform.json_schema.schema_factory', ]; if (\in_array('odm', $doctrineIntegrationsToLoad, true)) { @@ -1269,6 +1335,14 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo ]; } + // Only when swagger is enabled + if ($hasSwagger) { + $aliases += [ + TypeFactoryInterface::class => 'api_platform.json_schema.type_factory', + SchemaFactoryInterface::class => 'api_platform.json_schema.schema_factory', + ]; + } + foreach ($aliases as $alias => $service) { $containerBuilderProphecy->setAlias($alias, $service)->shouldBeCalled(); } @@ -1283,4 +1357,21 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo return $containerBuilderProphecy; } + + private function getFormatsFromConfiguration(array $formatsConfiguration) + { + $formats = []; + + foreach ($formatsConfiguration as $format => $mimeTypes) { + if (!isset($formats[$format])) { + $formats[$format] = []; + } + + foreach ($mimeTypes['mime_types'] as $mimeType) { + $formats[$format][] = $mimeType; + } + } + + return $formats; + } } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPassTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPassTest.php index 25d7af71d56..adde3d3d0f9 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPassTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/AnnotationFilterPassTest.php @@ -14,15 +14,16 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection\Compiler; use ApiPlatform\Core\Annotation\ApiFilter; -use ApiPlatform\Core\Annotation\ApiProperty; -use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Api\FilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\AnnotationFilterPass; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Serializer\Filter\GroupFilter; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Orm\Filter\AnotherDummyFilter; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\DoesNotImplementInterfaceFilter; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\NoConstructorFilter; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter\NoPropertiesArgumentFilter; use Doctrine\Common\Annotations\Reader; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -37,174 +38,210 @@ */ class AnnotationFilterPassTest extends TestCase { - public function testProcess() + public function testConstruct(): void { $annotationFilterPass = new AnnotationFilterPass(); $this->assertInstanceOf(CompilerPassInterface::class, $annotationFilterPass); + } - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + public function testProcess(): void + { + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => SearchFilter::class, 'strategy' => 'exact', 'properties' => ['description', 'relatedDummy.name', 'name']]), + new ApiFilter(['value' => GroupFilter::class, 'arguments' => ['parameterName' => 'foobar']]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); + $readerProphecy->getPropertyAnnotations(Argument::allOf( + Argument::type(\ReflectionProperty::class), + Argument::that(function (\ReflectionProperty $reflectionProperty): bool { + return Dummy::class === $reflectionProperty->getDeclaringClass()->getName(); + }), + Argument::which('getName', 'dummyDate') + ))->willReturn([ + new ApiFilter(['value' => DateFilter::class]), + ]); - $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->shouldBeCalled()->willReturn([ + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->willReturn(false); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_serializer_filter_group_filter')->willReturn(false); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_date_filter')->willReturn(false); + $containerBuilderProphecy->has(SearchFilter::class)->willReturn(false); + $containerBuilderProphecy->has(GroupFilter::class)->willReturn(false); + $containerBuilderProphecy->has(DateFilter::class)->willReturn(true); + $containerBuilderProphecy->findDefinition(DateFilter::class)->willReturn((new Definition(DateFilter::class))->setAbstract(true)); + $containerBuilderProphecy->getReflectionClass(SearchFilter::class, false)->willReturn(new \ReflectionClass(SearchFilter::class)); + $containerBuilderProphecy->getReflectionClass(GroupFilter::class, false)->willReturn(new \ReflectionClass(GroupFilter::class)); + $containerBuilderProphecy->getReflectionClass(DateFilter::class, false)->willReturn(new \ReflectionClass(DateFilter::class)); + $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter', Argument::allOf( + Argument::type(Definition::class), + Argument::that(function (Definition $definition): bool { + return SearchFilter::class === $definition->getClass() && ['$properties' => ['description' => null, 'relatedDummy.name' => null, 'name' => null]] === $definition->getArguments(); + }) + ))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_serializer_filter_group_filter', Argument::allOf( + Argument::type(Definition::class), + Argument::that(function (Definition $definition): bool { + return GroupFilter::class === $definition->getClass() && ['$parameterName' => 'foobar'] === $definition->getArguments(); + }) + ))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_date_filter', Argument::allOf( + Argument::type(ChildDefinition::class), + Argument::that(function (ChildDefinition $definition): bool { + return DateFilter::class === $definition->getParent() && ['$properties' => ['dummyDate' => null]] === $definition->getArguments(); + }) + ))->shouldBeCalled(); - $reader = $this->prophesize(Reader::class); - - $reader->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->will(function ($args) { - if (Dummy::class === $args[0]->class && 'dummyDate' === $args[0]->name) { - return [new ApiFilter(['value' => DateFilter::class]), new ApiProperty()]; - } - - return []; - }); - - $reader->getClassAnnotations(Argument::type(\ReflectionClass::class))->will(function ($args) { - if (Dummy::class === $args[0]->name) { - return [new ApiFilter(['value' => SearchFilter::class, 'strategy' => 'exact', 'properties' => ['description', 'relatedDummy.name', 'name']]), new ApiResource([]), new ApiFilter(['value' => GroupFilter::class, 'arguments' => ['parameterName' => 'foobar']])]; - } - - return []; - }); - - $containerBuilderProphecy->get('annotation_reader')->shouldBeCalled()->willReturn($reader->reveal()); - - $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->shouldBeCalled()->willReturn(false); - $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_serializer_filter_group_filter')->shouldBeCalled()->willReturn(false); - $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_date_filter')->shouldBeCalled()->willReturn(false); - - $containerBuilderProphecy->has(SearchFilter::class)->willReturn(false)->shouldBeCalled(); - $containerBuilderProphecy->has(GroupFilter::class)->willReturn(false)->shouldBeCalled(); - $containerBuilderProphecy->has(DateFilter::class)->willReturn(true)->shouldBeCalled(); - - $containerBuilderProphecy->findDefinition(DateFilter::class)->willReturn((new Definition(DateFilter::class))->setAbstract(true))->shouldBeCalled(); - - $containerBuilderProphecy->getReflectionClass(SearchFilter::class, false)->willReturn(new \ReflectionClass(SearchFilter::class))->shouldBeCalled(); - $containerBuilderProphecy->getReflectionClass(GroupFilter::class, false)->willReturn(new \ReflectionClass(GroupFilter::class))->shouldBeCalled(); - - $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter', Argument::that(function ($def) { - $this->assertInstanceOf(Definition::class, $def); - $this->assertEquals(SearchFilter::class, $def->getClass()); - $this->assertEquals(['$properties' => ['description' => null, 'relatedDummy.name' => null, 'name' => null]], $def->getArguments()); - - return true; - }))->shouldBeCalled(); - - $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_serializer_filter_group_filter', Argument::that(function ($def) { - $this->assertInstanceOf(Definition::class, $def); - $this->assertEquals(GroupFilter::class, $def->getClass()); - $this->assertEquals(['$parameterName' => 'foobar'], $def->getArguments()); - - return true; - }))->shouldBeCalled(); - - $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_date_filter', Argument::that(function ($def) { - $this->assertInstanceOf(ChildDefinition::class, $def); - $this->assertEquals(DateFilter::class, $def->getParent()); - $this->assertEquals(['$properties' => ['dummyDate' => null]], $def->getArguments()); - - return true; - }))->shouldBeCalled(); - + $annotationFilterPass = new AnnotationFilterPass(); $annotationFilterPass->process($containerBuilderProphecy->reveal()); } - public function testProcessWrongFilter() + public function testProcessFilterWhichDoesNotImplementRequiredInterface(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The filter class "ApiPlatform\\Core\\Tests\\Fixtures\\TestBundle\\Doctrine\\Orm\\Filter\\AnotherDummyFilter" does not implement "ApiPlatform\\Core\\Api\\FilterInterface".'); - - $annotationFilterPass = new AnnotationFilterPass(); - - $this->assertInstanceOf(CompilerPassInterface::class, $annotationFilterPass); + $this->expectExceptionMessage(sprintf('The filter class "%s" does not implement "%s".', DoesNotImplementInterfaceFilter::class, FilterInterface::class)); + + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => DoesNotImplementInterfaceFilter::class]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - - $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->shouldBeCalled()->willReturn([ + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); - $reader = $this->prophesize(Reader::class); - - $reader->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->will(function ($args) { - return []; - }); - - $reader->getClassAnnotations(Argument::type(\ReflectionClass::class))->will(function ($args) { - if (Dummy::class === $args[0]->name) { - return [new ApiFilter(['value' => AnotherDummyFilter::class])]; - } - - return []; - }); - - $containerBuilderProphecy->get('annotation_reader')->shouldBeCalled()->willReturn($reader->reveal()); - + $annotationFilterPass = new AnnotationFilterPass(); $annotationFilterPass->process($containerBuilderProphecy->reveal()); } - public function testProcessExistingFilter() + public function testProcessFilterWhichHasAlreadyBeenRegistered(): void { - $annotationFilterPass = new AnnotationFilterPass(); - - $this->assertInstanceOf(CompilerPassInterface::class, $annotationFilterPass); + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => SearchFilter::class]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - - $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->shouldBeCalled()->willReturn([ + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->willReturn(true); + $containerBuilderProphecy->setDefinition(Argument::cetera())->shouldNotBeCalled(); - $reader = $this->prophesize(Reader::class); - - $reader->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->will(function ($args) { - return []; - }); - - $reader->getClassAnnotations(Argument::type(\ReflectionClass::class))->will(function ($args) { - if (Dummy::class === $args[0]->name) { - return [new ApiFilter(['value' => SearchFilter::class])]; - } + $annotationFilterPass = new AnnotationFilterPass(); + $annotationFilterPass->process($containerBuilderProphecy->reveal()); + } - return []; - }); + public function testProcessInvalidFilterClass(): void + { + $this->expectException(DependencyInjectionInvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Class "%s" used for service "annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter" cannot be found.', SearchFilter::class)); - $containerBuilderProphecy->get('annotation_reader')->shouldBeCalled()->willReturn($reader->reveal()); + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => SearchFilter::class]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); - $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->shouldBeCalled()->willReturn(true); + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ + __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', + ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->willReturn(false); + $containerBuilderProphecy->getReflectionClass(SearchFilter::class, false)->willReturn(null); + $annotationFilterPass = new AnnotationFilterPass(); $annotationFilterPass->process($containerBuilderProphecy->reveal()); } - public function testProcessInvalidFilterClass() + public function testProcessFilterWithoutConstructor(): void { - $annotationFilterPass = new AnnotationFilterPass(); + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => NoConstructorFilter::class]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->shouldBeCalled()->willReturn([ + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_tests_fixtures_test_bundle_filter_no_constructor_filter')->willReturn(false); + $containerBuilderProphecy->getReflectionClass(NoConstructorFilter::class, false)->willReturn(new \ReflectionClass(NoConstructorFilter::class)); + $containerBuilderProphecy->has(NoConstructorFilter::class)->willReturn(true); + $containerBuilderProphecy->findDefinition(NoConstructorFilter::class)->willReturn((new Definition(NoConstructorFilter::class))->setAbstract(false)); + $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_tests_fixtures_test_bundle_filter_no_constructor_filter', Argument::allOf( + Argument::type(Definition::class), + Argument::that(function (Definition $definition): bool { + return NoConstructorFilter::class === $definition->getClass() && [] === $definition->getArguments(); + }) + ))->shouldBeCalled(); - $reader = $this->prophesize(Reader::class); - $reader->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->will(function ($args) { - return []; - }); - $reader->getClassAnnotations(Argument::type(\ReflectionClass::class))->will(function ($args) { - if (Dummy::class === $args[0]->name) { - return [new ApiFilter(['value' => SearchFilter::class])]; - } - - return []; - }); + $annotationFilterPass = new AnnotationFilterPass(); + $annotationFilterPass->process($containerBuilderProphecy->reveal()); + } - $containerBuilderProphecy->get('annotation_reader')->shouldBeCalled()->willReturn($reader->reveal()); - $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter')->shouldBeCalled()->willReturn(false); - $containerBuilderProphecy->has(SearchFilter::class)->willReturn(false)->shouldBeCalled(); - $containerBuilderProphecy->getReflectionClass(SearchFilter::class, false)->willReturn(null)->shouldBeCalled(); + public function testProcessFilterWithoutPropertiesArgument(): void + { + $readerProphecy = $this->prophesize(Reader::class); + $readerProphecy->getClassAnnotations(Argument::type(\ReflectionClass::class))->willReturn([]); + $readerProphecy->getClassAnnotations(Argument::allOf( + Argument::type(\ReflectionClass::class), + Argument::which('getName', Dummy::class) + ))->willReturn([ + new ApiFilter(['value' => NoPropertiesArgumentFilter::class]), + ]); + $readerProphecy->getPropertyAnnotations(Argument::type(\ReflectionProperty::class))->willReturn([]); - $this->expectException(DependencyInjectionInvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('Class "%s" used for service "annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_bridge_doctrine_orm_filter_search_filter" cannot be found.', SearchFilter::class)); + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->getParameter('api_platform.resource_class_directories')->willReturn([ + __DIR__.'/../../../../../Fixtures/TestBundle/Entity/', + ]); + $containerBuilderProphecy->get('annotation_reader')->willReturn($readerProphecy); + $containerBuilderProphecy->has('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_tests_fixtures_test_bundle_filter_no_properties_argument_filter')->willReturn(false); + $containerBuilderProphecy->getReflectionClass(NoPropertiesArgumentFilter::class, false)->willReturn(new \ReflectionClass(NoPropertiesArgumentFilter::class)); + $containerBuilderProphecy->has(NoPropertiesArgumentFilter::class)->willReturn(true); + $containerBuilderProphecy->findDefinition(NoPropertiesArgumentFilter::class)->willReturn((new Definition(NoPropertiesArgumentFilter::class))->setAbstract(false)); + $containerBuilderProphecy->setDefinition('annotated_api_platform_core_tests_fixtures_test_bundle_entity_dummy_api_platform_core_tests_fixtures_test_bundle_filter_no_properties_argument_filter', Argument::allOf( + Argument::type(Definition::class), + Argument::that(function (Definition $definition): bool { + return NoPropertiesArgumentFilter::class === $definition->getClass() && [] === $definition->getArguments(); + }) + ))->shouldBeCalled(); + $annotationFilterPass = new AnnotationFilterPass(); $annotationFilterPass->process($containerBuilderProphecy->reveal()); } } diff --git a/tests/Bridge/Symfony/Messenger/ContextStampTest.php b/tests/Bridge/Symfony/Messenger/ContextStampTest.php new file mode 100644 index 00000000000..0c2cbddf74f --- /dev/null +++ b/tests/Bridge/Symfony/Messenger/ContextStampTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Bridge\Symfony\Messenger; + +use ApiPlatform\Core\Bridge\Symfony\Messenger\ContextStamp; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * @author Sergii Pavlenko + */ +class ContextStampTest extends TestCase +{ + public function testConstruct() + { + $this->assertInstanceOf(StampInterface::class, new ContextStamp()); + } + + public function testGetContext() + { + $contextStamp = new ContextStamp(); + $this->assertIsArray($contextStamp->getContext()); + } +} diff --git a/tests/Bridge/Symfony/Messenger/DataPersisterTest.php b/tests/Bridge/Symfony/Messenger/DataPersisterTest.php index 2ff2d0dba05..781ab6277b0 100644 --- a/tests/Bridge/Symfony/Messenger/DataPersisterTest.php +++ b/tests/Bridge/Symfony/Messenger/DataPersisterTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\Messenger; +use ApiPlatform\Core\Bridge\Symfony\Messenger\ContextStamp; use ApiPlatform\Core\Bridge\Symfony\Messenger\DataPersister; use ApiPlatform\Core\Bridge\Symfony\Messenger\RemoveStamp; use ApiPlatform\Core\Exception\ResourceClassNotFoundException; @@ -56,7 +57,9 @@ public function testPersist() $dummy = new Dummy(); $messageBus = $this->prophesize(MessageBusInterface::class); - $messageBus->dispatch($dummy)->willReturn(new Envelope($dummy))->shouldBeCalled(); + $messageBus->dispatch(Argument::that(function (Envelope $envelope) use ($dummy) { + return $dummy === $envelope->getMessage() && null !== $envelope->last(ContextStamp::class); + }))->willReturn(new Envelope($dummy))->shouldBeCalled(); $dataPersister = new DataPersister($this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), $messageBus->reveal()); $this->assertSame($dummy, $dataPersister->persist($dummy)); @@ -67,6 +70,7 @@ public function testRemove() $dummy = new Dummy(); $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(function (Envelope $envelope) use ($dummy) { return $dummy === $envelope->getMessage() && null !== $envelope->last(RemoveStamp::class); }))->willReturn(new Envelope($dummy))->shouldBeCalled(); @@ -80,7 +84,9 @@ public function testHandle() $dummy = new Dummy(); $messageBus = $this->prophesize(MessageBusInterface::class); - $messageBus->dispatch($dummy)->willReturn((new Envelope($dummy))->with(new HandledStamp($dummy, 'DummyHandler::__invoke')))->shouldBeCalled(); + $messageBus->dispatch(Argument::that(function (Envelope $envelope) use ($dummy) { + return $dummy === $envelope->getMessage() && null !== $envelope->last(ContextStamp::class); + }))->willReturn((new Envelope($dummy))->with(new HandledStamp($dummy, 'DummyHandler::__invoke')))->shouldBeCalled(); $dataPersister = new DataPersister($this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), $messageBus->reveal()); $this->assertSame($dummy, $dataPersister->persist($dummy)); diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index 2ddc9f878e8..a25c1bff186 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -27,6 +27,11 @@ * @author Alexandre Delplace * * @ApiResource(attributes={ + * "doctrine_mongodb"={ + * "execute_options"={ + * "allowDiskUse"=true + * } + * }, * "filters"={ * "my_dummy.mongodb.boolean", * "my_dummy.mongodb.date", diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index 04f0871ea2f..dcf01f6e5de 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; +use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Annotation\ApiSubresource; use Doctrine\Common\Collections\ArrayCollection; @@ -32,9 +33,10 @@ class RelatedDummy extends ParentDummy { /** + * @ApiProperty(writable=false) * @ApiSubresource * @ODM\Id(strategy="INCREMENT", type="integer") - * @Groups({"friends"}) + * @Groups({"chicago", "friends"}) */ private $id; diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index 8802cdfc054..02f13cf929e 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; +use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Annotation\ApiSubresource; use Doctrine\Common\Collections\ArrayCollection; @@ -31,11 +32,12 @@ class RelatedDummy extends ParentDummy { /** + * @ApiProperty(writable=false) * @ApiSubresource * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") - * @Groups({"friends"}) + * @Groups({"chicago", "friends"}) */ private $id; diff --git a/tests/Fixtures/TestBundle/Doctrine/Orm/Filter/AnotherDummyFilter.php b/tests/Fixtures/TestBundle/Filter/DoesNotImplementInterfaceFilter.php similarity index 77% rename from tests/Fixtures/TestBundle/Doctrine/Orm/Filter/AnotherDummyFilter.php rename to tests/Fixtures/TestBundle/Filter/DoesNotImplementInterfaceFilter.php index 862867eacc1..9fe8e5b9cd0 100644 --- a/tests/Fixtures/TestBundle/Doctrine/Orm/Filter/AnotherDummyFilter.php +++ b/tests/Fixtures/TestBundle/Filter/DoesNotImplementInterfaceFilter.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Orm\Filter; +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter; -class AnotherDummyFilter +final class DoesNotImplementInterfaceFilter { public function getDescription(string $resourceClass): array { diff --git a/tests/Fixtures/TestBundle/Filter/NoConstructorFilter.php b/tests/Fixtures/TestBundle/Filter/NoConstructorFilter.php new file mode 100644 index 00000000000..0af9a9c7711 --- /dev/null +++ b/tests/Fixtures/TestBundle/Filter/NoConstructorFilter.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter; + +use ApiPlatform\Core\Api\FilterInterface; + +final class NoConstructorFilter implements FilterInterface +{ + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + return [ + 'property' => 'foo', + 'type' => '', + 'required' => false, + ]; + } +} diff --git a/tests/Fixtures/TestBundle/Filter/NoPropertiesArgumentFilter.php b/tests/Fixtures/TestBundle/Filter/NoPropertiesArgumentFilter.php new file mode 100644 index 00000000000..81b67ec6976 --- /dev/null +++ b/tests/Fixtures/TestBundle/Filter/NoPropertiesArgumentFilter.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Filter; + +use ApiPlatform\Core\Api\FilterInterface; + +final class NoPropertiesArgumentFilter implements FilterInterface +{ + private $foo; + + public function __construct(string $foo = 'bar') + { + $this->foo = $foo; + } + + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + return [ + 'property' => $this->foo, + 'type' => '', + 'required' => false, + ]; + } +} diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index feb981f694c..91ba2ba57d1 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -137,10 +137,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], ]; - $c->loadFromExtension('security', $securityConfig); + $c->prependExtensionConfig('security', $securityConfig); if (class_exists(DoctrineMongoDBBundle::class)) { - $c->loadFromExtension('doctrine_mongodb', [ + $c->prependExtensionConfig('doctrine_mongodb', [ 'connections' => [ 'default' => null, ], @@ -153,7 +153,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load } if ($_SERVER['LEGACY'] ?? true) { - $c->loadFromExtension('nelmio_api_doc', [ + $c->prependExtensionConfig('nelmio_api_doc', [ 'sandbox' => [ 'accept_type' => 'application/json', 'body_format' => [ @@ -165,7 +165,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], ], ]); - $c->loadFromExtension('api_platform', ['enable_nelmio_api_doc' => true]); + $c->prependExtensionConfig('api_platform', ['enable_nelmio_api_doc' => true]); } } } diff --git a/tests/Fixtures/app/config/config_elasticsearch.yml b/tests/Fixtures/app/config/config_elasticsearch.yml index 157b2a13d86..abb0610ccf3 100644 --- a/tests/Fixtures/app/config/config_elasticsearch.yml +++ b/tests/Fixtures/app/config/config_elasticsearch.yml @@ -1,12 +1,14 @@ imports: - - { resource: parameters_elasticsearch.yml } - { resource: config_test.yml } +parameters: + env(ELASTICSEARCH_URL): http://localhost:9200 + api_platform: mapping: paths: ['%kernel.project_dir%/../Elasticsearch/Model'] elasticsearch: - hosts: '%host%' + hosts: '%env(resolve:ELASTICSEARCH_URL)%' services: test.api_platform.elasticsearch.client: diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index cbf657afedb..b59f3069e11 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -1,13 +1,16 @@ imports: - - { resource: parameters_mongodb.yml } - { resource: config_common.yml } +parameters: + env(MONGODB_DB): api_platform_test + env(MONGODB_URL): mongodb://localhost:27017 + doctrine_mongodb: connections: default: - server: '%server%' + server: '%env(resolve:MONGODB_URL)%' options: {} - default_database: '%dbname%' + default_database: '%env(resolve:MONGODB_DB)%' document_managers: default: auto_mapping: true diff --git a/tests/Fixtures/app/config/config_mysql.yml b/tests/Fixtures/app/config/config_mysql.yml index c1d3b894f48..d83e9650176 100644 --- a/tests/Fixtures/app/config/config_mysql.yml +++ b/tests/Fixtures/app/config/config_mysql.yml @@ -1,13 +1,15 @@ imports: - - { resource: parameters_mysql.yml } - { resource: config_test.yml } +parameters: + env(DATABASE_URL): mysql://root:@localhost/api_platform_test + env(MYSQL_VERSION): '5.7' + doctrine: dbal: - driver: 'pdo_mysql' - dbname: '%dbname%' - user: '%user%' - password: '%password%' - host: '%host%' + driver: pdo_mysql + url: '%env(resolve:DATABASE_URL)%' + path: ~ + server_version: '%env(MYSQL_VERSION)%' types: - uuid: Ramsey\Uuid\Doctrine\UuidType + uuid: Ramsey\Uuid\Doctrine\UuidType diff --git a/tests/Fixtures/app/config/config_postgres.yml b/tests/Fixtures/app/config/config_postgres.yml index 0ce81552b72..005a25c9d5a 100644 --- a/tests/Fixtures/app/config/config_postgres.yml +++ b/tests/Fixtures/app/config/config_postgres.yml @@ -1,13 +1,15 @@ imports: - - { resource: parameters_postgres.yml } - { resource: config_test.yml } +parameters: + env(DATABASE_URL): postgres://postgres:@localhost/api_platform_test + env(POSTGRES_VERSION): '10' + doctrine: dbal: - driver: 'pdo_pgsql' - dbname: '%dbname%' - user: '%user%' - password: '%password%' - host: '%host%' + driver: pdo_pgsql + url: '%env(resolve:DATABASE_URL)%' + path: ~ + server_version: '%env(POSTGRES_VERSION)%' types: - uuid: Ramsey\Uuid\Doctrine\UuidType + uuid: Ramsey\Uuid\Doctrine\UuidType diff --git a/tests/Fixtures/app/config/parameters_elasticsearch.yml b/tests/Fixtures/app/config/parameters_elasticsearch.yml deleted file mode 100644 index 0b4678a7521..00000000000 --- a/tests/Fixtures/app/config/parameters_elasticsearch.yml +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - host: 'http://localhost:9200' diff --git a/tests/Fixtures/app/config/parameters_mongodb.yml b/tests/Fixtures/app/config/parameters_mongodb.yml deleted file mode 100644 index 1671ef84289..00000000000 --- a/tests/Fixtures/app/config/parameters_mongodb.yml +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - server: mongodb://localhost:27017 - dbname: 'api_platform_test' diff --git a/tests/Fixtures/app/config/parameters_mysql.yml b/tests/Fixtures/app/config/parameters_mysql.yml deleted file mode 100644 index ddc3a7c7a93..00000000000 --- a/tests/Fixtures/app/config/parameters_mysql.yml +++ /dev/null @@ -1,5 +0,0 @@ -parameters: - dbname: 'api_platform_test' - user: 'travis' - password: '' - host: 'localhost' diff --git a/tests/Fixtures/app/config/parameters_postgres.yml b/tests/Fixtures/app/config/parameters_postgres.yml deleted file mode 100644 index 31078030bc3..00000000000 --- a/tests/Fixtures/app/config/parameters_postgres.yml +++ /dev/null @@ -1,5 +0,0 @@ -parameters: - dbname: 'api_platform_test' - user: 'postgres' - password: '' - host: 'localhost' diff --git a/tests/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageTest.php b/tests/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageTest.php index 21f11dda5ce..301963c836a 100644 --- a/tests/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageTest.php +++ b/tests/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageTest.php @@ -115,4 +115,34 @@ public function testNotGranted(): void 'extra_variables' => $extraVariables, ]); } + + public function testNoSecurityBundleInstalled(): void + { + $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage($this->resourceMetadataFactoryProphecy->reveal(), null); + + $operationName = 'item_query'; + $resourceClass = 'myResource'; + $isGranted = 'not_granted'; + $resourceMetadata = (new ResourceMetadata())->withGraphql([ + $operationName => ['security_post_denormalize' => $isGranted], + ]); + $this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); + + $this->expectException(\LogicException::class); + + ($this->securityPostDenormalizeStage)($resourceClass, 'item_query', []); + } + + public function testNoSecurityBundleInstalledNoExpression(): void + { + $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage($this->resourceMetadataFactoryProphecy->reveal(), null); + + $resourceClass = 'myResource'; + $resourceMetadata = new ResourceMetadata(); + $this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); + + $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); + + ($this->securityPostDenormalizeStage)($resourceClass, 'item_query', []); + } } diff --git a/tests/GraphQl/Resolver/Stage/SecurityStageTest.php b/tests/GraphQl/Resolver/Stage/SecurityStageTest.php index 4b18a7345fd..a49207679a2 100644 --- a/tests/GraphQl/Resolver/Stage/SecurityStageTest.php +++ b/tests/GraphQl/Resolver/Stage/SecurityStageTest.php @@ -96,4 +96,34 @@ public function testNotGranted(): void 'extra_variables' => $extraVariables, ]); } + + public function testNoSecurityBundleInstalled(): void + { + $this->securityStage = new SecurityStage($this->resourceMetadataFactoryProphecy->reveal(), null); + + $operationName = 'item_query'; + $resourceClass = 'myResource'; + $isGranted = 'not_granted'; + $resourceMetadata = (new ResourceMetadata())->withGraphql([ + $operationName => ['security' => $isGranted], + ]); + $this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); + + $this->expectException(\LogicException::class); + + ($this->securityStage)($resourceClass, 'item_query', []); + } + + public function testNoSecurityBundleInstalledNoExpression(): void + { + $this->securityStage = new SecurityStage($this->resourceMetadataFactoryProphecy->reveal(), null); + + $resourceClass = 'myResource'; + $resourceMetadata = new ResourceMetadata(); + $this->resourceMetadataFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); + + $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); + + ($this->securityStage)($resourceClass, 'item_query', []); + } } diff --git a/tests/Metadata/Resource/Factory/AnnotationResourceMetadataFactoryTest.php b/tests/Metadata/Resource/Factory/AnnotationResourceMetadataFactoryTest.php index 27835cd7468..744e3e66053 100644 --- a/tests/Metadata/Resource/Factory/AnnotationResourceMetadataFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AnnotationResourceMetadataFactoryTest.php @@ -46,6 +46,38 @@ public function testCreate($reader, $decorated, string $expectedShortName, strin $this->assertEquals(['foo' => 'bar'], $metadata->getGraphql()); } + public function testCreateWithDefaults() + { + $defaults = [ + 'shortName' => 'Default shortname should not be ignored', + 'description' => 'CHANGEME!', + 'collection_operations' => ['get'], + 'item_operations' => ['get', 'put'], + 'attributes' => [ + 'pagination_items_per_page' => 4, + 'pagination_maximum_items_per_page' => 6, + ], + ]; + + $annotation = new ApiResource([ + 'itemOperations' => ['get', 'delete'], + 'attributes' => [ + 'pagination_maximum_items_per_page' => 10, + ], + ]); + $reader = $this->prophesize(Reader::class); + $reader->getClassAnnotation(Argument::type(\ReflectionClass::class), ApiResource::class)->willReturn($annotation)->shouldBeCalled(); + $factory = new AnnotationResourceMetadataFactory($reader->reveal(), null, $defaults); + $metadata = $factory->create(Dummy::class); + + $this->assertNull($metadata->getShortName()); + $this->assertEquals('CHANGEME!', $metadata->getDescription()); + $this->assertEquals(['get'], $metadata->getCollectionOperations()); + $this->assertEquals(['get', 'delete'], $metadata->getItemOperations()); + $this->assertEquals(4, $metadata->getAttribute('pagination_items_per_page')); + $this->assertEquals(10, $metadata->getAttribute('pagination_maximum_items_per_page')); + } + public function testCreateWithoutAttributes() { $annotation = new ApiResource([]); diff --git a/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php b/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php index 4b16a9cafb2..28aea8d4b52 100644 --- a/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php +++ b/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Extractor\ExtractorInterface; use ApiPlatform\Core\Metadata\Extractor\XmlExtractor; use ApiPlatform\Core\Metadata\Extractor\YamlExtractor; use ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory; @@ -23,6 +24,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ShortNameResourceMetadataFactory; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\DummyResourceInterface; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy; /** @@ -286,4 +288,52 @@ public function testItSupportsInterfaceAsAResource() $resourceMetadata = $shortNameResourceMetadataFactory->create(DummyResourceInterface::class); $this->assertSame('DummyResourceInterface', $resourceMetadata->getShortName()); } + + public function testItFallbacksToDefaultConfiguration() + { + $defaults = [ + 'shortName' => 'Default shortname should not be ignored', + 'description' => 'CHANGEME!', + 'collection_operations' => ['get'], + 'item_operations' => ['get', 'put'], + 'attributes' => [ + 'pagination_items_per_page' => 4, + 'pagination_maximum_items_per_page' => 6, + ], + ]; + $resourceConfiguration = [ + Dummy::class => [ + 'shortName' => null, + 'description' => null, + 'subresourceOperations' => null, + 'itemOperations' => ['get', 'delete'], + 'attributes' => [ + 'pagination_maximum_items_per_page' => 10, + ], + ], + ]; + + $extractor = new class($resourceConfiguration) implements ExtractorInterface { + private $resources; + + public function __construct(array $resources) + { + $this->resources = $resources; + } + + public function getResources(): array + { + return $this->resources; + } + }; + $factory = new ExtractorResourceMetadataFactory($extractor, null, $defaults); + $metadata = $factory->create(Dummy::class); + + $this->assertNull($metadata->getShortName()); + $this->assertEquals('CHANGEME!', $metadata->getDescription()); + $this->assertEquals(['get'], $metadata->getCollectionOperations()); + $this->assertEquals(['get', 'delete'], $metadata->getItemOperations()); + $this->assertEquals(4, $metadata->getAttribute('pagination_items_per_page')); + $this->assertEquals(10, $metadata->getAttribute('pagination_maximum_items_per_page')); + } } diff --git a/tests/Operation/DashedPathSegmentNameGeneratorTest.php b/tests/Operation/DashedPathSegmentNameGeneratorTest.php new file mode 100644 index 00000000000..40aa614f96a --- /dev/null +++ b/tests/Operation/DashedPathSegmentNameGeneratorTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Operation; + +use ApiPlatform\Core\Operation\DashPathSegmentNameGenerator; +use PHPUnit\Framework\TestCase; + +class DashedPathSegmentNameGeneratorTest extends TestCase +{ + public function testCreateSegmentNameGeneration() + { + $generator = new DashPathSegmentNameGenerator(); + $this->assertSame('ordering-people', $generator->getSegmentName('orderingPerson')); + $this->assertSame('some-person-names', $generator->getSegmentName('somePersonName')); + } +} diff --git a/tests/Swagger/Serializer/ApiGatewayNormalizerTest.php b/tests/Swagger/Serializer/ApiGatewayNormalizerTest.php index f97c6f65078..cf7f4b75903 100644 --- a/tests/Swagger/Serializer/ApiGatewayNormalizerTest.php +++ b/tests/Swagger/Serializer/ApiGatewayNormalizerTest.php @@ -13,220 +13,533 @@ namespace ApiPlatform\Core\Tests\Swagger\Serializer; +use ApiPlatform\Core\Documentation\Documentation; +use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; use ApiPlatform\Core\Swagger\Serializer\ApiGatewayNormalizer; +use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; -use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -/** - * @author Vincent Chalamon - */ final class ApiGatewayNormalizerTest extends TestCase { - /** - * @var ObjectProphecy - */ - private $documentationNormalizerProphecy; - - /** - * @var ObjectProphecy - */ - private $objectProphecy; - - /** - * @var ApiGatewayNormalizer - */ - private $normalizer; - - protected function setUp(): void + public function testSupportsNormalization(): void { - $this->documentationNormalizerProphecy = $this->prophesize(NormalizerInterface::class); - $this->documentationNormalizerProphecy->willImplement(CacheableSupportsMethodInterface::class); - $this->documentationNormalizerProphecy->hasCacheableSupportsMethod()->willReturn(true); - $this->objectProphecy = $this->prophesize(\stdClass::class); - $this->normalizer = new ApiGatewayNormalizer($this->documentationNormalizerProphecy->reveal()); - } + $documentationNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $documentationNormalizerProphecy->willImplement(CacheableSupportsMethodInterface::class); + $documentationNormalizerProphecy->supportsNormalization(DocumentationNormalizer::FORMAT, Documentation::class)->willReturn(true); + $documentationNormalizerProphecy->hasCacheableSupportsMethod()->willReturn(true); - public function testSupportsNormalization() - { - $this->documentationNormalizerProphecy->supportsNormalization('foo', 'bar')->willReturn(true)->shouldBeCalledTimes(1); - $this->assertTrue($this->normalizer->supportsNormalization('foo', 'bar')); - $this->assertTrue($this->normalizer->hasCacheableSupportsMethod()); - } + $normalizer = new ApiGatewayNormalizer($documentationNormalizerProphecy->reveal()); - public function testNormalizeWithoutApiGateway() - { - $this->documentationNormalizerProphecy->normalize($this->objectProphecy, 'jsonld', []) - ->willReturn(['basePath' => '/api']) - ->shouldBeCalledTimes(1); - $this->assertEquals(['basePath' => '/api'], $this->normalizer->normalize($this->objectProphecy->reveal(), 'jsonld')); + $this->assertTrue($normalizer->supportsNormalization(DocumentationNormalizer::FORMAT, Documentation::class)); + $this->assertTrue($normalizer->hasCacheableSupportsMethod()); } - public function testNormalizeWithApiGateway() + public function testNormalize(): void { - $this->documentationNormalizerProphecy->normalize($this->objectProphecy, 'jsonld', ['api_gateway' => true]) - ->willReturn([ - 'basePath' => '', - 'paths' => [ - '/foo' => [ - 'get' => [ - 'responses' => [ - '200' => [ - 'schema' => [ - 'items' => [ - '$ref' => '#/definitions/Foo-foo_read', - ], - ], + $documentation = new Documentation(new ResourceNameCollection([ + Dummy::class, + ])); + + $swaggerDocument = [ + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'post' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'schema' => [ + '$ref' => '#/definitions/Dummy', ], ], - 'parameters' => [ - [ - 'name' => 'bar', - ], - [ - 'name' => 'bar[]', + ], + 'responses' => [ + 201 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummy-list_details', ], ], ], - 'post' => [ - 'parameters' => [ - [ - 'name' => 'foo', - 'schema' => [ - '$ref' => '#/definitions/Foo-foo_write', + ]), + 'get' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'relatedDummy', + 'in' => 'query', + 'required' => false, + 'type' => 'string', + ], + [ + 'name' => 'relatedDummy[]', + 'in' => 'query', + 'required' => false, + 'type' => 'string', + ], + ], + 'responses' => [ + 200 => [ + 'schema' => [ + 'type' => 'array', + 'items' => [ + '$ref' => '#/definitions/Dummy-list', ], ], ], - 'responses' => [ - '201' => [ - 'schema' => [ - 'items' => [ - '$ref' => '#/definitions/Foo-foo_read', - ], - ], + ], + ]), + ], + '/dummies/{id}' => [ + 'get' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'type' => 'string', + ], + ], + 'responses' => [ + 200 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummy-list_details', ], ], ], - ], + ]), ], - 'definitions' => new \ArrayObject([ - 'Foo-foo_write' => [ - 'properties' => [ - 'bar' => [ - '$ref' => '#/definitions/Bar-bar_read', + '/dummies/{id}/what' => [ + 'post' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'schema' => [ + '$ref' => '#/definitions/Dummy:InputDto', + ], + ], + ], + 'responses' => [ + 200 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummy:OutputDto', + ], ], ], + ]), + ], + ]), + 'definitions' => new \ArrayObject([ + 'Dummy' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ]), + 'Dummy-list' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ]), + 'Dummy-list_details' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + 'relatedDummy' => new \ArrayObject([ + '$ref' => '#/definitions/RelatedDummy-list_details', + ]), + ], + ]), + 'Dummy:OutputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'baz' => new \ArrayObject([ + 'readOnly' => true, + 'type' => 'string', + ]), + 'bat' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'Dummy:InputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'foo' => new \ArrayObject([ + 'type' => 'string', + ]), + 'bar' => new \ArrayObject([ + 'type' => 'integer', + ]), ], - 'Foo-foo_read' => [ - 'properties' => [ - 'id' => [ - 'readOnly' => true, - 'type' => 'integer', + ]), + 'RelatedDummy-list_details' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + ]), + ], + ]), + ]), + ]; + + $modifiedSwaggerDocument = [ + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'post' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'schema' => [ + '$ref' => '#/definitions/Dummy', + ], ], - 'bar' => [ - '$ref' => '#/definitions/Bar-bar_write', + ], + 'responses' => [ + 201 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummylistdetails', + ], ], ], - ], - 'Bar-bar_write' => [ - 'properties' => [ - 'foo' => [ + ]), + 'get' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'relatedDummy', + 'in' => 'query', + 'required' => false, 'type' => 'string', ], ], - ], - 'Bar-bar_read' => [ - 'properties' => [ - 'id' => [ - 'readOnly' => true, - 'type' => 'integer', + 'responses' => [ + 200 => [ + 'schema' => [ + 'type' => 'array', + 'items' => [ + '$ref' => '#/definitions/Dummylist', + ], + ], ], - 'foo' => [ + ], + ]), + ], + '/dummies/{id}' => [ + 'get' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, 'type' => 'string', ], ], + 'responses' => [ + 200 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummylistdetails', + ], + ], + ], + ]), + ], + '/dummies/{id}/what' => [ + 'post' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'schema' => [ + '$ref' => '#/definitions/DummyInputDto', + ], + ], + ], + 'responses' => [ + 200 => [ + 'schema' => [ + '$ref' => '#/definitions/DummyOutputDto', + ], + ], + ], + ]), + ], + ]), + 'definitions' => new \ArrayObject([ + 'Dummy' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], ], ]), - ]) - ->shouldBeCalledTimes(1); - $this->assertEquals([ + 'Dummylist' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ]), + 'Dummylistdetails' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + 'relatedDummy' => new \ArrayObject([ + '$ref' => '#/definitions/RelatedDummylistdetails', + ]), + ], + ]), + 'DummyOutputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'baz' => new \ArrayObject([ + 'type' => 'string', + ]), + 'bat' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'DummyInputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'foo' => new \ArrayObject([ + 'type' => 'string', + ]), + 'bar' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'RelatedDummylistdetails' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + ]), + ], + ]), + ]), 'basePath' => '/', - 'paths' => [ - '/foo' => [ - 'get' => [ + ]; + + $documentationNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $documentationNormalizerProphecy->normalize($documentation, DocumentationNormalizer::FORMAT, [ + 'spec_version' => 2, + ApiGatewayNormalizer::API_GATEWAY => true, + ])->willReturn($swaggerDocument); + + $normalizer = new ApiGatewayNormalizer($documentationNormalizerProphecy->reveal()); + + $this->assertEquals($modifiedSwaggerDocument, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, [ + 'spec_version' => 2, + ApiGatewayNormalizer::API_GATEWAY => true, + ])); + } + + public function testNormalizeNotInApiGatewayContext(): void + { + $documentation = new Documentation(new ResourceNameCollection([ + Dummy::class, + ])); + + $swaggerDocument = [ + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'post' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'schema' => [ + '$ref' => '#/definitions/Dummy', + ], + ], + ], 'responses' => [ - '200' => [ + 201 => [ 'schema' => [ + '$ref' => '#/definitions/Dummy-list_details', + ], + ], + ], + ]), + 'get' => new \ArrayObject([ + 'parameters' => [ + [ + 'name' => 'relatedDummy', + 'in' => 'query', + 'required' => false, + 'type' => 'string', + ], + [ + 'name' => 'relatedDummy[]', + 'in' => 'query', + 'required' => false, + 'type' => 'string', + ], + ], + 'responses' => [ + 200 => [ + 'schema' => [ + 'type' => 'array', 'items' => [ - '$ref' => '#/definitions/Foofooread', + '$ref' => '#/definitions/Dummy-list', ], ], ], ], + ]), + ], + '/dummies/{id}' => [ + 'get' => new \ArrayObject([ 'parameters' => [ [ - 'name' => 'bar', + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'type' => 'string', ], ], - ], - 'post' => [ + 'responses' => [ + 200 => [ + 'schema' => [ + '$ref' => '#/definitions/Dummy-list_details', + ], + ], + ], + ]), + ], + '/dummies/{id}/what' => [ + 'post' => new \ArrayObject([ 'parameters' => [ [ - 'name' => 'foo', + 'name' => 'dummy', + 'in' => 'body', 'schema' => [ - '$ref' => '#/definitions/Foofoowrite', + '$ref' => '#/definitions/Dummy:InputDto', ], ], ], 'responses' => [ - '201' => [ + 200 => [ 'schema' => [ - 'items' => [ - '$ref' => '#/definitions/Foofooread', - ], + '$ref' => '#/definitions/Dummy:OutputDto', ], ], ], - ], + ]), ], - ], + ]), 'definitions' => new \ArrayObject([ - 'Foofoowrite' => [ - 'properties' => [ - 'bar' => [ - '$ref' => '#/definitions/Barbarread', - ], - ], - ], - 'Foofooread' => [ + 'Dummy' => new \ArrayObject([ 'properties' => [ 'id' => [ + 'readOnly' => true, 'type' => 'integer', ], - 'bar' => [ - '$ref' => '#/definitions/Barbarwrite', + 'description' => [ + 'type' => 'string', ], ], - ], - 'Barbarwrite' => [ + ]), + 'Dummy-list' => new \ArrayObject([ 'properties' => [ - 'foo' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ 'type' => 'string', ], ], - ], - 'Barbarread' => [ + ]), + 'Dummy-list_details' => new \ArrayObject([ 'properties' => [ 'id' => [ + 'readOnly' => true, 'type' => 'integer', ], - 'foo' => [ + 'description' => [ 'type' => 'string', ], + 'relatedDummy' => new \ArrayObject([ + '$ref' => '#/definitions/RelatedDummy-list_details', + ]), ], - ], + ]), + 'Dummy:OutputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'baz' => new \ArrayObject([ + 'readOnly' => true, + 'type' => 'string', + ]), + 'bat' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'Dummy:InputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'foo' => new \ArrayObject([ + 'type' => 'string', + ]), + 'bar' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'RelatedDummy-list_details' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + ]), + ], + ]), ]), - ], $this->normalizer->normalize($this->objectProphecy->reveal(), 'jsonld', ['api_gateway' => true])); + ]; + + $documentationNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $documentationNormalizerProphecy->normalize($documentation, DocumentationNormalizer::FORMAT, [ + 'spec_version' => 2, + ])->willReturn($swaggerDocument); + + $normalizer = new ApiGatewayNormalizer($documentationNormalizerProphecy->reveal()); + + $this->assertEquals($swaggerDocument, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, [ + 'spec_version' => 2, + ])); } } diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php index 3b7f806dff6..b8e1b605ae9 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php @@ -803,6 +803,201 @@ public function testNormalizeWithOnlyNormalizationGroups(): void $this->assertEquals($expected, $normalizer->normalize($documentation)); } + public function testNormalizeNotAddExtraBodyParameters(): void + { + $title = 'Test API'; + $description = 'This is a test API.'; + $version = '1.2.3'; + $documentation = new Documentation(new ResourceNameCollection([Dummy::class]), $title, $description, $version); + $groups = ['dummy', 'foo', 'bar']; + + $ref = 'Dummy-'.implode('_', $groups); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, ['serializer_groups' => $groups])->shouldBeCalledTimes(1)->willReturn(new PropertyNameCollection(['gerard'])); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); + + $dummyMetadata = new ResourceMetadata( + 'Dummy', + 'This is a dummy.', + 'http://schema.example.com/Dummy', + [ + 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS, + 'put' => ['method' => 'PUT', 'normalization_context' => [AbstractNormalizer::GROUPS => $groups]] + self::OPERATION_FORMATS, + ], + [ + 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS, + 'post' => [ + 'method' => 'POST', + 'swagger_context' => [ + 'parameters' => [ + [ + 'name' => 'dummy', + 'in' => 'body', + 'description' => 'The new custom Dummy resource', + 'schema' => ['$ref' => '#/definitions/Dummy'], + ], + ], + ], + ] + self::OPERATION_FORMATS, + ] + ); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [])); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'gerard')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a gerard.', true, true, true, true, false, false, null, null, [])); + + $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + + $normalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + null, + null, + $operationPathResolver + ); + + $expected = [ + 'swagger' => '2.0', + 'basePath' => '/', + 'info' => [ + 'title' => 'Test API', + 'description' => 'This is a test API.', + 'version' => '1.2.3', + ], + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'get' => new \ArrayObject([ + 'tags' => [ + 'Dummy', + ], + 'operationId' => 'getDummyCollection', + 'produces' => ['application/ld+json'], + 'summary' => 'Retrieves the collection of Dummy resources.', + 'parameters' => [ + [ + 'name' => 'page', + 'in' => 'query', + 'required' => false, + 'type' => 'integer', + 'description' => 'The collection page number', + ], + ], + 'responses' => [ + 200 => [ + 'description' => 'Dummy collection response', + 'schema' => [ + 'type' => 'array', + 'items' => ['$ref' => '#/definitions/Dummy'], + ], + ], + ], + ]), + 'post' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'postDummyCollection', + 'consumes' => ['application/ld+json'], + 'produces' => ['application/ld+json'], + 'summary' => 'Creates a Dummy resource.', + 'parameters' => [[ + 'name' => 'dummy', + 'in' => 'body', + 'description' => 'The new custom Dummy resource', + 'schema' => ['$ref' => '#/definitions/Dummy'], + ]], + 'responses' => [ + 201 => [ + 'description' => 'Dummy resource created', + 'schema' => ['$ref' => '#/definitions/Dummy'], + ], + 400 => ['description' => 'Invalid input'], + 404 => ['description' => 'Resource not found'], + ], + ]), + ], + '/dummies/{id}' => [ + 'get' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'getDummyItem', + 'produces' => ['application/ld+json'], + 'summary' => 'Retrieves a Dummy resource.', + 'parameters' => [[ + 'name' => 'id', + 'in' => 'path', + 'type' => 'string', + 'required' => true, + ]], + 'responses' => [ + 200 => [ + 'description' => 'Dummy resource response', + 'schema' => ['$ref' => '#/definitions/Dummy'], + ], + 404 => ['description' => 'Resource not found'], + ], + ]), + 'put' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'putDummyItem', + 'consumes' => ['application/ld+json'], + 'produces' => ['application/ld+json'], + 'summary' => 'Replaces the Dummy resource.', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'type' => 'string', + 'required' => true, + ], + [ + 'name' => 'dummy', + 'in' => 'body', + 'description' => 'The updated Dummy resource', + 'schema' => ['$ref' => '#/definitions/Dummy'], + ], + ], + 'responses' => [ + 200 => [ + 'description' => 'Dummy resource updated', + 'schema' => ['$ref' => '#/definitions/'.$ref], + ], + 400 => ['description' => 'Invalid input'], + 404 => ['description' => 'Resource not found'], + ], + ]), + ], + ]), + 'definitions' => new \ArrayObject([ + 'Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ]), + $ref => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'gerard' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a gerard.', + ]), + ], + ]), + ]), + ]; + + $this->assertEquals($expected, $normalizer->normalize($documentation)); + } + public function testNormalizeWithSwaggerDefinitionName(): void { $documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3'); @@ -2585,7 +2780,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 'schema' => [ 'type' => 'array', 'items' => [ - '$ref' => '#/definitions/Dummy:300dcd476cef011532fb0ca7683395d7', + '$ref' => '#/definitions/Dummy:OutputDto', ], ], ], @@ -2610,7 +2805,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 201 => [ 'description' => 'Dummy resource created', 'schema' => [ - '$ref' => '#/definitions/Dummy:300dcd476cef011532fb0ca7683395d7', + '$ref' => '#/definitions/Dummy:OutputDto', ], ], 400 => [ @@ -2626,7 +2821,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 'in' => 'body', 'description' => 'The new Dummy resource', 'schema' => [ - '$ref' => '#/definitions/Dummy:b4f76c1a44965bd401aa23bb37618acc', + '$ref' => '#/definitions/Dummy:InputDto', ], ], ], @@ -2650,7 +2845,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 200 => [ 'description' => 'Dummy resource response', 'schema' => [ - '$ref' => '#/definitions/Dummy:300dcd476cef011532fb0ca7683395d7', + '$ref' => '#/definitions/Dummy:OutputDto', ], ], 404 => [ @@ -2676,7 +2871,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 'in' => 'body', 'description' => 'The updated Dummy resource', 'schema' => [ - '$ref' => '#/definitions/Dummy:b4f76c1a44965bd401aa23bb37618acc', + '$ref' => '#/definitions/Dummy:InputDto', ], ], ], @@ -2684,7 +2879,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void 200 => [ 'description' => 'Dummy resource updated', 'schema' => [ - '$ref' => '#/definitions/Dummy:300dcd476cef011532fb0ca7683395d7', + '$ref' => '#/definitions/Dummy:OutputDto', ], ], 400 => [ @@ -2698,7 +2893,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void ], ]), 'definitions' => new \ArrayObject([ - 'Dummy:300dcd476cef011532fb0ca7683395d7' => new \ArrayObject([ + 'Dummy:OutputDto' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => [ @@ -2716,7 +2911,7 @@ private function doTestNormalizeWithInputAndOutputClass(): void ]), ], ]), - 'Dummy:b4f76c1a44965bd401aa23bb37618acc' => new \ArrayObject([ + 'Dummy:InputDto' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => [ diff --git a/update-js.sh b/update-js.sh index d1efaa52df2..0d425d01540 100755 --- a/update-js.sh +++ b/update-js.sh @@ -15,6 +15,7 @@ cp node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js "$dest" cp node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js.map "$dest" cp node_modules/swagger-ui-dist/swagger-ui.css "$dest" cp node_modules/swagger-ui-dist/swagger-ui.css.map "$dest" +cp node_modules/swagger-ui-dist/oauth2-redirect.html "$dest" dest=src/Bridge/Symfony/Bundle/Resources/public/react/ if [[ -d "$dest" ]]; then