From 791692c8f38c582b2ee9ae6557db32dcbdb6b5cb Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:22:03 +0200 Subject: [PATCH 01/22] #1 Update README. Add docs, CHANGELOG & CONTRIBUTING. --- .gitignore | 7 +++++ CHANGELOG.md | 41 ++++++++++++++++++++++++ CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++ LICENSE | 2 +- README.md | 20 +++++++++++- docs/index.md | 25 +++++++++++++++ docs/reference/tasks/_template.md | 44 ++++++++++++++++++++++++++ 7 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/index.md create mode 100644 docs/reference/tasks/_template.md diff --git a/.gitignore b/.gitignore index ff72e2d..ca08796 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ /composer.lock /vendor +.env +.idea +/phpunit.xml +.phpunit.result.cache +.phpunit.cache +.php-cs-fixer.cache +coverage-report diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8646ea0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +v2.0 +------ + +## BC breaks + +### Changes + +### Fixes + +v1.0.4 +------ + +### Changes + +* Fixed dependencies after removing sidus/base-bundle from the base process bundle + +v1.0.3 +------ + +### Changes + +* Minor refactoring in RequestTask to allow override of options more easily + +v1.0.2 +------ + +### Fixes + +* Fixing trailing '?'/'&' in request uri + +v1.0.1 +------ + +### Changes + +* Adding debug information in RequestTask + +v1.0.0 +------ + +* Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9a8dfba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +Contributing +============ + +First of all, **thank you** for contributing, **you are awesome**! + +Here are a few rules to follow in order to ease code reviews, and discussions before +maintainers accept and merge your work. + +You MUST run the quality & test suites. + +You SHOULD write (or update) unit tests. + +You SHOULD write documentation. + +Please, write [commit messages that make sense](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), +and [rebase your branch](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) before submitting your Pull Request. + +One may ask you to [squash your commits](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +too. This is used to "clean" your Pull Request before merging it (we don't want +commits such as `fix tests`, `fix 2`, `fix 3`, etc.). + +Thank you! + +## Running the quality & test suites + +Tests suite uses Docker environments in order to be idempotent to OS's. More than this +PHP version is written inside the Dockerfile; this assures to test the bundle with +the same resources. No need to have PHP installed. + +You only need Docker set it up. + +To allow testing environments more smooth we implemented **Makefile**. +You have two commands available: + +```bash +make quality +``` + +```bash +make tests +``` + +## Deprecations notices + +When a feature should be deprecated, or when you have a breaking change for a future version, please : +* [Fill an issue](https://github.com/cleverage/rest-process-bundle/issues/new) +* Add TODO comments with the following format: `@TODO deprecated v2.0` +* Trigger a deprecation error: `@trigger_error('This feature will be deprecated in v2.0', E_USER_DEPRECATED);` + +You can check which deprecation notice is triggered in tests +* `make bash` +* `SYMFONY_DEPRECATIONS_HELPER=0 ./vendor/bin/phpunit` diff --git a/LICENSE b/LICENSE index fdc6131..045d824 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2015-2019 Clever-Age +Copyright (c) Clever-Age Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9c3b0fe..6b00965 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,22 @@ CleverAge/RestProcessBundle ======================= -See process bundle documentation +This bundle is a part of the [CleverAge/ProcessBundle](https://github.com/cleverage/process-bundle) project. +It provides [Rest](https://fr.wikipedia.org/wiki/Representational_state_transfer) integration on Process bundle. + +Compatible with [Symfony stable version and latest Long-Term Support (LTS) release](https://symfony.com/releases). + +## Documentation + +For usage documentation, see: +[docs/index.md](doc/index.md) + +## Support & Contribution + +For general support and questions, please use [Github](https://github.com/cleverage/rest-process-bundle/issues). +If you think you found a bug or you have a feature idea to propose, feel free to open an issue after looking at the [contributing](CONTRIBUTING.md) guide. + +## License + +This bundle is under the MIT license. +For the whole copyright, see the [LICENSE](LICENSE) file distributed with this source code. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c5c2a75 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,25 @@ +## Prerequisite + +CleverAge/ProcessBundle must be [installed](https://github.com/cleverage/process-bundle/blob/main/docs/01-quick_start.md#installation. + +## Installation + +Make sure Composer is installed globally, as explained in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + +Open a command console, enter your project directory and install it using composer: + +```bash +composer require cleverage/rest-process-bundle +``` + +Remember to add the following line to config/bundles.php (not required if Symfony Flex is used) + +```php +CleverAge\FlysystemProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true], +``` + +## Reference + +- Tasks + - [RequestTask] diff --git a/docs/reference/tasks/_template.md b/docs/reference/tasks/_template.md new file mode 100644 index 0000000..ed1d4a5 --- /dev/null +++ b/docs/reference/tasks/_template.md @@ -0,0 +1,44 @@ +TaskName +======== + +_Describe main goal an use cases of the task_ + +Task reference +-------------- + +* **Service**: `ClassName` + +Accepted inputs +--------------- + +_Description of allowed types_ + +Possible outputs +---------------- + +_Description of possible types_ + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `code` | `type` | **X** _or nothing_ | `default value` _if available_ | _description_ | + +Examples +-------- + +_YAML samples and explanations_ + +* Example 1 + - details + - details + +```yaml +# Task configuration level +code: + service: '@service_ref' + options: + a: 1 + b: 2 +``` From bfbad3946ac6d77fa16cf5c358bdbfaae768f02c Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:28:32 +0200 Subject: [PATCH 02/22] #1 Add Makefile & .docker for local standalone usage. Add rector, phpstan & php-cs-fixer configurations --- .docker/compose.yaml | 14 ++++++++++ .docker/php/Dockerfile | 29 ++++++++++++++++++++ .docker/php/conf.d/dev.ini | 5 ++++ .php-cs-fixer.dist.php | 46 ++++++++++++++++++++++++++++++++ Makefile | 54 ++++++++++++++++++++++++++++++++++++++ composer.json | 25 ++++++++++++++++-- phpstan.neon | 18 +++++++++++++ phpunit.xml.dist | 27 +++++++++++++++++++ rector.php | 30 +++++++++++++++++++++ tests/.gitkeep | 0 10 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 .docker/compose.yaml create mode 100644 .docker/php/Dockerfile create mode 100644 .docker/php/conf.d/dev.ini create mode 100644 .php-cs-fixer.dist.php create mode 100644 Makefile create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 rector.php create mode 100644 tests/.gitkeep diff --git a/.docker/compose.yaml b/.docker/compose.yaml new file mode 100644 index 0000000..66cc06e --- /dev/null +++ b/.docker/compose.yaml @@ -0,0 +1,14 @@ +x-build-args: &build-args + UID: "${UID:-1000}" + GID: "${GID:-1000}" + +name: cleverage-rest-process-bundle + +services: + php: + build: + context: php + args: + <<: *build-args + volumes: + - ../:/var/www diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000..f98c3ba --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.2-fpm-alpine + +ARG UID +ARG GID + +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" +COPY /conf.d/ "$PHP_INI_DIR/conf.d/" + +RUN apk update && apk add \ + tzdata \ + shadow \ + nano \ + bash \ + icu-dev \ + && docker-php-ext-configure intl \ + && docker-php-ext-install intl opcache \ + && docker-php-ext-enable opcache + +RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \ + && sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN usermod -u $UID www-data \ + && groupmod -g $GID www-data + +USER www-data:www-data + +WORKDIR /var/www diff --git a/.docker/php/conf.d/dev.ini b/.docker/php/conf.d/dev.ini new file mode 100644 index 0000000..2a141be --- /dev/null +++ b/.docker/php/conf.d/dev.ini @@ -0,0 +1,5 @@ +display_errors = 1 +error_reporting = E_ALL + +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7e90154 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,46 @@ +setRules([ + '@PHP71Migration' => true, + '@PHP82Migration' => true, + '@PHPUnit75Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'protected_to_private' => false, + 'native_constant_invocation' => ['strict' => false], + 'header_comment' => ['header' => $fileHeaderComment], + 'modernize_strpos' => true, + 'get_class_to_class_keyword' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->append([__FILE__]) + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0a58e32 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.ONESHELL: +SHELL := /bin/bash + +DOCKER_RUN_PHP = docker compose -f .docker/compose.yaml run --rm php "bash" "-c" +DOCKER_COMPOSE = docker compose -f .docker/compose.yaml + +start: upd #[Global] Start application + +src/vendor: #[Composer] install dependencies + $(DOCKER_RUN_PHP) "composer install --no-interaction" + +upd: #[Docker] Start containers detached + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans --detach + +up: #[Docker] Start containers + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans + +stop: #[Docker] Down containers + $(DOCKER_COMPOSE) stop + +down: #[Docker] Down containers + $(DOCKER_COMPOSE) down + +build: #[Docker] Build containers + $(DOCKER_COMPOSE) build + +ps: # [Docker] Show running containers + $(DOCKER_COMPOSE) ps + +bash: #[Docker] Connect to php container with current host user + $(DOCKER_COMPOSE) exec php bash + +logs: #[Docker] Show logs + $(DOCKER_COMPOSE) logs -f + +quality: phpstan php-cs-fixer rector #[Quality] Run all quality checks + +phpstan: #[Quality] Run PHPStan + $(DOCKER_RUN_PHP) "vendor/bin/phpstan --no-progress --memory-limit=1G analyse" + +php-cs-fixer: #[Quality] Run PHP-CS-Fixer + $(DOCKER_RUN_PHP) "vendor/bin/php-cs-fixer fix --diff --verbose" + +rector: #[Quality] Run Rector + $(DOCKER_RUN_PHP) "vendor/bin/rector" + +tests: phpunit #[Tests] Run all tests + +phpunit: #[Tests] Run PHPUnit + $(DOCKER_RUN_PHP) "vendor/bin/phpunit" diff --git a/composer.json b/composer.json index fb0c16f..a9502ff 100644 --- a/composer.json +++ b/composer.json @@ -37,12 +37,33 @@ "CleverAge\\RestProcessBundle\\": "" } }, + "autoload-dev": { + "psr-4": { + "CleverAge\\FlysystemProcessBundle\\Tests\\": "tests/" + } + }, "require": { - "cleverage/process-bundle": "3.*|dev-v3.0-dev", + "php": ">=8.1", + "cleverage/process-bundle": "dev-prepare-release", "nategood/httpful": ">0.2.0, <1.0.0", "sidus/base-bundle": "~1.0" }, "require-dev": { - "phpunit/phpunit": "~6.4" + "friendsofphp/php-cs-fixer": "*", + "phpstan/extension-installer": "*", + "phpstan/phpstan": "*", + "phpstan/phpstan-symfony": "*", + "phpunit/phpunit": "*", + "rector/rector": "*", + "roave/security-advisories": "dev-latest", + "symfony/test-pack": "^1.1" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "sort-packages": true } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..30e9572 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,18 @@ +parameters: + level: 6 + paths: + - src + - tests + ignoreErrors: + - '#type has no value type specified in iterable type#' + - '#has parameter .* with no value type specified in iterable type#' + - '#has no value type specified in iterable type array#' + - '#configureOptions\(\) has no return type specified.#' + - '#configure\(\) has no return type specified#' + - '#process\(\) has no return type specified#' + - '#should return Iterator but returns Traversable#' + - '#Negated boolean expression is always false#' + checkGenericClassInNonGenericObjectType: false + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..72a2408 --- /dev/null +++ b/rector.php @@ -0,0 +1,30 @@ +withPhpVersion(PhpVersion::PHP_82) + ->withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withPhpSets(php82: true) + // here we can define, what prepared sets of rules will be applied + ->withPreparedSets( + deadCode: true, + codeQuality: true + ) + ->withSets([ + LevelSetList::UP_TO_PHP_82, + SymfonySetList::SYMFONY_64, + SymfonySetList::SYMFONY_71, + SymfonySetList::SYMFONY_CODE_QUALITY, + SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, + SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, + ]) +; diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 From 87c407ff7877b6ae217f70cc80559c4d184e2608 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:28:55 +0200 Subject: [PATCH 03/22] #1 Add notifications, quality & test github workflows --- .github/ISSUE_TEMPLATE.md | 14 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 14 ++++++ .github/workflows/notifications.yml | 23 +++++++++ .github/workflows/quality.yml | 62 ++++++++++++++++++++++++ .github/workflows/test.yml | 74 +++++++++++++++++++++++++++++ 5 files changed, 187 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/notifications.yml create mode 100644 .github/workflows/quality.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..7711713 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..58db37d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 0000000..e7974ab --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,23 @@ +name: Rocket chat notifications + +# Controls when the action will run. +on: + push: + tags: + - '*' + +jobs: + notification: + runs-on: ubuntu-latest + + steps: + - name: Get the tag short reference + id: get_tag + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + + - name: Rocket.Chat Notification + uses: madalozzo/Rocket.Chat.GitHub.Action.Notification@master + with: + type: success + job_name: "[cleverage/rest-process-bundle](https://github.com/cleverage/rest-process-bundle) : ${{ steps.get_tag.outputs.TAG }} has been released" + url: ${{ secrets.CLEVER_AGE_ROCKET_CHAT_WEBOOK_URL }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..9f1580f --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,62 @@ +name: Quality + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHPStan + run: vendor/bin/phpstan --no-progress --memory-limit=1G analyse --error-format=github + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHP-CS-Fixer + run: vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none + + rector: + name: Rector + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: Rector + run: vendor/bin/rector --no-progress-bar --dry-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2d7e7a4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allowed-to-fail }} + env: + SYMFONY_REQUIRE: ${{matrix.symfony-require}} + + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + dependencies: [highest] + allowed-to-fail: [false] + symfony-require: [''] + variant: [normal] + include: + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + tools: composer:v2, flex + - name: Add PHPUnit matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install variant + if: matrix.variant != 'normal' && !startsWith(matrix.variant, 'symfony/symfony') + run: composer require ${{ matrix.variant }} --no-update + - name: Install Composer dependencies (${{ matrix.dependencies }}) + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + - name: Run Tests with coverage + run: vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml + #- name: Send coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # files: build/logs/clover.xml From dbdc3e8a434f7e0defba028305f34fd8a9d5abee Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:32:25 +0200 Subject: [PATCH 04/22] #1 Update directory structure with code on /src --- Transformer/RequestTransformer.php | 4 ++-- composer.json | 2 +- {Client => src/Client}/Client.php | 0 {Client => src/Client}/ClientInterface.php | 0 .../DependencyInjection}/CleverAgeRestProcessExtension.php | 0 {Exception => src/Exception}/MissingClientException.php | 0 {Exception => src/Exception}/RestException.php | 0 {Exception => src/Exception}/RestRequestException.php | 0 {Registry => src/Registry}/ClientRegistry.php | 0 {Resources => src/Resources}/config/services/registry.yml | 0 {Resources => src/Resources}/config/services/task.yml | 0 {Resources => src/Resources}/config/services/transformer.yml | 0 {Task => src/Task}/RequestTask.php | 4 ++-- 13 files changed, 5 insertions(+), 5 deletions(-) rename {Client => src/Client}/Client.php (100%) rename {Client => src/Client}/ClientInterface.php (100%) rename {DependencyInjection => src/DependencyInjection}/CleverAgeRestProcessExtension.php (100%) rename {Exception => src/Exception}/MissingClientException.php (100%) rename {Exception => src/Exception}/RestException.php (100%) rename {Exception => src/Exception}/RestRequestException.php (100%) rename {Registry => src/Registry}/ClientRegistry.php (100%) rename {Resources => src/Resources}/config/services/registry.yml (100%) rename {Resources => src/Resources}/config/services/task.yml (100%) rename {Resources => src/Resources}/config/services/transformer.yml (100%) rename {Task => src/Task}/RequestTask.php (100%) diff --git a/Transformer/RequestTransformer.php b/Transformer/RequestTransformer.php index d021586..57ab434 100644 --- a/Transformer/RequestTransformer.php +++ b/Transformer/RequestTransformer.php @@ -10,9 +10,9 @@ namespace CleverAge\RestProcessBundle\Transformer; -use CleverAge\RestProcessBundle\Registry\ClientRegistry; use CleverAge\ProcessBundle\Exception\TransformerException; use CleverAge\ProcessBundle\Transformer\ConfigurableTransformerInterface; +use CleverAge\RestProcessBundle\Registry\ClientRegistry; use Psr\Log\LoggerInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -52,7 +52,7 @@ public function __construct(LoggerInterface $logger, ClientRegistry $registry) * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \CleverAge\RestProcessBundle\Exception\MissingClientException + * @throws \CleverAge\RestProcessBundle\src\Exception\MissingClientException */ public function transform($value, array $options = []) { diff --git a/composer.json b/composer.json index a9502ff..06ab2ea 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ ], "autoload": { "psr-4": { - "CleverAge\\RestProcessBundle\\": "" + "CleverAge\\FlysystemProcessBundle\\": "src/" } }, "autoload-dev": { diff --git a/Client/Client.php b/src/Client/Client.php similarity index 100% rename from Client/Client.php rename to src/Client/Client.php diff --git a/Client/ClientInterface.php b/src/Client/ClientInterface.php similarity index 100% rename from Client/ClientInterface.php rename to src/Client/ClientInterface.php diff --git a/DependencyInjection/CleverAgeRestProcessExtension.php b/src/DependencyInjection/CleverAgeRestProcessExtension.php similarity index 100% rename from DependencyInjection/CleverAgeRestProcessExtension.php rename to src/DependencyInjection/CleverAgeRestProcessExtension.php diff --git a/Exception/MissingClientException.php b/src/Exception/MissingClientException.php similarity index 100% rename from Exception/MissingClientException.php rename to src/Exception/MissingClientException.php diff --git a/Exception/RestException.php b/src/Exception/RestException.php similarity index 100% rename from Exception/RestException.php rename to src/Exception/RestException.php diff --git a/Exception/RestRequestException.php b/src/Exception/RestRequestException.php similarity index 100% rename from Exception/RestRequestException.php rename to src/Exception/RestRequestException.php diff --git a/Registry/ClientRegistry.php b/src/Registry/ClientRegistry.php similarity index 100% rename from Registry/ClientRegistry.php rename to src/Registry/ClientRegistry.php diff --git a/Resources/config/services/registry.yml b/src/Resources/config/services/registry.yml similarity index 100% rename from Resources/config/services/registry.yml rename to src/Resources/config/services/registry.yml diff --git a/Resources/config/services/task.yml b/src/Resources/config/services/task.yml similarity index 100% rename from Resources/config/services/task.yml rename to src/Resources/config/services/task.yml diff --git a/Resources/config/services/transformer.yml b/src/Resources/config/services/transformer.yml similarity index 100% rename from Resources/config/services/transformer.yml rename to src/Resources/config/services/transformer.yml diff --git a/Task/RequestTask.php b/src/Task/RequestTask.php similarity index 100% rename from Task/RequestTask.php rename to src/Task/RequestTask.php index 0e6502f..d31adc6 100644 --- a/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -10,11 +10,11 @@ namespace CleverAge\RestProcessBundle\Task; -use CleverAge\RestProcessBundle\Exception\MissingClientException; -use CleverAge\RestProcessBundle\Registry\ClientRegistry; use CleverAge\ProcessBundle\Configuration\TaskConfiguration; use CleverAge\ProcessBundle\Model\AbstractConfigurableTask; use CleverAge\ProcessBundle\Model\ProcessState; +use CleverAge\RestProcessBundle\Exception\MissingClientException; +use CleverAge\RestProcessBundle\Registry\ClientRegistry; use Psr\Log\LoggerInterface; use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; From 20f638c87b2ec70b21d75db424e9cc654a95d7b5 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:43:33 +0200 Subject: [PATCH 05/22] #3 Temporary fix nategood/httpful version dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 06ab2ea..fd9ba2f 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "require": { "php": ">=8.1", "cleverage/process-bundle": "dev-prepare-release", - "nategood/httpful": ">0.2.0, <1.0.0", + "nategood/httpful": "^1.0.0", "sidus/base-bundle": "~1.0" }, "require-dev": { From bb7d26dd1ba4d447f6c38c2aaabc79f615778762 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:49:23 +0200 Subject: [PATCH 06/22] #2 Remove `sidus/base-bundle` dependency --- CHANGELOG.md | 4 +++ composer.json | 7 ++-- .../CleverAgeRestProcessExtension.php | 35 +++++++++++++------ .../services/{registry.yml => registry.yaml} | 0 .../config/services/{task.yml => task.yaml} | 0 .../{transformer.yml => transformer.yaml} | 0 6 files changed, 31 insertions(+), 15 deletions(-) rename src/Resources/config/services/{registry.yml => registry.yaml} (100%) rename src/Resources/config/services/{task.yml => task.yaml} (100%) rename src/Resources/config/services/{transformer.yml => transformer.yaml} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8646ea0..7cf60cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ v2.0 ### Changes +* [#1](https://github.com/cleverage/rest-process-bundle/issues/1) Add Makefile & .docker for local standalone usage +* [#1](https://github.com/cleverage/rest-process-bundle/issues/1) Add rector, phpstan & php-cs-fixer configurations & apply it +* [#2](https://github.com/cleverage/rest-process-bundle/issues/2) Remove `sidus/base-bundle` dependency + ### Fixes v1.0.4 diff --git a/composer.json b/composer.json index fd9ba2f..5ffc42b 100644 --- a/composer.json +++ b/composer.json @@ -34,19 +34,18 @@ ], "autoload": { "psr-4": { - "CleverAge\\FlysystemProcessBundle\\": "src/" + "CleverAge\\RestProcessBundle\\": "src/" } }, "autoload-dev": { "psr-4": { - "CleverAge\\FlysystemProcessBundle\\Tests\\": "tests/" + "CleverAge\\RestProcessBundle\\Tests\\": "tests/" } }, "require": { "php": ">=8.1", "cleverage/process-bundle": "dev-prepare-release", - "nategood/httpful": "^1.0.0", - "sidus/base-bundle": "~1.0" + "nategood/httpful": "^1.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", diff --git a/src/DependencyInjection/CleverAgeRestProcessExtension.php b/src/DependencyInjection/CleverAgeRestProcessExtension.php index 5342cba..aaf17a8 100644 --- a/src/DependencyInjection/CleverAgeRestProcessExtension.php +++ b/src/DependencyInjection/CleverAgeRestProcessExtension.php @@ -10,17 +10,30 @@ namespace CleverAge\RestProcessBundle\DependencyInjection; -use Sidus\BaseBundle\DependencyInjection\SidusBaseExtension; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\Finder\Finder; -/** - * This is the class that loads and manages your bundle configuration. - * - * @see http://symfony.com/doc/current/cookbook/bundles/extension.html - * - * @author Valentin Clavreul - * @author Vincent Chalnot - * @author Madeline Veyrenc - */ -class CleverAgeRestProcessExtension extends SidusBaseExtension +class CleverAgeRestProcessExtension extends Extension { + public function load(array $configs, ContainerBuilder $container): void + { + $this->findServices($container, __DIR__.'/../Resources/config/services'); + } + + /** + * Recursively import config files into container. + */ + protected function findServices(ContainerBuilder $container, string $path, string $extension = 'yaml'): void + { + $finder = new Finder(); + $finder->in($path) + ->name('*.'.$extension)->files(); + $loader = new YamlFileLoader($container, new FileLocator($path)); + foreach ($finder as $file) { + $loader->load($file->getFilename()); + } + } } diff --git a/src/Resources/config/services/registry.yml b/src/Resources/config/services/registry.yaml similarity index 100% rename from src/Resources/config/services/registry.yml rename to src/Resources/config/services/registry.yaml diff --git a/src/Resources/config/services/task.yml b/src/Resources/config/services/task.yaml similarity index 100% rename from src/Resources/config/services/task.yml rename to src/Resources/config/services/task.yaml diff --git a/src/Resources/config/services/transformer.yml b/src/Resources/config/services/transformer.yaml similarity index 100% rename from src/Resources/config/services/transformer.yml rename to src/Resources/config/services/transformer.yaml From 571e2cbacd5136a780ad17a229f8e151c6f072a6 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 10:53:13 +0200 Subject: [PATCH 07/22] #1 Apply phpstan, php-cs-fixer & rector rules --- src/Client/Client.php | 106 +++--------------- src/Client/ClientInterface.php | 30 ++--- .../CleverAgeRestProcessExtension.php | 9 +- src/Exception/MissingClientException.php | 13 ++- src/Exception/RestException.php | 14 ++- src/Exception/RestRequestException.php | 14 ++- src/Registry/ClientRegistry.php | 24 ++-- src/Task/RequestTask.php | 30 ++--- 8 files changed, 77 insertions(+), 163 deletions(-) diff --git a/src/Client/Client.php b/src/Client/Client.php index 759f45c..1fc567c 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -1,8 +1,11 @@ - */ class Client implements ClientInterface { - /** @var LoggerInterface */ - private $logger; - - /** @var string */ - private $code; - - /** @var string */ - private $uri; - /** * Shopify constructor. - * - * @param LoggerInterface $logger - * @param string $code - * @param string $uri */ - public function __construct(LoggerInterface $logger, string $code, string $uri) + public function __construct(private readonly LoggerInterface $logger, private readonly string $code, private string $uri) { - $this->logger = $logger; - $this->code = $code; - $this->uri = $uri; } - /** - * @return LoggerInterface - */ public function getLogger(): LoggerInterface { return $this->logger; } - /** - * @return string - */ public function getCode(): string { return $this->code; } - /** - * @return string - */ public function geUri(): string { return $this->uri; } - /** - * @param string $uri - */ public function setUri(string $uri): void { $this->uri = $uri; } /** - * @param array $options - * * @throws \Exception - * - * @return Response */ public function call(array $options = []): Response { @@ -97,9 +68,6 @@ public function call(array $options = []): Response return $this->sendRequest($request, $options); } - /** - * @param OptionsResolver $resolver - */ protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired( @@ -129,11 +97,6 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('headers', ['array']); } - /** - * @param array $options - * - * @return array - */ protected function getOptions(array $options = []): array { $resolver = new OptionsResolver(); @@ -143,21 +106,16 @@ protected function getOptions(array $options = []): array } /** - * @param array $options - * * @throws RestRequestException - * - * @return Request - * */ protected function initializeRequest(array $options = []): Request { - if (!in_array( + if (!\in_array( $options['method'], [Http::HEAD, Http::GET, Http::POST, Http::PUT, Http::DELETE, Http::OPTIONS, Http::TRACE, Http::PATCH], true )) { - throw new RestRequestException(sprintf('%s is not an HTTP method', $options['method'])); + throw new RestRequestException(\sprintf('%s is not an HTTP method', $options['method'])); } $request = Request::init($options['method']); $request->sends($options['sends']); @@ -168,22 +126,18 @@ protected function initializeRequest(array $options = []): Request } /** - * @param Request $request - * @param array $options - * - * * @throws \Exception */ protected function setRequestQueryParameters(Request $request, array $options = []): void { $uri = $this->constructUri($options); if (Http::GET === $options['method']) { - if (is_array($options['query_parameters'])) { + if (\is_array($options['query_parameters'])) { $parametersString = http_build_query($options['query_parameters']); } else { $parametersString = (string) $options['query_parameters']; } - if ($parametersString) { + if ('' !== $parametersString && '0' !== $parametersString) { $uri .= strpos($uri, '?') ? '&' : '?'; $uri .= $parametersString; } @@ -195,12 +149,6 @@ protected function setRequestQueryParameters(Request $request, array $options = $request->uri($uri); } - /** - * @param Request $request - * @param array $options - * - * - */ protected function setRequestHeader(Request $request, array $options = []): void { if ($options['headers']) { @@ -209,12 +157,7 @@ protected function setRequestHeader(Request $request, array $options = []): void } /** - * @param Request $request - * @param array $options - * * @throws RestRequestException - * - * @return Response|null */ protected function sendRequest(Request $request, array $options = []): ?Response { @@ -232,36 +175,21 @@ protected function sendRequest(Request $request, array $options = []): ?Response } } - /** - * @return string - */ protected function getApiUrl(): string { - return sprintf('%s', $this->geUri()); + return $this->geUri(); } - /** - * @param array $options - * - * @return string - */ protected function constructUri(array $options): string { - $uri = ltrim($options['url'], '/'); + $uri = ltrim((string) $options['url'], '/'); - return sprintf('%s/%s', $this->getApiUrl(), $uri); + return \sprintf('%s/%s', $this->getApiUrl(), $uri); } - /** - * @param string $uri - * @param array $options - * - * @return string - * - */ protected function replaceParametersInUri(string $uri, array $options = []): string { - if (array_key_exists('url_parameters', $options) && $options['url_parameters']) { + if (\array_key_exists('url_parameters', $options) && $options['url_parameters']) { $search = array_keys($options['url_parameters']); array_walk( $search, diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php index 437f593..2083293 100644 --- a/src/Client/ClientInterface.php +++ b/src/Client/ClientInterface.php @@ -1,8 +1,11 @@ - */ @@ -21,31 +24,18 @@ interface ClientInterface { /** * Return the code of the client used in client registry. - * - * @return string */ public function getCode(): string; /** - * Return the URI - * - * @return string + * Return the URI. */ public function geUri(): string; /** - * Set the URI - * - * @param string $uri - * - * @return void + * Set the URI. */ public function setUri(string $uri): void; - /** - * @param array $options - * - * @return \Httpful\Response - */ public function call(array $options = []): Response; } diff --git a/src/DependencyInjection/CleverAgeRestProcessExtension.php b/src/DependencyInjection/CleverAgeRestProcessExtension.php index aaf17a8..30c7ecc 100644 --- a/src/DependencyInjection/CleverAgeRestProcessExtension.php +++ b/src/DependencyInjection/CleverAgeRestProcessExtension.php @@ -1,8 +1,11 @@ - */ diff --git a/src/Exception/RestException.php b/src/Exception/RestException.php index c09f5a0..786a041 100644 --- a/src/Exception/RestException.php +++ b/src/Exception/RestException.php @@ -1,8 +1,11 @@ - */ class RestException extends \Exception { - } diff --git a/src/Exception/RestRequestException.php b/src/Exception/RestRequestException.php index 7bc2287..574a97d 100644 --- a/src/Exception/RestRequestException.php +++ b/src/Exception/RestRequestException.php @@ -1,8 +1,11 @@ - */ class RestRequestException extends RestException { - } diff --git a/src/Registry/ClientRegistry.php b/src/Registry/ClientRegistry.php index 17cf58f..ef3f57f 100644 --- a/src/Registry/ClientRegistry.php +++ b/src/Registry/ClientRegistry.php @@ -1,8 +1,11 @@ - */ @@ -23,12 +26,9 @@ class ClientRegistry /** @var ClientInterface[] */ private $clients = []; - /** - * @param ClientInterface $client - */ public function addClient(ClientInterface $client): void { - if (array_key_exists($client->getCode(), $this->getClients())) { + if (\array_key_exists($client->getCode(), $this->getClients())) { throw new \UnexpectedValueException("Client {$client->getCode()} is already defined"); } $this->clients[$client->getCode()] = $client; @@ -46,8 +46,6 @@ public function getClients(): array * @param string $code * * @throws MissingClientException - * - * @return ClientInterface */ public function getClient($code): ClientInterface { @@ -60,11 +58,9 @@ public function getClient($code): ClientInterface /** * @param string $code - * - * @return bool */ public function hasClient($code): bool { - return array_key_exists($code, $this->getClients()); + return \array_key_exists($code, $this->getClients()); } } diff --git a/src/Task/RequestTask.php b/src/Task/RequestTask.php index d31adc6..5321869 100644 --- a/src/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -1,8 +1,11 @@ - */ @@ -34,10 +37,6 @@ class RequestTask extends AbstractConfigurableTask /** @var ClientRegistry */ protected $registry; - /** - * @param LoggerInterface $logger - * @param ClientRegistry $registry - */ public function __construct(LoggerInterface $logger, ClientRegistry $registry) { $this->logger = $logger; @@ -45,9 +44,6 @@ public function __construct(LoggerInterface $logger, ClientRegistry $registry) } /** - * {@inheritdoc} - * @param ProcessState $state - * * @throws MissingClientException * @throws ExceptionInterface */ @@ -85,9 +81,9 @@ public function execute(ProcessState $state): void ); $state->setErrorOutput($result->body); - if ($state->getTaskConfiguration()->getErrorStrategy() === TaskConfiguration::STRATEGY_SKIP) { + if (TaskConfiguration::STRATEGY_SKIP === $state->getTaskConfiguration()->getErrorStrategy()) { $state->setSkipped(true); - } elseif ($state->getTaskConfiguration()->getErrorStrategy() === TaskConfiguration::STRATEGY_STOP) { + } elseif (TaskConfiguration::STRATEGY_STOP === $state->getTaskConfiguration()->getErrorStrategy()) { $state->setStopped(true); } @@ -98,8 +94,6 @@ public function execute(ProcessState $state): void } /** - * @param OptionsResolver $resolver - * * @throws UndefinedOptionsException * @throws AccessException */ @@ -132,11 +126,7 @@ protected function configureOptions(OptionsResolver $resolver): void } /** - * @param ProcessState $state - * * @throws ExceptionInterface - * - * @return array */ protected function getRequestOptions(ProcessState $state): array { From eff77dba3e6582578338a959dbe4b8b99d1f7a30 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 14:12:13 +0200 Subject: [PATCH 08/22] #3 Replace `nategood/httpful` dependency by `symfony/http-client` --- CHANGELOG.md | 2 + composer.json | 2 +- docs/index.md | 2 + src/Client/Client.php | 142 +++++++----------- src/Client/ClientInterface.php | 15 +- src/Registry/ClientRegistry.php | 13 +- src/Task/RequestTask.php | 54 +++---- .../Transformer}/RequestTransformer.php | 76 ++++------ 8 files changed, 113 insertions(+), 193 deletions(-) rename {Transformer => src/Transformer}/RequestTransformer.php (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cf60cd..be94c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ v2.0 ## BC breaks +* [#3](https://github.com/cleverage/rest-process-bundle/issues/3) Replace `nategood/httpful` dependency by `symfony/http-client` + ### Changes * [#1](https://github.com/cleverage/rest-process-bundle/issues/1) Add Makefile & .docker for local standalone usage diff --git a/composer.json b/composer.json index 5ffc42b..5ae2e0f 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "require": { "php": ">=8.1", "cleverage/process-bundle": "dev-prepare-release", - "nategood/httpful": "^1.0.0" + "symfony/http-client": "^6.4|^7.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", diff --git a/docs/index.md b/docs/index.md index c5c2a75..a46c712 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,3 +23,5 @@ CleverAge\FlysystemProcessBundle\CleverAgeRestProcessBundle::class => ['all' => - Tasks - [RequestTask] +- Transformers + - [RequestTransformer] diff --git a/src/Client/Client.php b/src/Client/Client.php index 1fc567c..f6beff5 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -14,24 +14,19 @@ namespace CleverAge\RestProcessBundle\Client; use CleverAge\RestProcessBundle\Exception\RestRequestException; -use Httpful\Http; -use Httpful\Request; -use Httpful\Response; use Psr\Log\LoggerInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; -/** - * Class AbstractRestClient. - * - * @author Madeline Veyrenc - */ class Client implements ClientInterface { - /** - * Shopify constructor. - */ - public function __construct(private readonly LoggerInterface $logger, private readonly string $code, private string $uri) - { + public function __construct( + private readonly HttpClientInterface $httpClient, + private readonly LoggerInterface $logger, + private readonly string $code, + private string $uri, + ) { } public function getLogger(): LoggerInterface @@ -55,17 +50,36 @@ public function setUri(string $uri): void } /** - * @throws \Exception + * @throws RestRequestException */ - public function call(array $options = []): Response + public function call(array $options = []): ResponseInterface { $options = $this->getOptions($options); - $request = $this->initializeRequest($options); - $this->setRequestQueryParameters($request, $options); - $this->setRequestHeader($request, $options); + if (!\in_array( + $options['method'], + ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'PATCH'], + true + )) { + throw new RestRequestException(\sprintf('%s is not an HTTP method', $options['method'])); + } - return $this->sendRequest($request, $options); + try { + return $this->httpClient->request( + $options['method'], + $this->getRequestUri($options), + $this->getRequestOptions($options), + ); + } catch (\Exception|\Throwable $e) { + $this->logger->error( + 'Rest request failed', + [ + 'url' => $this->getRequestUri($options), + 'error' => $e->getMessage(), + ] + ); + throw new RestRequestException('Rest request failed', 0, $e); + } } protected function configureOptions(OptionsResolver $resolver): void @@ -82,8 +96,8 @@ protected function configureOptions(OptionsResolver $resolver): void 'url_parameters' => [], 'query_parameters' => [], 'headers' => [], - 'sends' => 'json', - 'expects' => 'json', + 'sends' => 'application/json', + 'expects' => 'application/json', 'body' => null, ] ); @@ -105,79 +119,30 @@ protected function getOptions(array $options = []): array return $resolver->resolve($options); } - /** - * @throws RestRequestException - */ - protected function initializeRequest(array $options = []): Request + protected function getRequestUri(array $options = []): string { - if (!\in_array( - $options['method'], - [Http::HEAD, Http::GET, Http::POST, Http::PUT, Http::DELETE, Http::OPTIONS, Http::TRACE, Http::PATCH], - true - )) { - throw new RestRequestException(\sprintf('%s is not an HTTP method', $options['method'])); - } - $request = Request::init($options['method']); - $request->sends($options['sends']); - $request->expects($options['expects']); - $request->body($options['body']); - - return $request; + return $this->replaceParametersInUri($this->constructUri($options), $options); } - /** - * @throws \Exception - */ - protected function setRequestQueryParameters(Request $request, array $options = []): void + protected function getRequestOptions(array $options = []): array { - $uri = $this->constructUri($options); - if (Http::GET === $options['method']) { - if (\is_array($options['query_parameters'])) { - $parametersString = http_build_query($options['query_parameters']); - } else { - $parametersString = (string) $options['query_parameters']; - } - if ('' !== $parametersString && '0' !== $parametersString) { - $uri .= strpos($uri, '?') ? '&' : '?'; - $uri .= $parametersString; - } - } elseif ($options['query_parameters']) { - $request->body($options['query_parameters']); + $requestOptions = []; + $requestOptions['headers'] = empty($options['headers']) ? [] : $options['headers']; + if (!empty($options['sends'])) { + $requestOptions['headers']['Content-Type'] = $options['sends']; } - - $uri = $this->replaceParametersInUri($uri, $options); - $request->uri($uri); - } - - protected function setRequestHeader(Request $request, array $options = []): void - { - if ($options['headers']) { - $request->addHeaders($options['headers']); + if (!empty($options['expects'])) { + $requestOptions['headers']['Accept'] = $options['expects']; } - } - - /** - * @throws RestRequestException - */ - protected function sendRequest(Request $request, array $options = []): ?Response - { - try { - return $request->send(); - } catch (\Exception $e) { - $this->logger->error( - 'Rest request failed', - [ - 'url' => $request->uri, - 'error' => $e->getMessage(), - ] - ); - throw new RestRequestException('Rest request failed', 0, $e); + if ('POST' === $options['method'] && 'application/json' === $options['sends']) { + $requestOptions['json'] = $options['body']; + } elseif ('GET' === $options['method']) { + $requestOptions['query'] = $options['query_parameters']; + } else { + $requestOptions['body'] = $options['body']; } - } - protected function getApiUrl(): string - { - return $this->geUri(); + return $requestOptions; } protected function constructUri(array $options): string @@ -187,6 +152,11 @@ protected function constructUri(array $options): string return \sprintf('%s/%s', $this->getApiUrl(), $uri); } + protected function getApiUrl(): string + { + return $this->geUri(); + } + protected function replaceParametersInUri(string $uri, array $options = []): string { if (\array_key_exists('url_parameters', $options) && $options['url_parameters']) { diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php index 2083293..7e066e1 100644 --- a/src/Client/ClientInterface.php +++ b/src/Client/ClientInterface.php @@ -13,13 +13,8 @@ namespace CleverAge\RestProcessBundle\Client; -use Httpful\Response; +use Symfony\Contracts\HttpClient\ResponseInterface; -/** - * Interface ClientInterface. - * - * @author Madeline Veyrenc - */ interface ClientInterface { /** @@ -27,15 +22,9 @@ interface ClientInterface */ public function getCode(): string; - /** - * Return the URI. - */ public function geUri(): string; - /** - * Set the URI. - */ public function setUri(string $uri): void; - public function call(array $options = []): Response; + public function call(array $options = []): ResponseInterface; } diff --git a/src/Registry/ClientRegistry.php b/src/Registry/ClientRegistry.php index ef3f57f..828852c 100644 --- a/src/Registry/ClientRegistry.php +++ b/src/Registry/ClientRegistry.php @@ -18,13 +18,11 @@ /** * Holds all tagged rest client services. - * - * @author Madeline Veyrenc */ class ClientRegistry { /** @var ClientInterface[] */ - private $clients = []; + private array $clients = []; public function addClient(ClientInterface $client): void { @@ -43,11 +41,9 @@ public function getClients(): array } /** - * @param string $code - * * @throws MissingClientException */ - public function getClient($code): ClientInterface + public function getClient(string $code): ClientInterface { if (!$this->hasClient($code)) { throw MissingClientException::create($code); @@ -56,10 +52,7 @@ public function getClient($code): ClientInterface return $this->getClients()[$code]; } - /** - * @param string $code - */ - public function hasClient($code): bool + public function hasClient(string $code): bool { return \array_key_exists($code, $this->getClients()); } diff --git a/src/Task/RequestTask.php b/src/Task/RequestTask.php index 5321869..3ac70bc 100644 --- a/src/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -20,32 +20,17 @@ use CleverAge\RestProcessBundle\Registry\ClientRegistry; use Psr\Log\LoggerInterface; use Symfony\Component\OptionsResolver\Exception\AccessException; -use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * Class RequestTask. - * - * @author Madeline Veyrenc - */ class RequestTask extends AbstractConfigurableTask { - /** @var LoggerInterface */ - protected $logger; - - /** @var ClientRegistry */ - protected $registry; - - public function __construct(LoggerInterface $logger, ClientRegistry $registry) + public function __construct(protected LoggerInterface $logger, protected ClientRegistry $registry) { - $this->logger = $logger; - $this->registry = $registry; } /** * @throws MissingClientException - * @throws ExceptionInterface */ public function execute(ProcessState $state): void { @@ -57,40 +42,46 @@ public function execute(ProcessState $state): void "Sending request {$requestOptions['method']} to '{$requestOptions['url']}'", ['requestOptions' => $requestOptions] ); - $result = $this->registry->getClient($options['client'])->call($requestOptions); + $response = $this->registry->getClient($options['client'])->call($requestOptions); if ($options['log_response']) { $this->logger->debug( "Response received from '{$options['url']}'", [ 'requestOptions' => $requestOptions, - 'result' => $result, + 'result' => $response, ] ); } // Handle empty results - if (!\in_array($result->code, $options['valid_response_code'], false)) { + try { + if (!\in_array($response->getStatusCode(), $options['valid_response_code'], false)) { + $state->setErrorOutput($response->getContent()); + + if (TaskConfiguration::STRATEGY_SKIP === $state->getTaskConfiguration()->getErrorStrategy()) { + $state->setSkipped(true); + } elseif (TaskConfiguration::STRATEGY_STOP === $state->getTaskConfiguration()->getErrorStrategy()) { + $state->setStopped(true); + } + + throw new \Exception('Invalid response code'); + } + + $state->setOutput($response->getContent()); + } catch (\Exception|\Throwable $e) { $this->logger->error( 'REST request failed', [ 'client' => $options['client'], 'options' => $options, - 'raw_headers' => $result->raw_headers, - 'raw_body' => $result->raw_body, + 'message' => $e->getMessage(), + 'raw_headers' => $response->getHeaders(false), + 'raw_body' => $response->getContent(false), ] ); - $state->setErrorOutput($result->body); - - if (TaskConfiguration::STRATEGY_SKIP === $state->getTaskConfiguration()->getErrorStrategy()) { - $state->setSkipped(true); - } elseif (TaskConfiguration::STRATEGY_STOP === $state->getTaskConfiguration()->getErrorStrategy()) { - $state->setStopped(true); - } return; } - - $state->setOutput($result->body); } /** @@ -125,9 +116,6 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('log_response', ['bool']); } - /** - * @throws ExceptionInterface - */ protected function getRequestOptions(ProcessState $state): array { $options = $this->getOptions($state); diff --git a/Transformer/RequestTransformer.php b/src/Transformer/RequestTransformer.php similarity index 53% rename from Transformer/RequestTransformer.php rename to src/Transformer/RequestTransformer.php index 57ab434..c0e83fd 100644 --- a/Transformer/RequestTransformer.php +++ b/src/Transformer/RequestTransformer.php @@ -1,8 +1,11 @@ - - */ class RequestTransformer implements ConfigurableTransformerInterface { - - /** @var LoggerInterface */ - protected $logger; - - /** @var ClientRegistry */ - protected $registry; - - /** - * @param LoggerInterface $logger - * @param ClientRegistry $registry - */ - public function __construct(LoggerInterface $logger, ClientRegistry $registry) + public function __construct(protected LoggerInterface $logger, protected ClientRegistry $registry) { - $this->logger = $logger; - $this->registry = $registry; } /** - * {@inheritdoc} - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \RuntimeException - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \CleverAge\RestProcessBundle\src\Exception\MissingClientException + * @throws MissingClientException + * @throws TransformerException */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): string { $resolver = new OptionsResolver(); $this->configureOptions($resolver); @@ -73,40 +49,40 @@ public function transform($value, array $options = []) $input = $value ?: []; $requestOptions = array_merge($requestOptions, $input); - $result = $client->call($requestOptions); + $response = $client->call($requestOptions); // Handle empty results - if (!\in_array($result->code, $options['valid_response_code'], false)) { + try { + if (!\in_array($response->getStatusCode(), $options['valid_response_code'], false)) { + throw new \Exception('Invalid response code'); + } + + return $response->getContent(false); + } catch (\Exception|\Throwable $e) { $this->logger->error( 'REST request failed', [ 'client' => $options['client'], 'options' => $options, - 'raw_headers' => $result->raw_headers, - 'raw_body' => $result->raw_body, + 'message' => $e->getMessage(), + 'raw_headers' => $response->getHeaders(false), + 'raw_body' => $response->getContent(false), ] ); throw new TransformerException('REST request failed'); } - - return $result->body; } /** - * Returns the unique code to identify the transformer - * - * @return string + * Returns the unique code to identify the transformer. */ - public function getCode() + public function getCode(): string { return 'rest_request'; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired( [ From 271f7ffb596582e1499c1bc94e9199089fd3459c Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 14:23:02 +0200 Subject: [PATCH 09/22] #4 Update services according to Symfony best practices. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_rest_process` --- CHANGELOG.md | 5 ++++ config/services/registry.yaml | 4 +++ config/services/task.yaml | 9 +++++++ config/services/transformer.yaml | 10 ++++++++ .../CleverAgeRestProcessBundle.php | 25 +++++++++---------- .../CleverAgeRestProcessExtension.php | 2 +- src/Resources/config/services/registry.yaml | 3 --- src/Resources/config/services/task.yaml | 8 ------ .../config/services/transformer.yaml | 8 ------ 9 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 config/services/registry.yaml create mode 100644 config/services/task.yaml create mode 100644 config/services/transformer.yaml rename CleverAgeRestProcessBundle.php => src/CleverAgeRestProcessBundle.php (67%) delete mode 100644 src/Resources/config/services/registry.yaml delete mode 100644 src/Resources/config/services/task.yaml delete mode 100644 src/Resources/config/services/transformer.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index be94c74..d2df220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ v2.0 ## BC breaks * [#3](https://github.com/cleverage/rest-process-bundle/issues/3) Replace `nategood/httpful` dependency by `symfony/http-client` +* [#3](https://github.com/cleverage/rest-process-bundle/issues/5) Update Tasks for "symfony/http-client": "^6.4|^7.1" +* [#4](https://github.com/cleverage/rest-process-bundle/issues/4) Update services according to Symfony best practices. +Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. +Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_rest_process` + ### Changes diff --git a/config/services/registry.yaml b/config/services/registry.yaml new file mode 100644 index 0000000..f2f59d9 --- /dev/null +++ b/config/services/registry.yaml @@ -0,0 +1,4 @@ +services: + cleverage_rest_process.registry.client: + class: CleverAge\RestProcessBundle\Registry\ClientRegistry + public: false diff --git a/config/services/task.yaml b/config/services/task.yaml new file mode 100644 index 0000000..910c24e --- /dev/null +++ b/config/services/task.yaml @@ -0,0 +1,9 @@ +services: + cleverage_rest_process.task.request: + class: CleverAge\RestProcessBundle\Task\RequestTask + public: false + arguments: + - '@monolog.logger' + - '@cleverage_rest_process.registry.client' + tags: + - { name: monolog.logger, channel: cleverage_process_task } diff --git a/config/services/transformer.yaml b/config/services/transformer.yaml new file mode 100644 index 0000000..e9aed47 --- /dev/null +++ b/config/services/transformer.yaml @@ -0,0 +1,10 @@ +services: + cleverage_rest_process.transformer.request: + class: CleverAge\RestProcessBundle\Transformer\RequestTransformer + public: false + arguments: + - '@monolog.logger' + - '@cleverage_rest_process.registry.client' + tags: + - { name: cleverage.transformer } + - { name: monolog.logger, channel: cleverage_process_transformer } diff --git a/CleverAgeRestProcessBundle.php b/src/CleverAgeRestProcessBundle.php similarity index 67% rename from CleverAgeRestProcessBundle.php rename to src/CleverAgeRestProcessBundle.php index 6f3146e..c7e4683 100644 --- a/CleverAgeRestProcessBundle.php +++ b/src/CleverAgeRestProcessBundle.php @@ -1,8 +1,11 @@ - - * @author Vincent Chalnot - * @author Madeline Veyrenc - */ class CleverAgeRestProcessBundle extends Bundle { /** - * Adding compiler passes to inject services into registry - * - * @param ContainerBuilder $container + * Adding compiler passes to inject services into registry. */ public function build(ContainerBuilder $container): void { @@ -39,4 +33,9 @@ public function build(ContainerBuilder $container): void ) ); } + + public function getPath(): string + { + return \dirname(__DIR__); + } } diff --git a/src/DependencyInjection/CleverAgeRestProcessExtension.php b/src/DependencyInjection/CleverAgeRestProcessExtension.php index 30c7ecc..da6f35a 100644 --- a/src/DependencyInjection/CleverAgeRestProcessExtension.php +++ b/src/DependencyInjection/CleverAgeRestProcessExtension.php @@ -23,7 +23,7 @@ class CleverAgeRestProcessExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { - $this->findServices($container, __DIR__.'/../Resources/config/services'); + $this->findServices($container, __DIR__.'/../../config/services'); } /** diff --git a/src/Resources/config/services/registry.yaml b/src/Resources/config/services/registry.yaml deleted file mode 100644 index 323b1aa..0000000 --- a/src/Resources/config/services/registry.yaml +++ /dev/null @@ -1,3 +0,0 @@ -services: - CleverAge\RestProcessBundle\Registry\ClientRegistry: - public: false diff --git a/src/Resources/config/services/task.yaml b/src/Resources/config/services/task.yaml deleted file mode 100644 index f0591f4..0000000 --- a/src/Resources/config/services/task.yaml +++ /dev/null @@ -1,8 +0,0 @@ -services: - CleverAge\RestProcessBundle\Task\: - resource: '../../../Task/*' - autowire: true - public: true - shared: false - tags: - - { name: monolog.logger, channel: cleverage_process_task } diff --git a/src/Resources/config/services/transformer.yaml b/src/Resources/config/services/transformer.yaml deleted file mode 100644 index 20242ec..0000000 --- a/src/Resources/config/services/transformer.yaml +++ /dev/null @@ -1,8 +0,0 @@ -services: - CleverAge\RestProcessBundle\Transformer\: - resource: '../../../Transformer/*' - autowire: true - public: false - tags: - - { name: cleverage.transformer } - - { name: monolog.logger, channel: cleverage_process_transformer } From 12844b73bb32b1396df5e5424f34bddbadcbf51d Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 14:55:51 +0200 Subject: [PATCH 10/22] Minor doc fix --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a46c712..d518ac1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,7 @@ composer require cleverage/rest-process-bundle Remember to add the following line to config/bundles.php (not required if Symfony Flex is used) ```php -CleverAge\FlysystemProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true], +CleverAge\RestProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true], ``` ## Reference From d76e819ed69d94fa2ec0102de55a679fed514e69 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 24 Oct 2024 14:55:51 +0200 Subject: [PATCH 11/22] Minor doc fix --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b00965..7c88fe9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Compatible with [Symfony stable version and latest Long-Term Support (LTS) relea ## Documentation For usage documentation, see: -[docs/index.md](doc/index.md) +[docs/index.md](docs/index.md) ## Support & Contribution diff --git a/docs/index.md b/docs/index.md index a46c712..d518ac1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,7 @@ composer require cleverage/rest-process-bundle Remember to add the following line to config/bundles.php (not required if Symfony Flex is used) ```php -CleverAge\FlysystemProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true], +CleverAge\RestProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true], ``` ## Reference From b086b84d1cd4a21cd9cf8e16848af07cd17277fa Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Tue, 29 Oct 2024 17:02:29 +0100 Subject: [PATCH 12/22] #1 Apply <10.0 restriction on phpunit/phpunit since configuration file is not compatible with 10.0+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5ae2e0f..0a0adf3 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "phpstan/extension-installer": "*", "phpstan/phpstan": "*", "phpstan/phpstan-symfony": "*", - "phpunit/phpunit": "*", + "phpunit/phpunit": "<10.0", "rector/rector": "*", "roave/security-advisories": "dev-latest", "symfony/test-pack": "^1.1" From bc29d863deaadcde788f79f49d06de3b7c15171f Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 22 Nov 2024 09:51:53 +0100 Subject: [PATCH 13/22] #7 Aliasing Transformers & Tasks with FQCN to maintain old release configuration compatibility. --- config/services/task.yaml | 2 ++ config/services/transformer.yaml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/config/services/task.yaml b/config/services/task.yaml index 910c24e..27a9a1e 100644 --- a/config/services/task.yaml +++ b/config/services/task.yaml @@ -7,3 +7,5 @@ services: - '@cleverage_rest_process.registry.client' tags: - { name: monolog.logger, channel: cleverage_process_task } + CleverAge\RestProcessBundle\Task\RequestTask: + alias: cleverage_rest_process.task.request diff --git a/config/services/transformer.yaml b/config/services/transformer.yaml index e9aed47..a4312e6 100644 --- a/config/services/transformer.yaml +++ b/config/services/transformer.yaml @@ -8,3 +8,5 @@ services: tags: - { name: cleverage.transformer } - { name: monolog.logger, channel: cleverage_process_transformer } + CleverAge\RestProcessBundle\Transformer\RequestTransformer: + alias: cleverage_rest_process.transformer.request From 8c84c20ebe1fef4e109c1c51e059c12c93e20304 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Tue, 26 Nov 2024 11:58:42 +0100 Subject: [PATCH 14/22] Set as public RequestTask. Fix RegistryCompilerPass using service name instead of class FQCN. Fix Client->replaceParametersInUri() code. --- config/services/task.yaml | 1 + src/CleverAgeRestProcessBundle.php | 2 +- src/Client/Client.php | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/services/task.yaml b/config/services/task.yaml index 27a9a1e..10822b1 100644 --- a/config/services/task.yaml +++ b/config/services/task.yaml @@ -9,3 +9,4 @@ services: - { name: monolog.logger, channel: cleverage_process_task } CleverAge\RestProcessBundle\Task\RequestTask: alias: cleverage_rest_process.task.request + public: true diff --git a/src/CleverAgeRestProcessBundle.php b/src/CleverAgeRestProcessBundle.php index c7e4683..2a60470 100644 --- a/src/CleverAgeRestProcessBundle.php +++ b/src/CleverAgeRestProcessBundle.php @@ -27,7 +27,7 @@ public function build(ContainerBuilder $container): void { $container->addCompilerPass( new RegistryCompilerPass( - ClientRegistry::class, + 'cleverage_rest_process.registry.client', 'cleverage.rest.client', 'addClient' ) diff --git a/src/Client/Client.php b/src/Client/Client.php index f6beff5..19ea4d3 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -163,14 +163,16 @@ protected function replaceParametersInUri(string $uri, array $options = []): str $search = array_keys($options['url_parameters']); array_walk( $search, - static function (&$item) { + static function (&$item, $key) { $item = '{'.$item.'}'; } ); $replace = array_values($options['url_parameters']); array_walk( $replace, - 'rawurlencode' + static function (&$item, $key) { + $item = rawurlencode($item); + } ); $uri = str_replace($search, $replace, $uri); From f67c1f2a2f1cd9428916f15b7a94c1631f9fd699 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Tue, 26 Nov 2024 14:31:17 +0100 Subject: [PATCH 15/22] Apply quality rules --- src/CleverAgeRestProcessBundle.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CleverAgeRestProcessBundle.php b/src/CleverAgeRestProcessBundle.php index 2a60470..b78e4c5 100644 --- a/src/CleverAgeRestProcessBundle.php +++ b/src/CleverAgeRestProcessBundle.php @@ -14,7 +14,6 @@ namespace CleverAge\RestProcessBundle; use CleverAge\ProcessBundle\DependencyInjection\Compiler\RegistryCompilerPass; -use CleverAge\RestProcessBundle\Registry\ClientRegistry; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; From eb8e6e27d4946296260603d0a26ed226fc32ed9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tonon=20Gr=C3=A9gory?= Date: Tue, 3 Dec 2024 14:17:16 +0100 Subject: [PATCH 16/22] Replace monolog.logger by logger on CleverAge\RestProcessBundle\Task\RequestTaskto use cleverage_process_task channel instead of app channel. --- config/services/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/services/task.yaml b/config/services/task.yaml index 10822b1..bb53f13 100644 --- a/config/services/task.yaml +++ b/config/services/task.yaml @@ -3,7 +3,7 @@ services: class: CleverAge\RestProcessBundle\Task\RequestTask public: false arguments: - - '@monolog.logger' + - '@logger' - '@cleverage_rest_process.registry.client' tags: - { name: monolog.logger, channel: cleverage_process_task } From 8fc4aa1c238b2a3a7521f0010a7487e2e67197dc Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 12 Dec 2024 15:35:47 +0100 Subject: [PATCH 17/22] RequestTask : `query_parameters` option is deprecated, use `data` instead. Add missing throw Exception to allow use error_strategy: *. --- CHANGELOG.md | 2 +- src/Client/Client.php | 15 +++++++-------- src/Task/RequestTask.php | 12 ++++++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2df220..96845bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ v2.0 * [#4](https://github.com/cleverage/rest-process-bundle/issues/4) Update services according to Symfony best practices. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_rest_process` - +* RequestTask : `query_parameters` option is deprecated, use `data` instead ### Changes diff --git a/src/Client/Client.php b/src/Client/Client.php index 19ea4d3..1532ad9 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -93,12 +93,11 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults( [ 'method' => 'GET', - 'url_parameters' => [], - 'query_parameters' => [], - 'headers' => [], 'sends' => 'application/json', 'expects' => 'application/json', - 'body' => null, + 'url_parameters' => [], + 'headers' => [], + 'data' => null, ] ); @@ -107,8 +106,8 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('sends', ['string']); $resolver->setAllowedTypes('expects', ['string']); $resolver->setAllowedTypes('url_parameters', ['array']); - $resolver->setAllowedTypes('query_parameters', ['array']); $resolver->setAllowedTypes('headers', ['array']); + $resolver->setAllowedTypes('data', ['array', 'string', 'null']); } protected function getOptions(array $options = []): array @@ -135,11 +134,11 @@ protected function getRequestOptions(array $options = []): array $requestOptions['headers']['Accept'] = $options['expects']; } if ('POST' === $options['method'] && 'application/json' === $options['sends']) { - $requestOptions['json'] = $options['body']; + $requestOptions['json'] = $options['data']; } elseif ('GET' === $options['method']) { - $requestOptions['query'] = $options['query_parameters']; + $requestOptions['query'] = $options['data']; } else { - $requestOptions['body'] = $options['body']; + $requestOptions['body'] = $options['data']; } return $requestOptions; diff --git a/src/Task/RequestTask.php b/src/Task/RequestTask.php index 3ac70bc..0abc302 100644 --- a/src/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -80,7 +80,7 @@ public function execute(ProcessState $state): void ] ); - return; + throw $e; } } @@ -101,9 +101,9 @@ protected function configureOptions(OptionsResolver $resolver): void [ 'headers' => [], 'url_parameters' => [], - 'query_parameters' => [], - 'sends' => 'json', - 'expects' => 'json', + 'data' => null, + 'sends' => 'application/json', + 'expects' => 'application/json', 'valid_response_code' => [200], 'log_response' => false, ] @@ -121,13 +121,13 @@ protected function getRequestOptions(ProcessState $state): array $options = $this->getOptions($state); $requestOptions = [ - 'method' => $options['method'], 'url' => $options['url'], + 'method' => $options['method'], 'headers' => $options['headers'], 'url_parameters' => $options['url_parameters'], - 'query_parameters' => $options['query_parameters'], 'sends' => $options['sends'], 'expects' => $options['expects'], + 'data' => $options['data'], ]; $input = $state->getInput() ?: []; From 2bd26ee758db58104f6b5ddf8582f91907aab9fd Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 12 Dec 2024 15:36:47 +0100 Subject: [PATCH 18/22] Add RequestTask documentation --- docs/index.md | 2 +- docs/reference/tasks/request_task.md | 90 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 docs/reference/tasks/request_task.md diff --git a/docs/index.md b/docs/index.md index d518ac1..0eab58a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,6 @@ CleverAge\RestProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true] ## Reference - Tasks - - [RequestTask] + - [RequestTask](reference/tasks/request_task.md) - Transformers - [RequestTransformer] diff --git a/docs/reference/tasks/request_task.md b/docs/reference/tasks/request_task.md new file mode 100644 index 0000000..88315cb --- /dev/null +++ b/docs/reference/tasks/request_task.md @@ -0,0 +1,90 @@ +RequestTask +=============== + +Call a Rest Request and get result. + +Task reference +-------------- + +* **Client Service Interface**: `CleverAge\RestProcessBundle\Client\ClientInterface` +* **Task Service**: `CleverAge\RestProcessBundle\Task\RequestTask` + +Accepted inputs +--------------- + +`array`: inputs are merged with task defined options. + +Possible outputs +---------------- + +`string`: the result content of the rest call. + +Options +------- + +### For Client + +| Code | Type | Required | Default | Description | +|--------|----------|:--------:|---------|------------------------------------------------| +| `code` | `string` | **X** | | Service identifier, used by Task client option | +| `uri` | `string` | **X** | | Base uri, concatenated with Task `url` | + +### For Task + +| Code | Type | Required | Default | Description | +|-----------------------|-----------------------------|:--------:|--------------------|------------------------------------------------------------------------------------------| +| `client` | `string` | **X** | | `ClientInterface` service identifier | +| `url` | `string` | **X** | | Relative url to call | +| `method` | `string` | **X** | | HTTP method from `['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'PATCH']` | +| `headers` | `array` | | `[]` | | +| `url_parameters` | `array` | | `[]` | Search/Replace data on `url` | +| `data` | `array`, `string` or `null` | | `null` | Treated as `body`, `query` or `json` on HttpClient, depending on `method` and `sends` | +| `sends` | `string` | | `application/json` | `Content-Type` header, if value is not empty | +| `expects` | `string` | | `application/json` | `Accept` header, if value is not empty | +| `valid_response_code` | `array` | | `[200]` | One or more [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) | +| `log_response` | `bool` | | `false` | | + +Examples +-------- + +### Client + +```yaml +services: + app.cleverage_rest_process.client.apicarto_ign: + class: CleverAge\RestProcessBundle\Client\Client + bind: + $code: 'domain_sample' + $uri: 'https://domain/api' + tags: + - { name: cleverage.rest.client } +``` + +### Task + +```yaml +# Task configuration level +code: + service: '@CleverAge\RestProcessBundle\Task\RequestTask' + error_strategy: 'stop' + options: + client: domain_sample + url: '/sample/{parameter}' + method: 'GET' + url_parameters: { parameter: '{{ parameter }}' } +``` + +```yaml +# Task configuration level +code: + service: '@CleverAge\RestProcessBundle\Task\RequestTask' + error_strategy: 'stop' + options: + client: domain_sample + url: '/sample' + method: 'POST' + data: # May be a json string or an array + parameter_1: + parameter_11: "eleven" + array: [-1, 666] +``` From 95dea37ec036b25f976a5606804f23ea02ed5a59 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 12 Dec 2024 16:10:43 +0100 Subject: [PATCH 19/22] Remove RequestTransformer, use RequestTask instead. --- CHANGELOG.md | 1 + config/services/transformer.yaml | 12 --- docs/index.md | 2 - src/Transformer/RequestTransformer.php | 105 ------------------------- 4 files changed, 1 insertion(+), 119 deletions(-) delete mode 100644 config/services/transformer.yaml delete mode 100644 src/Transformer/RequestTransformer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 96845bc..0afe273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v2.0 Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_rest_process` * RequestTask : `query_parameters` option is deprecated, use `data` instead +* Remove RequestTransformer, use RequestTask instead. ### Changes diff --git a/config/services/transformer.yaml b/config/services/transformer.yaml deleted file mode 100644 index a4312e6..0000000 --- a/config/services/transformer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -services: - cleverage_rest_process.transformer.request: - class: CleverAge\RestProcessBundle\Transformer\RequestTransformer - public: false - arguments: - - '@monolog.logger' - - '@cleverage_rest_process.registry.client' - tags: - - { name: cleverage.transformer } - - { name: monolog.logger, channel: cleverage_process_transformer } - CleverAge\RestProcessBundle\Transformer\RequestTransformer: - alias: cleverage_rest_process.transformer.request diff --git a/docs/index.md b/docs/index.md index 0eab58a..1b7effa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,5 +23,3 @@ CleverAge\RestProcessBundle\CleverAgeRestProcessBundle::class => ['all' => true] - Tasks - [RequestTask](reference/tasks/request_task.md) -- Transformers - - [RequestTransformer] diff --git a/src/Transformer/RequestTransformer.php b/src/Transformer/RequestTransformer.php deleted file mode 100644 index c0e83fd..0000000 --- a/src/Transformer/RequestTransformer.php +++ /dev/null @@ -1,105 +0,0 @@ -configureOptions($resolver); - $options = $resolver->resolve($options); - - $client = $this->registry->getClient($options['client']); - - $requestOptions = [ - 'url' => $options['url'], - 'headers' => $options['headers'], - 'url_parameters' => $options['url_parameters'], - 'query_parameters' => $options['query_parameters'], - 'sends' => $options['sends'], - 'expects' => $options['expects'], - ]; - - $input = $value ?: []; - $requestOptions = array_merge($requestOptions, $input); - $response = $client->call($requestOptions); - - // Handle empty results - try { - if (!\in_array($response->getStatusCode(), $options['valid_response_code'], false)) { - throw new \Exception('Invalid response code'); - } - - return $response->getContent(false); - } catch (\Exception|\Throwable $e) { - $this->logger->error( - 'REST request failed', - [ - 'client' => $options['client'], - 'options' => $options, - 'message' => $e->getMessage(), - 'raw_headers' => $response->getHeaders(false), - 'raw_body' => $response->getContent(false), - ] - ); - - throw new TransformerException('REST request failed'); - } - } - - /** - * Returns the unique code to identify the transformer. - */ - public function getCode(): string - { - return 'rest_request'; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setRequired( - [ - 'client', - 'url', - 'method', - ] - ); - $resolver->setDefault('headers', []); - $resolver->setDefault('url_parameters', []); - $resolver->setDefault('query_parameters', []); - $resolver->setDefault('sends', 'json'); - $resolver->setDefault('expects', 'json'); - $resolver->setDefault('valid_response_code', [200]); - $resolver->setAllowedTypes('client', ['string']); - $resolver->setAllowedTypes('url', ['string']); - $resolver->setAllowedTypes('method', ['string']); - $resolver->setAllowedTypes('valid_response_code', ['array']); - } -} From aaa5243bc6ff7624cfcd8f86b167230568b5ef92 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 12 Dec 2024 17:24:09 +0100 Subject: [PATCH 20/22] #8 Remove all phpstan ignoreErrors, exceptp parameter.defaultValue because of OptionsResolver. Fix code. --- phpstan.neon | 13 +------------ src/Client/Client.php | 33 +++++++++++++++++++++++++++++++-- src/Client/ClientInterface.php | 6 ++++++ src/Task/RequestTask.php | 27 +++++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 30e9572..2629ea6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,15 +4,4 @@ parameters: - src - tests ignoreErrors: - - '#type has no value type specified in iterable type#' - - '#has parameter .* with no value type specified in iterable type#' - - '#has no value type specified in iterable type array#' - - '#configureOptions\(\) has no return type specified.#' - - '#configure\(\) has no return type specified#' - - '#process\(\) has no return type specified#' - - '#should return Iterator but returns Traversable#' - - '#Negated boolean expression is always false#' - checkGenericClassInNonGenericObjectType: false - reportUnmatchedIgnoredErrors: false - inferPrivatePropertyTypeFromConstructor: true - treatPhpDocTypesAsCertain: false + - identifier: parameter.defaultValue diff --git a/src/Client/Client.php b/src/Client/Client.php index 1532ad9..008c076 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -19,6 +19,9 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; +/** + * @phpstan-import-type Options from \CleverAge\RestProcessBundle\Task\RequestTask + */ class Client implements ClientInterface { public function __construct( @@ -50,6 +53,8 @@ public function setUri(string $uri): void } /** + * @param Options $options + * * @throws RestRequestException */ public function call(array $options = []): ResponseInterface @@ -70,7 +75,7 @@ public function call(array $options = []): ResponseInterface $this->getRequestUri($options), $this->getRequestOptions($options), ); - } catch (\Exception|\Throwable $e) { + } catch (\Throwable $e) { $this->logger->error( 'Rest request failed', [ @@ -110,6 +115,11 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('data', ['array', 'string', 'null']); } + /** + * @param Options $options + * + * @return Options + */ protected function getOptions(array $options = []): array { $resolver = new OptionsResolver(); @@ -118,11 +128,24 @@ protected function getOptions(array $options = []): array return $resolver->resolve($options); } + /** + * @param Options $options + */ protected function getRequestUri(array $options = []): string { return $this->replaceParametersInUri($this->constructUri($options), $options); } + /** + * @param Options $options + * + * @return array{ + * 'headers': array, + * 'json'?: array|string|null, + * 'query'?: array|string|null, + * 'body'?: array|string|null + * } + */ protected function getRequestOptions(array $options = []): array { $requestOptions = []; @@ -144,6 +167,9 @@ protected function getRequestOptions(array $options = []): array return $requestOptions; } + /** + * @param Options $options + */ protected function constructUri(array $options): string { $uri = ltrim((string) $options['url'], '/'); @@ -156,9 +182,12 @@ protected function getApiUrl(): string return $this->geUri(); } + /** + * @param Options $options + */ protected function replaceParametersInUri(string $uri, array $options = []): string { - if (\array_key_exists('url_parameters', $options) && $options['url_parameters']) { + if ($options['url_parameters']) { $search = array_keys($options['url_parameters']); array_walk( $search, diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php index 7e066e1..4e95bf5 100644 --- a/src/Client/ClientInterface.php +++ b/src/Client/ClientInterface.php @@ -15,6 +15,9 @@ use Symfony\Contracts\HttpClient\ResponseInterface; +/** + * @phpstan-import-type Options from \CleverAge\RestProcessBundle\Task\RequestTask + */ interface ClientInterface { /** @@ -26,5 +29,8 @@ public function geUri(): string; public function setUri(string $uri): void; + /** + * @param Options $options + */ public function call(array $options = []): ResponseInterface; } diff --git a/src/Task/RequestTask.php b/src/Task/RequestTask.php index 0abc302..9816df0 100644 --- a/src/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -22,7 +22,22 @@ use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; - +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; + +/** + * @phpstan-type Options array{ + * 'url': string, + * 'method': string, + * 'headers': array, + * 'url_parameters': array, + * 'sends': string, + * 'expects': string, + * 'data': array|string|null + * } + */ class RequestTask extends AbstractConfigurableTask { public function __construct(protected LoggerInterface $logger, protected ClientRegistry $registry) @@ -31,6 +46,11 @@ public function __construct(protected LoggerInterface $logger, protected ClientR /** * @throws MissingClientException + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + * @throws TransportExceptionInterface + * @throws \Throwable */ public function execute(ProcessState $state): void { @@ -68,7 +88,7 @@ public function execute(ProcessState $state): void } $state->setOutput($response->getContent()); - } catch (\Exception|\Throwable $e) { + } catch (\Throwable $e) { $this->logger->error( 'REST request failed', [ @@ -116,6 +136,9 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('log_response', ['bool']); } + /** + * @return Options + */ protected function getRequestOptions(ProcessState $state): array { $options = $this->getOptions($state); From 60a0b828b70fff0b2009544b5e21b16063504a4e Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 13 Dec 2024 08:48:02 +0100 Subject: [PATCH 21/22] #8 Apply phpstan level 10 --- phpstan.neon | 2 +- src/Client/Client.php | 23 ++++++++++++++--------- src/Client/ClientInterface.php | 4 ++-- src/Task/RequestTask.php | 24 +++++++++++++++++++++--- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 2629ea6..9cae086 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 10 paths: - src - tests diff --git a/src/Client/Client.php b/src/Client/Client.php index 008c076..94d586f 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -20,7 +20,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface; /** - * @phpstan-import-type Options from \CleverAge\RestProcessBundle\Task\RequestTask + * @phpstan-import-type RequestOptions from \CleverAge\RestProcessBundle\Task\RequestTask */ class Client implements ClientInterface { @@ -53,7 +53,7 @@ public function setUri(string $uri): void } /** - * @param Options $options + * @param RequestOptions $options * * @throws RestRequestException */ @@ -116,20 +116,23 @@ protected function configureOptions(OptionsResolver $resolver): void } /** - * @param Options $options + * @param RequestOptions $options * - * @return Options + * @return RequestOptions */ protected function getOptions(array $options = []): array { $resolver = new OptionsResolver(); $this->configureOptions($resolver); - return $resolver->resolve($options); + /** @var RequestOptions $resolved */ + $resolved = $resolver->resolve($options); + + return $resolved; } /** - * @param Options $options + * @param RequestOptions $options */ protected function getRequestUri(array $options = []): string { @@ -137,7 +140,7 @@ protected function getRequestUri(array $options = []): string } /** - * @param Options $options + * @param RequestOptions $options * * @return array{ * 'headers': array, @@ -168,7 +171,7 @@ protected function getRequestOptions(array $options = []): array } /** - * @param Options $options + * @param RequestOptions $options */ protected function constructUri(array $options): string { @@ -183,11 +186,12 @@ protected function getApiUrl(): string } /** - * @param Options $options + * @param RequestOptions $options */ protected function replaceParametersInUri(string $uri, array $options = []): string { if ($options['url_parameters']) { + /** @var array $search */ $search = array_keys($options['url_parameters']); array_walk( $search, @@ -195,6 +199,7 @@ static function (&$item, $key) { $item = '{'.$item.'}'; } ); + /** @var array $replace */ $replace = array_values($options['url_parameters']); array_walk( $replace, diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php index 4e95bf5..b08bf2a 100644 --- a/src/Client/ClientInterface.php +++ b/src/Client/ClientInterface.php @@ -16,7 +16,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface; /** - * @phpstan-import-type Options from \CleverAge\RestProcessBundle\Task\RequestTask + * @phpstan-import-type RequestOptions from \CleverAge\RestProcessBundle\Task\RequestTask */ interface ClientInterface { @@ -30,7 +30,7 @@ public function geUri(): string; public function setUri(string $uri): void; /** - * @param Options $options + * @param RequestOptions $options */ public function call(array $options = []): ResponseInterface; } diff --git a/src/Task/RequestTask.php b/src/Task/RequestTask.php index 9816df0..d8b4b9b 100644 --- a/src/Task/RequestTask.php +++ b/src/Task/RequestTask.php @@ -29,6 +29,18 @@ /** * @phpstan-type Options array{ + * 'client': string, + * 'url': string, + * 'method': string, + * 'headers': array, + * 'url_parameters': array, + * 'data': array|string|null, + * 'sends': string, + * 'expects': string, + * 'valid_response_code': array, + * 'log_response': bool, + * } + * @phpstan-type RequestOptions array{ * 'url': string, * 'method': string, * 'headers': array, @@ -36,7 +48,7 @@ * 'sends': string, * 'expects': string, * 'data': array|string|null - * } + * } */ class RequestTask extends AbstractConfigurableTask { @@ -54,6 +66,7 @@ public function __construct(protected LoggerInterface $logger, protected ClientR */ public function execute(ProcessState $state): void { + /** @var Options $options */ $options = $this->getOptions($state); $requestOptions = $this->getRequestOptions($state); @@ -137,10 +150,11 @@ protected function configureOptions(OptionsResolver $resolver): void } /** - * @return Options + * @return RequestOptions */ protected function getRequestOptions(ProcessState $state): array { + /** @var Options $options */ $options = $this->getOptions($state); $requestOptions = [ @@ -153,8 +167,12 @@ protected function getRequestOptions(ProcessState $state): array 'data' => $options['data'], ]; + /** @var array $input */ $input = $state->getInput() ?: []; - return array_merge($requestOptions, $input); + /** @var RequestOptions $mergedOptions */ + $mergedOptions = array_merge($requestOptions, $input); + + return $mergedOptions; } } From 4f2e3835a34f5ffc303706f6c353a6dbd5143aa8 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Tue, 17 Dec 2024 15:28:53 +0100 Subject: [PATCH 22/22] #5 Bump dependency "cleverage/process-bundle": "^4.0" --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0a0adf3..afa9628 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ }, "require": { "php": ">=8.1", - "cleverage/process-bundle": "dev-prepare-release", + "cleverage/process-bundle": "^4.0", "symfony/http-client": "^6.4|^7.1" }, "require-dev": {