From f8bfd8fe5bb4c001d4fab840bec9ba690360d752 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 11 Apr 2020 16:03:21 +1000 Subject: [PATCH] Autobots, roll out --- .github/workflows/main.yml | 110 ++++++++ .gitignore | 6 + .php_cs.dist | 8 + composer.json | 71 +++++ infection.json.dist | 16 ++ license.txt | 21 ++ phpstan.neon | 7 + phpunit.xml.dist | 19 ++ psalm.xml | 34 +++ readme.md | 151 ++++++++++ src/HasParameters.php | 203 ++++++++++++++ tests/HasParametersTest.php | 264 ++++++++++++++++++ tests/Middleware/Basic.php | 19 ++ tests/Middleware/Optional.php | 19 ++ tests/Middleware/OptionalRequired.php | 19 ++ tests/Middleware/Required.php | 19 ++ tests/Middleware/RequiredOptionalVariadic.php | 19 ++ tests/Middleware/Variadic.php | 19 ++ 18 files changed, 1024 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .php_cs.dist create mode 100644 composer.json create mode 100644 infection.json.dist create mode 100644 license.txt create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 psalm.xml create mode 100644 readme.md create mode 100644 src/HasParameters.php create mode 100644 tests/HasParametersTest.php create mode 100644 tests/Middleware/Basic.php create mode 100644 tests/Middleware/Optional.php create mode 100644 tests/Middleware/OptionalRequired.php create mode 100644 tests/Middleware/Required.php create mode 100644 tests/Middleware/RequiredOptionalVariadic.php create mode 100644 tests/Middleware/Variadic.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e61ffaa --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,110 @@ +name: CI + +on: + schedule: + # once a month + - cron: '0 0 1 * *' + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + lint: + runs-on: ubuntu-latest + name: "Lint" + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: pcov + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-composer-${{ hashFiles('composer.json') }}-v1 + + - name: Install dependencies + run: composer install --no-suggest --no-interaction --verbose + + - name: Check platform requirements + run: composer check-platform-reqs --verbose + + - name: PHP-CS-Fixer + run: ./vendor/bin/php-cs-fixer fix --dry-run --no-interaction --verbose + + - name: composer normalize + run: composer normalize --dry-run --no-interaction --verbose + + - name: Infection + run: ./vendor/bin/infection --show-mutations --min-msi=100 --no-progress --no-interaction --verbose + env: + INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} + + - name: Psalm + run: ./vendor/bin/psalm --shepherd --no-progress + + - name: PHPStan + run: ./vendor/bin/phpstan --no-interaction --verbose + + tests: + runs-on: ubuntu-latest + name: "Test suite on PHP: ${{ matrix.php }}; Laravel: ${{ matrix.laravel }}; Dependecies: ${{ matrix.dependency-version }}" + strategy: + matrix: + php: ['7.1', '7.2', '7.3', '7.4'] + laravel: ['5.5.*', '5.6.*', '5.7.*', '5.8.*', '^6.0', '^7.0'] + dependency-version: ['prefer-lowest', 'prefer-stable'] + include: + - laravel: '5.5.*' + testbench: '3.5.*' + - laravel: '5.6.*' + testbench: '3.6.*' + - laravel: '5.7.*' + testbench: '3.7.*' + - laravel: '5.8.*' + testbench: '3.8.*' + - laravel: '^6.0' + testbench: '^4.0' + - laravel: '^7.0' + testbench: '^5.0' + exclude: + - laravel: '5.3.*' + php: '7.2' + - laravel: '^6.0' + php: '7.1' + - laravel: '^7.0' + php: '7.1' + + steps: + - name: checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-${{ matrix.dependency-version }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}-v1 + + - name: Install dependencies + run: | + composer remove infection/infection phpunit/phpunit ergebnis/composer-normalize orchestra/testbench orchestra/testbench phpstan/phpstan phpunit/phpunit timacdonald/php-style vimeo/psalm --dev --no-update --no-interaction --verbose + composer require "illuminate/support:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-update --no-interaction --verbose + composer update --${{ matrix.dependency-version }} --no-suggest --no-interaction --verbose + + - name: Check platform requirements + run: composer check-platform-reqs --verbose + + - name: Run tests + run: ./vendor/bin/phpunit --coverage-text --debug --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..417d292 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +/composer.lock +/.phpunit.result.cache +/.php_cs.cache +/coverage +/infection.log diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 0000000..574194b --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,8 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->notPath('OptionalRequired.php'); + +return style_rules($finder); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..94202b5 --- /dev/null +++ b/composer.json @@ -0,0 +1,71 @@ +{ + "name": "timacdonald/has-parameters", + "description": "A trait that allows you to pass arguments to Laravel middleware in a more PHP'ish way.", + "keywords": [ + "laravel", + "middleware", + "parameters", + "arguments" + ], + "license": "MIT", + "authors": [ + { + "name": "Tim MacDonald", + "email": "hello@timacdonald.me", + "homepage": "https://timacdonald.me" + } + ], + "require": { + "php": "^7.1", + "illuminate/support": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.3", + "infection/infection": "^0.16.1", + "orchestra/testbench": "^5.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9.0", + "timacdonald/php-style": "dev-master", + "vimeo/psalm": "^3.0" + }, + "config": { + "preferred-install": "dist", + "sort-packages": true + }, + "autoload": { + "psr-4": { + "TiMacDonald\\Middleware\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "minimum-stability": "stable", + "prefer-stable": true, + "scripts": { + "fix": [ + "clear", + "@composer normalize", + "./vendor/bin/php-cs-fixer fix" + ], + "lint": [ + "clear", + "@composer normalize --dry-run", + "./vendor/bin/php-cs-fixer fix --dry-run", + "./vendor/bin/psalm --threads=8", + "./vendor/bin/phpstan analyse" + ], + "test": [ + "clear", + "./vendor/bin/phpunit", + "./vendor/bin/infection --threads=8" + ] + }, + "support": { + "issues": "https://github.com/timacdonald/with-parameters/issues", + "source": "https://github.com/timacdonald/with-parameters/releases/latest", + "docs": "https://github.com/timacdonald/with-parameters/blob/master/readme.md" + } +} diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..7b603a5 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,16 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "infection.log", + "badge": { + "branch": "master" + } + }, + "mutators": { + "@default": true + } +} diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..0cc0ef4 --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Tim MacDonald + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..cddc7d2 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +parameters: + checkMissingIterableValueType: false + level: max + paths: + - src + - tests + ignoreErrors: diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7dbde61 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + tests + + + + + + src + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..5de662b --- /dev/null +++ b/psalm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2671fb0 --- /dev/null +++ b/readme.md @@ -0,0 +1,151 @@ +# Has Parameters + +![CI](https://github.com/timacdonald/has-parameters/workflows/CI/badge.svg) [![Mutation testing](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftimacdonald%2Fhas-parameters%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/timacdonald/has-parameters/master) ![Type coverage](https://shepherd.dev/github/timacdonald/has-parameters/coverage.svg) [![Latest Stable Version](https://poser.pugx.org/timacdonald/has-parameters/v/stable)](https://packagist.org/packages/timacdonald/has-parameters) [![Total Downloads](https://poser.pugx.org/timacdonald/has-parameters/downloads)](https://packagist.org/packages/timacdonald/has-parameters) [![License](https://poser.pugx.org/timacdonald/has-parameters/license)](https://packagist.org/packages/timacdonald/has-parameters) + +A trait for Laravel middleware that allows you to pass arguments in a more PHP'ish way, including as a key => value pair for named parameters, and as a list for variadic parameters. Improves static analysis / IDE support, allows you to specify arguments by referencing the parameter name, enables skipping optional parameters (which fallback to their default value), and adds some validation so you don't forget any required parameters by accident. + +## Installation + +You can install using [composer](https://getcomposer.org/) from [Packagist](https://packagist.org/packages/timacdonald/has-parameters). + +``` +$ composer require timacdonald/has-parameters +``` + +## Basic usage + +To get started with an example, I'm going to use a stripped back version of Laravel's `ThrottleRequests`. First up, add the `HasParameters` trait to your middleware. + +```php +class ThrottleRequests +{ + use HasParameters; + + public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') + { + // + } +} +``` + +You can now pass arguments to this middleware using the static `with()` method, using the parameter name as the key. + +```php +Route::stuff() + ->middleware([ + ThrottleRequests::with([ + 'maxAttempts' => 120, + ]), + ]); +``` + +You'll notice at first this is a little more verbose, but I think you'll enjoy the complete feature set after reading these docs and taking it for a spin. + +## Middleware::with() + +The static `with()` method allows you to easily see which values represent what when declaring your middleware, instead of just declaring a comma seperate list of values. +The order of the keys does not matter. The trait will pair up the keys to the parameter names in the `handle()` method. + +```php +// before... +Route::stuff() + ->middleware([ + 'throttle:10,1' // what does 10 or 1 stand for here? + ]); + +// after... +Route::stuff() + ->middleware([ + ThrottleRequests::with([ + 'decayMinutes' => 1, + 'maxAttempts' => 10, + ]), + ]); +``` + +### Skipping parameters + +If any parameters in the `handle` method have a default value, you do not need to pass them through - unless you are changing their value. As an example, if you'd like to only specify a prefix for the `ThrottleRequests` middleware, but keep the `$decayMinutes` and `$maxAttempts` as their default values, you can do the following... + +```php +Route::stuff() + ->middleware([ + ThrottleRequests::with([ + 'prefix' => 'admins', + ]), + ]); +``` + +As we saw previously in the handle method, the default values of `$decayMinutes` is `1` and `$maxAttempts` is `60`. The middleware will receive those values for those parameters, but will now receive `"admins"` for the `$prefix`. + +### Arrays for variadic parameters + +When your middleware ends in a variadic paramater, you can pass an array of values for the variadic parameter key. Take a look at the following `handle()` method. + +```php +public function handle(Request $request, Closure $next, string $ability, string ...$models) +``` + +Here is how we can pass a list of values to the variadic `$models` parameter... + +```php +Route::stuff() + ->middleware([ + Authorize::with([ + 'ability' => PostVideoPolicy::UPDATE, + 'models' => [Post::class, Video::class], + ]), + ]); +``` + +### Validation + +These validations occur whenever the routes file is loaded or compiled, not just when you hit a route that contains the declaration. + +#### Unexpected parameter + +The trait validates that you do not declare any keys that do not exist as parameter variables in the `handle()` method. This helps make sure you don't mis-type a parameter name. + +#### Required parameters + +Another validation that occurs is checking to make sure all required parameters (those without default values) have been provided. + +## Middleware::in() + +The static `in()` method very much reflects and works the same as the existing concatination API. It accepts a list of values, i.e. a non-associative array. You should use this method if your `handle()` method is a single variadic parameter, i.e. expecting a single list of values, as shown in the following middleware handle method... +. +```php +public function handle(Request $request, Closure $next, string ...$states) +{ + // +} +``` + +You can pass through a list of "states" to the middleware like so... + +```php +Route::stuff() + ->middleware([ + EnsurePostState::in([PostState::DRAFT, PostState::UNDER_REVIEW]), + ]); +``` + +### Validation + +#### Required parameters + +Just like the `with()` method, the `in()` method will validate that you have passed enough values through to cover all the required parameters. Because variadic parameters do not require any values to be passed through, you only really rub up against this when you should probably be using the `with()` method. + +## Value transformation + +You should keep in mind that everything will still be cast to a string. Although you are passing in, for example, integers, the middleware itself will *always* receive a string. This is how Laravel works under-the-hood to implement route caching. + +One thing to note is the `false` is actually cast to the string `"0"` to keep some consistency with casting `true` to a string, which results in the string `"1"`. + +## Developing and testing + +Although this package requires `"PHP": "^7.1"`, in order to install and develop locally, you should be running a recent version of PHP to ensure compatibility with the development tools. + +## Thanksware + +You are free to use this package, but I ask that you reach out to someone (not me) who has previously, or is currently, maintaining or contributing to an open source library you are using in your project and thank them for their work. Consider your entire tech stack: packages, frameworks, languages, databases, operating systems, frontend, backend, etc. diff --git a/src/HasParameters.php b/src/HasParameters.php new file mode 100644 index 0000000..1efd3ad --- /dev/null +++ b/src/HasParameters.php @@ -0,0 +1,203 @@ +isEmpty()) { + return static::class; + } + + return static::class.':'.$arguments->implode(','); + } + + private static function parseArgumentList(Collection $arguments): Collection + { + return $arguments->map( + /** + * @param mixed $argument + */ + static function ($argument): string { + return static::castToString($argument); + } + ); + } + + private static function parseArgumentMap(Collection $parameters, Collection $arguments): Collection + { + return $parameters->map(static function (ReflectionParameter $parameter) use ($arguments): ?string { + if ($parameter->isVariadic()) { + return static::parseVariadicArgument($parameter, $arguments); + } + + return static::parseStandardArgument($parameter, $arguments); + })->reject(static function (?string $argument): bool { + /** + * A null value indicates that the last item in the parameter list + * is a variadic function that is not expecting any values. Because + * of the way variadic parameters work, we don't want to pass null, + * we really want to pass void, so we just filter it out of the + * list completely. null !== void. + */ + return $argument === null; + }); + } + + private static function parseVariadicArgument(ReflectionParameter $parameter, Collection $arguments): ?string + { + if (! $arguments->has($parameter->getName())) { + return null; + } + + $values = new Collection($arguments->get($parameter->getName())); + + if ($values->isEmpty()) { + return null; + } + + return $values->map( + /** + * @param mixed $value + */ + static function ($value) { + return static::castToString($value); + } + )->implode(','); + } + + private static function parseStandardArgument(ReflectionParameter $parameter, Collection $arguments): string + { + if ($arguments->has($parameter->getName())) { + return static::castToString($arguments->get($parameter->getName())); + } + + return static::castToString($parameter->getDefaultValue()); + } + + private static function parameters(): Collection + { + $handle = new ReflectionMethod(static::class, 'handle'); + + return Collection::make($handle->getParameters()) + ->slice(2) + ->keyBy(static function (ReflectionParameter $parameter): string { + return $parameter->getName(); + }); + } + + /** + * @param mixed $value + */ + private static function castToString($value): string + { + if ($value === false) { + return '0'; + } + + return (string) $value; + } + + private static function validateArgumentList(Collection $parameters, Collection $arguments): void + { + static::validateArgumentListIsNotAnAssociativeArray($arguments); + + static::validateParametersAreOptional($parameters->slice($arguments->count())); + } + + private static function validateArgumentMap(Collection $parameters, Collection $arguments): void + { + static::validateArgumentMapIsAnAssociativeArray($arguments); + + static::validateNoUnexpectedArguments($parameters, $arguments); + + static::validateParametersAreOptional($parameters->diffKeys($arguments)); + } + + private static function validateParametersAreOptional(Collection $parameters): void + { + $missingRequiredParameter = $parameters->reject(static function (ReflectionParameter $parameter): bool { + return $parameter->isDefaultValueAvailable() || $parameter->isVariadic(); + }) + ->first(); + + if ($missingRequiredParameter === null) { + return; + } + + \assert($missingRequiredParameter instanceof ReflectionParameter); + + throw new TypeError('Missing required argument $'.$missingRequiredParameter->getName().' for middleware '.static::class.'::handle()'); + } + + private static function validateArgumentListIsNotAnAssociativeArray(Collection $arguments): void + { + if (Arr::isAssoc($arguments->all())) { + throw new TypeError('Expected a non-associative array in HasParameters::in() but received an associative array. You should use the HasParameters::with() method instead.'); + } + } + + private static function validateArgumentMapIsAnAssociativeArray(Collection $arguments): void + { + if ($arguments->isNotEmpty() && ! Arr::isAssoc($arguments->all())) { + throw new TypeError('Expected an associative array in HasParameters::with() but received a non-associative array. You should use the HasParameters::in() method instead.'); + } + } + + private static function validateNoUnexpectedArguments(Collection $parameters, Collection $arguments): void + { + $unexpectedArgument = $arguments->keys() + ->first(static function (string $name) use ($parameters): bool { + return ! $parameters->has($name); + }); + + if ($unexpectedArgument === null) { + return; + } + + \assert(\is_string($unexpectedArgument)); + + throw new TypeError('Unknown argument $'.$unexpectedArgument.' passed to middleware '.static::class.'::handle()'); + } +} diff --git a/tests/HasParametersTest.php b/tests/HasParametersTest.php new file mode 100644 index 0000000..85f0f89 --- /dev/null +++ b/tests/HasParametersTest.php @@ -0,0 +1,264 @@ +assertSame('Tests\\Middleware\\Basic', $result); + + $result = Basic::in([null]); + $this->assertSame('Tests\\Middleware\\Basic:', $result); + + $result = Basic::in(['']); + $this->assertSame('Tests\\Middleware\\Basic:', $result); + + $result = Basic::in([' ']); + $this->assertSame('Tests\\Middleware\\Basic: ', $result); + + $result = Basic::in([1.2]); + $this->assertSame('Tests\\Middleware\\Basic:1.2', $result); + + $result = Basic::in(['laravel']); + $this->assertSame('Tests\\Middleware\\Basic:laravel', $result); + + $result = Basic::in(['laravel', 'vue']); + $this->assertSame('Tests\\Middleware\\Basic:laravel,vue', $result); + + $result = Basic::in(['laravel', ' ', null, 'tailwind']); + $this->assertSame('Tests\\Middleware\\Basic:laravel, ,,tailwind', $result); + + $result = Basic::in(new Collection(['laravel', 'vue'])); + $this->assertSame('Tests\\Middleware\\Basic:laravel,vue', $result); + + $result = Basic::in([new Collection(['laravel', 'vue'])]); + $this->assertSame('Tests\\Middleware\\Basic:["laravel","vue"]', $result); + + $result = Basic::in([true, false]); + $this->assertSame('Tests\\Middleware\\Basic:1,0', $result); + + $result = Variadic::in([]); + $this->assertSame('Tests\\Middleware\\Variadic', $result); + + $result = Variadic::in(['laravel', 'vue']); + $this->assertSame('Tests\\Middleware\\Variadic:laravel,vue', $result); + + $result = RequiredOptionalVariadic::in(['laravel']); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel', $result); + + $result = RequiredOptionalVariadic::in(['laravel', 'vue', 'tailwind', 'react']); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,vue,tailwind,react', $result); + } + + public function testListDoesNotAcceptSubArray(): void + { + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('Array to string conversion'); + + Basic::in(['laravel', ['vue', 'react']]); + } + + public function testListDetectsRequiredParametersThatHaveNotBeenProvided(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Missing required argument $required for middleware Tests\\Middleware\\OptionalRequired::handle()'); + + OptionalRequired::in(['laravel']); + } + + public function testListDoesNotAcceptAssociativeArray(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Expected a non-associative array in HasParameters::in() but received an associative array. You should use the HasParameters::with() method instead.'); + + Basic::in(['framework' => 'laravel']); + } + + public function testMap(): void + { + $result = Required::with(['required' => null]); + $this->assertSame('Tests\\Middleware\\Required:', $result); + + $result = Required::with(['required' => '']); + $this->assertSame('Tests\\Middleware\\Required:', $result); + + $result = Required::with(['required' => ' ']); + $this->assertSame('Tests\\Middleware\\Required: ', $result); + + $result = Required::with(['required' => false]); + $this->assertSame('Tests\\Middleware\\Required:0', $result); + + $result = Required::with(['required' => true]); + $this->assertSame('Tests\\Middleware\\Required:1', $result); + + $result = Required::with(['required' => 'laravel']); + $this->assertSame('Tests\\Middleware\\Required:laravel', $result); + + $result = Required::with(['required' => 1.2]); + $this->assertSame('Tests\\Middleware\\Required:1.2', $result); + + $result = Required::with(new Collection(['required' => 'laravel'])); + $this->assertSame('Tests\\Middleware\\Required:laravel', $result); + + $result = Required::with(['required' => new Collection(['laravel', 'vue'])]); + $this->assertSame('Tests\\Middleware\\Required:["laravel","vue"]', $result); + + $result = Optional::with([]); + $this->assertSame('Tests\\Middleware\\Optional:default', $result); + + $result = Optional::with(['optional' => null]); + $this->assertSame('Tests\\Middleware\\Optional:', $result); + + $result = Optional::with(['optional' => '']); + $this->assertSame('Tests\\Middleware\\Optional:', $result); + + $result = Optional::with(['optional' => ' ']); + $this->assertSame('Tests\\Middleware\\Optional: ', $result); + + $result = Optional::with(['optional' => 1.2]); + $this->assertSame('Tests\\Middleware\\Optional:1.2', $result); + + $result = Optional::with(['optional' => 'laravel']); + $this->assertSame('Tests\\Middleware\\Optional:laravel', $result); + + $result = Optional::with(new Collection(['optional' => 'laravel'])); + $this->assertSame('Tests\\Middleware\\Optional:laravel', $result); + + $result = Optional::with(['optional' => new Collection(['laravel', 'vue'])]); + $this->assertSame('Tests\\Middleware\\Optional:["laravel","vue"]', $result); + + $result = Optional::with(['optional' => true]); + $this->assertSame('Tests\\Middleware\\Optional:1', $result); + + $result = Optional::with(['optional' => false]); + $this->assertSame('Tests\\Middleware\\Optional:0', $result); + + $result = Variadic::with(['variadic' => '']); + $this->assertSame('Tests\\Middleware\\Variadic:', $result); + + $result = Variadic::with(['variadic' => ' ']); + $this->assertSame('Tests\\Middleware\\Variadic: ', $result); + + $result = Variadic::with(['variadic' => 1.2]); + $this->assertSame('Tests\\Middleware\\Variadic:1.2', $result); + + $result = Variadic::with(['variadic' => 'laravel']); + $this->assertSame('Tests\\Middleware\\Variadic:laravel', $result); + + $result = Variadic::with(['variadic' => ['laravel', 'vue']]); + $this->assertSame('Tests\\Middleware\\Variadic:laravel,vue', $result); + + $result = Variadic::with(['variadic' => ['laravel', ' ', null, 'vue']]); + $this->assertSame('Tests\\Middleware\\Variadic:laravel, ,,vue', $result); + + $result = Variadic::with(['variadic' => new Collection(['laravel', 'vue'])]); + $this->assertSame('Tests\\Middleware\\Variadic:laravel,vue', $result); + + $result = Variadic::with(['variadic' => [new Collection(['laravel', 'vue'])]]); + $this->assertSame('Tests\\Middleware\\Variadic:["laravel","vue"]', $result); + + $result = Variadic::with(['variadic' => true]); + $this->assertSame('Tests\\Middleware\\Variadic:1', $result); + + $result = Variadic::with(['variadic' => false]); + $this->assertSame('Tests\\Middleware\\Variadic:0', $result); + + $result = OptionalRequired::with(['required' => 'laravel']); + $this->assertSame('Tests\\Middleware\\OptionalRequired:default,laravel', $result); + + $result = OptionalRequired::with(['required' => 'laravel', 'optional' => 'vue']); + $this->assertSame('Tests\\Middleware\\OptionalRequired:vue,laravel', $result); + + $result = RequiredOptionalVariadic::with(['required' => 'laravel']); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,default', $result); + + $result = RequiredOptionalVariadic::with(['required' => 'laravel', 'optional' => 'vue']); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,vue', $result); + + $result = RequiredOptionalVariadic::with(['required' => 'laravel', 'optional' => 'vue', 'variadic' => 'tailwind']); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,vue,tailwind', $result); + + $result = RequiredOptionalVariadic::with(['required' => 'laravel', 'optional' => 'vue', 'variadic' => ['tailwind', 'react']]); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,vue,tailwind,react', $result); + + $result = RequiredOptionalVariadic::with(['required' => 'laravel', 'optional' => 'vue', 'variadic' => []]); + $this->assertSame('Tests\\Middleware\\RequiredOptionalVariadic:laravel,vue', $result); + } + + public function testMapDoesNotAcceptSubArray(): void + { + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('Array to string conversion'); + + Required::with(['required' => ['vue', 'react']]); + } + + public function testMapMustContainRequiredArguments(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Missing required argument $required for middleware Tests\\Middleware\\RequiredOptionalVariadic::handle()'); + + RequiredOptionalVariadic::with(['optional' => 'vue']); + } + + public function testMapMustHaveEnoughRequiredArguments(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Missing required argument $required for middleware Tests\\Middleware\\Required::handle()'); + + Required::with([]); + } + + public function testMapDoesNotAcceptANonAssociativeArray(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Expected an associative array in HasParameters::with() but received a non-associative array. You should use the HasParameters::in() method instead.'); + + Basic::with(['framework', 'laravel']); + } + + public function testMapMustPassCorrectRequiredArguments(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Unknown argument $missing passed to middleware Tests\\Middleware\\Required::handle()'); + + Required::with(['missing' => 'test']); + } + + public function testMapVariadicWithIncorrectArgumentName(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Unknown argument $missing passed to middleware Tests\\Middleware\\Variadic::handle()'); + + Variadic::with(['missing' => 'laravel']); + } + + public function testVariadicDoesNotAcceptSubArray(): void + { + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('Array to string conversion'); + + Variadic::with(['variadic' => [['laravel', 'vue']]]); + } + + public function testMiddlewareThatUsesFuncGetArgsCanAccessArgumentsThatAreNotPassedAsParameters(): void + { + $result = OptionalRequired::in(['laravel', 'vue', 'tailwind']); + $this->assertSame('Tests\\Middleware\\OptionalRequired:laravel,vue,tailwind', $result); + } +} diff --git a/tests/Middleware/Basic.php b/tests/Middleware/Basic.php new file mode 100644 index 0000000..601d8a8 --- /dev/null +++ b/tests/Middleware/Basic.php @@ -0,0 +1,19 @@ +