diff --git a/.gitattributes b/.gitattributes index a143f13..d4fd5de 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,5 +11,4 @@ *.dist export-ignore *.md export-ignore *.yml export-ignore -.php-cs-fixer.dist.php export-ignore Taskfile.yml export-ignore diff --git a/.github/actions/php-cs-fixer/action.yml b/.github/actions/php-cs-fixer/action.yml deleted file mode 100644 index 2b6cbee..0000000 --- a/.github/actions/php-cs-fixer/action.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: PHP CS Fixer -description: Run PHP CS Fixer in a Docker container - -inputs: - workspace: - description: 'The workspace directory to mount in the container' - required: true - args: - description: 'Additional arguments to pass to php-cs-fixer' - required: false - default: '--allow-risky=yes' - github-token: - description: 'GitHub token for authenticating with GHCR' - required: true - -runs: - using: composite - steps: - - name: Login to GitHub Container Registry - shell: bash - run: echo "${{ inputs.github-token }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - - name: Pull a Craftzing owned image - shell: bash - run: docker pull ghcr.io/craftzing/docker-images/php:8.4 - - - name: Run php-cs-fixer inside the container - shell: bash - run: | - docker run --rm \ - -v "${{ inputs.workspace }}:/app" \ - -w /app \ - ghcr.io/craftzing/docker-images/php:8.4 \ - bash -c " - VERSION=3.88.2 && \ - wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v\$VERSION/php-cs-fixer.phar -O php-cs-fixer && \ - chmod a+x php-cs-fixer && \ - mv php-cs-fixer /usr/local/bin/php-cs-fixer && \ - php-cs-fixer --version && \ - php-cs-fixer fix ${{ inputs.args }} - " diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index a0ee0bf..12bb8e6 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -5,30 +5,53 @@ on: branches: ['main'] jobs: - phpstan: + mago: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # https://github.com/shivammathur/setup-php/releases/tag/2.37.1 with: - php-version: 8.4 + php-version: 8.5 extensions: -pdo_mysql, -mysqli - run: composer update --prefer-stable --prefer-dist --no-interaction - - run: composer phpstan - - php-cs-fixer: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - with: - ref: ${{ github.head_ref }} - - name: 'Run PHP CS fixer' - uses: ./.github/actions/php-cs-fixer - with: - workspace: ${{ github.workspace }} - args: --config=.php-cs-fixer.dist.php --allow-risky=yes - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: 'Commit CS fixes' - uses: craftzing/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 - with: - commit_message: Fix code style violations + # Mago is installed through Composer, but the package is just a thin wrapper. By checking the version, we force + # the wrapper to download the binary before running actual checks in order not to pollute the check output... + - run: composer mago -- --version + - run: composer lint:check + id: lint + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: composer format:check + id: format + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: composer analyse:check + id: analyse + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Fail upon detected issues' + if: | + steps.lint.outcome == 'failure' || + steps.format.outcome == 'failure' || + steps.analyse.outcome == 'failure' + env: + LINT_OUTCOME: ${{ steps.lint.outcome }} + FORMAT_OUTCOME: ${{ steps.format.outcome }} + ANALYSE_OUTCOME: ${{ steps.analyse.outcome }} + run: | + if [ "$LINT_OUTCOME" == "failure" ]; then + echo "::error title=Lint::Linter detected issues." + fi + + if [ "$FORMAT_OUTCOME" == "failure" ]; then + echo "::error title=Format::Formatter detected files needing formatting." + fi + + if [ "$ANALYSE_OUTCOME" == "failure" ]; then + echo "::error title=Analyse::Analyzer detected issues." + fi + + exit 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf5aadf..95593d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,4 +22,4 @@ jobs: coverage: pcov extensions: -pdo_mysql, -mysqli - run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - - run: composer coverage:summary + - run: composer test:coverage diff --git a/.gitignore b/.gitignore index fb19485..229729c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,3 @@ /vendor /composer.lock /.phpunit.result.cache -/.php_cs.cache -/.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php deleted file mode 100644 index 1434259..0000000 --- a/.php-cs-fixer.dist.php +++ /dev/null @@ -1,55 +0,0 @@ -in([ - __DIR__ . '/src', - ]) - ->name('*.php') - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -return (new Config()) - ->setParallelConfig(ParallelConfigFactory::detect()) - ->setRiskyAllowed(true) - ->setRules([ - '@PHP84Migration' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => [ - 'imports_order' => ['class', 'function', 'const'], - 'sort_algorithm' => 'alpha', - ], - 'fully_qualified_strict_types' => [ - 'phpdoc_tags' => [], - ], - 'no_unused_imports' => true, - 'blank_line_after_opening_tag' => true, - 'declare_strict_types' => true, - 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline' => [ - 'elements' => ['arguments', 'arrays', 'match', 'parameters'], - ], - 'phpdoc_scalar' => true, - 'unary_operator_spaces' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], - ], - 'no_extra_blank_lines' => [ - 'tokens' => ['parenthesis_brace_block', 'return', 'square_brace_block', 'extra'], - ], - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, - 'method_argument_space' => [ - 'on_multiline' => 'ensure_fully_multiline', - 'keep_multiple_spaces_after_comma' => true, - ], - 'void_return' => true, - 'single_quote' => true, - ]) - ->setFinder($finder); diff --git a/Taskfile.yml b/Taskfile.yml index 7c2d679..b59860d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -58,4 +58,4 @@ tasks: - for: var: versions split: ',' - cmd: docker compose exec php-{{ .ITEM }} composer phpunit + cmd: docker compose exec php-{{ .ITEM }} composer test diff --git a/composer.json b/composer.json index e6df89b..ed57f5e 100644 --- a/composer.json +++ b/composer.json @@ -24,14 +24,13 @@ "fakerphp/faker": "^1.24" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75", + "carthage-software/mago": "^1.29.0", "illuminate/collections": "^10.10|^11.0|^12.0", "illuminate/console": "^10.10|^11.0|^12.0", "illuminate/container": "^10.10|^11.0|^12.0", "illuminate/contracts": "^10.10|^11.0|^12.0", "illuminate/support": "^10.10|^11.0|^12.0", "orchestra/testbench": "^9.9", - "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^11.5", "saloonphp/saloon": "^3.13" }, @@ -63,26 +62,29 @@ "minimum-stability": "dev", "prefer-stable": true, "scripts": { - "cs:check": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --verbose --dry-run", - "cs:fix": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --verbose", - "phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --ansi", + "mago": "vendor/bin/mago --colors=always", "phpunit": "vendor/bin/phpunit --colors=always --display-phpunit-deprecations --display-deprecations", - "test": [ - "@composer cs:check", - "@composer phpstan", - "@composer phpunit" + "lint:check": "@composer mago -- lint --minimum-fail-level=warning --reporting-format=rich", + "lint:fix": "@composer mago -- lint --fix", + "format:check": "@composer mago -- format --dry-run", + "format:fix": "@composer mago -- format", + "analyse:check": "@composer mago -- analyse --minimum-fail-level=warning --reporting-format=rich", + "static-analysis:check": [ + "@composer lint:check", + "@composer format:check", + "@composer analyse:check" ], - "coverage:summary": "@composer phpunit -- --coverage-text", - "coverage:report": "@composer phpunit -- --coverage-html=.reports/coverage" + "static-analysis:fix": [ + "@composer lint:fix", + "@composer format:fix" + ], + "test": "@composer phpunit", + "test:coverage": "@composer phpunit -- --coverage-text", + "test:report": "@composer phpunit -- --coverage-html=.reports/coverage" }, "extra": { "branch-alias": { "dev-main": "1.0.x-dev" - }, - "laravel": { - "providers": [ - "Craftzing\\TestBench\\Laravel\\ServiceProvider" - ] } } } diff --git a/mago.toml b/mago.toml new file mode 100644 index 0000000..cbc5a53 --- /dev/null +++ b/mago.toml @@ -0,0 +1,41 @@ +#:schema https://mago.carthage.software/1.29.0/schema.json +# For full documentation, see https://mago.carthage.software/tools/overview +version = "1" +php-version = "8.4.0" + +[source] +workspace = "." +paths = ["src"] +includes = ["vendor"] +excludes = [] + +[source.glob] +literal-separator = true + +[formatter] +preset = "psr-12" +print-width = 9999 # Horizontal limits are considered a guideline, not a requirement +space-around-assignment-in-declare = false +inline-empty-constructor-braces = true +trailing-comma = true +preserve-breaking-argument-list = true +preserve-breaking-parameter-list = true +preserve-breaking-array-like = true +preserve-breaking-attribute-list = true +preserve-breaking-member-access-chain = true + +[linter] +integrations = ["phpunit"] + +[linter.rules] +no-boolean-flag-parameter = { enabled = false } +too-many-methods = { exclude = ["src/**/*Test.php"] } +literal-named-argument = { exclude = ["src/**/*Test.php"] } +prefer-test-attribute = { enabled = true } + +[analyzer] +excludes = ["src/**/*Test.php"] +ignore = [ + "mixed-argument", + "mixed-assignment", +] diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 6e56e6e..0000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,9 +0,0 @@ -parameters: - level: 6 - paths: - - src - excludePaths: - - ./*/*Test.php - - ./*/Testing/* - ignoreErrors: - - '#is used zero times and is not analysed\.$#' diff --git a/src/Factories/ImmutableFactory.php b/src/Factories/ImmutableFactory.php index 40570dd..f4b47c7 100644 --- a/src/Factories/ImmutableFactory.php +++ b/src/Factories/ImmutableFactory.php @@ -14,6 +14,7 @@ /** * @template TClass of object + * @mago-expect lint:too-many-methods */ abstract class ImmutableFactory { @@ -34,7 +35,6 @@ final public function __construct( */ public function state(array $state): static { - // @phpstan-ignore-next-line return.type return new static($this->faker, [...$this->state, ...$state], $this->count); } @@ -43,7 +43,6 @@ public function state(array $state): static */ public function times(int $count): static { - // @phpstan-ignore-next-line return.type return new static($this->faker, $this->state, $count); } @@ -64,7 +63,7 @@ private function resolveValue(mixed $value): mixed return array_map($this->resolveValue(...), iterator_to_array($value)); } - if (! $value instanceof self) { + if (!$value instanceof self) { return $value; } @@ -102,7 +101,7 @@ public function rawMany(array $attributes = []): array */ public function rawCollection(array $attributes = []): Collection { - return Collection::times($this->count, fn (): array => $this->raw($attributes)); + return Collection::times($this->count, fn(): array => $this->raw($attributes)); } /** @@ -129,6 +128,6 @@ public function makeMany(array $attributes = []): array */ public function makeCollection(array $attributes = []): Collection { - return Collection::times($this->count, fn (): mixed => $this->makeOne($attributes)); + return Collection::times($this->count, fn(): mixed => $this->makeOne($attributes)); } } diff --git a/src/Factories/ImmutableFactoryTest.php b/src/Factories/ImmutableFactoryTest.php index e95cb35..65b2607 100644 --- a/src/Factories/ImmutableFactoryTest.php +++ b/src/Factories/ImmutableFactoryTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; use stdClass; +use function array_keys; use function collect; use function count; use function mt_rand; @@ -25,8 +26,7 @@ final class ImmutableFactoryTest extends TestCase use Conditionable; private ImmutableFactory $instance { - get => $this->instance ??= new class extends ImmutableFactory - { + get => $this->instance ??= new class extends ImmutableFactory { public function definition(): array { return [ @@ -164,9 +164,7 @@ public static function state(): iterable #[DataProvider('state')] public function itCanReturnRawAttributesWithState(array $attributes, array $state, array $expected): void { - $result = $this->instance - ->state($state) - ->raw($attributes); + $result = $this->instance->state($state)->raw($attributes); collect($expected)->each(function (mixed $value, string $attribute) use ($result): void { $this->assertSame($value, $result[$attribute]); @@ -212,9 +210,7 @@ public function itCanReturnRawAttributesCollectionsWithState(array $attributes, #[DataProvider('state')] public function itCanMakeOneWithState(array $attributes, array $state, array $expected): void { - $result = $this->instance - ->state($state) - ->makeOne($attributes); + $result = $this->instance->state($state)->makeOne($attributes); $this->assertInstanceOf(stdClass::class, $result); collect($expected)->each(function (mixed $value, string $attribute) use ($result): void { @@ -262,13 +258,13 @@ public function itCanMakeCollectionsWithState(array $attributes, array $state, a public static function nestedFactories(): iterable { yield [ - fn (ImmutableFactory $instance): ImmutableFactory => $instance->state([ + static fn(ImmutableFactory $instance): ImmutableFactory => $instance->state([ 'nested' => $instance->state([ 'deeplyNested' => $instance->state(['resolved' => true]), ]), 'nestedArray' => $instance->times(2)->state(['resolvedTimes' => true]), ]), - function (stdClass $result): void { + static function (stdClass $result): void { self::assertObjectHasProperty('nested', $result); self::assertInstanceOf(stdClass::class, $result->nested); self::assertObjectHasProperty('deeplyNested', $result->nested); @@ -277,7 +273,7 @@ function (stdClass $result): void { self::assertObjectHasProperty('nestedArray', $result); self::assertContainsOnlyInstancesOf(stdClass::class, $result->nestedArray); self::assertCount(2, $result->nestedArray); - collect($result->nestedArray)->each(function (stdClass $item): void { + collect($result->nestedArray)->each(static function (stdClass $item): void { self::assertObjectHasProperty('resolvedTimes', $item); self::assertTrue($item->resolvedTimes); }); @@ -312,7 +308,7 @@ public function itCanReturnManyRawAttributesWithNestedFactories(callable $resolv $results = $instance->rawMany(); - collect($results)->each(function (array $result) use ($assert): void { + collect($results)->each(static function (array $result) use ($assert): void { $assert((object) $result); }); } @@ -331,7 +327,7 @@ public function itCanReturnRawAttributesCollectionsWithNestedFactories( $results = $instance->rawCollection(); - $results->each(function (array $result) use ($assert): void { + $results->each(static function (array $result) use ($assert): void { $assert((object) $result); }); } @@ -368,7 +364,7 @@ public function itCanMakeManyWithNestedFactories(callable $resolveInstance, call /** * @param callable(ImmutableFactory): ImmutableFactory $resolveInstance - * @param callable(stdClass): void $assert + * @param callable(stdClass, int): mixed $assert */ #[Test] #[DataProvider('nestedFactories')] @@ -397,14 +393,14 @@ private function assertFactoryProperties( private function assertHasPropertyForEachDefinition(stdClass $result): void { - foreach ($this->instance->definition() as $attribute => $value) { + foreach (array_keys($this->instance->definition()) as $attribute) { self::assertObjectHasProperty($attribute, $result); } } private function assertHasArrayKeyForEachDefinition(array $result): void { - foreach ($this->instance->definition() as $attribute => $value) { + foreach (array_keys($this->instance->definition()) as $attribute) { self::assertArrayHasKey($attribute, $result); } } diff --git a/src/Factories/InstanceFactory.php b/src/Factories/InstanceFactory.php index 21b414d..a9de1ab 100644 --- a/src/Factories/InstanceFactory.php +++ b/src/Factories/InstanceFactory.php @@ -32,4 +32,3 @@ public function make(array $attributes): object return $instance; } } - diff --git a/src/Factories/InstanceFactoryTest.php b/src/Factories/InstanceFactoryTest.php index e8ad155..321533e 100644 --- a/src/Factories/InstanceFactoryTest.php +++ b/src/Factories/InstanceFactoryTest.php @@ -14,8 +14,7 @@ final class InstanceFactoryTest extends TestCase #[Test] public function itFailsWhenConstructingInstancesWithPropertiesThatDoNotExist(): void { - $subject = new readonly class('some-id') - { + $subject = new readonly class('some-id') { public function __construct( public string $id, ) {} @@ -34,8 +33,7 @@ public function itCanConstructInstances(): void 'protected' => 'Protected', 'private' => 'Private', ]; - $subject = new readonly class(...$attributes) - { + $subject = new readonly class(...$attributes) { public function __construct( public string $public, protected string $protected, @@ -63,8 +61,7 @@ public function private(): string #[Test] public function itConstructsInstancesWithUninitializedPropertiesWhenNotProvidingAttributes(): void { - $subject = new readonly class('some-id') - { + $subject = new readonly class('some-id') { public function __construct( public string $id, ) {} diff --git a/src/GraphQL/Constraints/HasErrorOnPath.php b/src/GraphQL/Constraints/HasErrorOnPath.php index 28c6bc7..423c8f7 100644 --- a/src/GraphQL/Constraints/HasErrorOnPath.php +++ b/src/GraphQL/Constraints/HasErrorOnPath.php @@ -7,7 +7,6 @@ use InvalidArgumentException; use Override; use PHPUnit\Framework\Constraint\Constraint; -use TypeError; use function gettype; use function implode; @@ -16,28 +15,11 @@ final class HasErrorOnPath extends Constraint { - /** - * @var array> - */ - private static array $responseResolvers = []; - public function __construct( public readonly string $path, public readonly string $category = 'graphql', ) {} - /** - * @param (callable(mixed): array)|null $resolveResponseUsing - */ - public static function resolveResponseUsing(?callable $resolveResponseUsing): void - { - if ($resolveResponseUsing === null) { - self::$responseResolvers = []; - } else { - self::$responseResolvers[] = $resolveResponseUsing; - } - } - public function authentication(): self { return new self($this->path, 'authentication'); @@ -59,14 +41,16 @@ protected function matches(mixed $other): bool $response = match (true) { is_array($other) => $other, is_iterable($other) => iterator_to_array($other), - default => $this->resolveResponse($other), + default => throw new InvalidArgumentException( + self::class . ' can only be evaluated for iterable values, got ' . gettype($other) . '.', + ), }; - is_array($response) or throw new InvalidArgumentException( - self::class . ' can only be evaluated for iterable values, got ' . gettype($other) . '.', - ); + if (!is_array($response['errors'] ?? null)) { + return false; + } - foreach ($response['errors'] ?? [] as $error) { + foreach ($response['errors'] as $error) { $path = implode('.', $error['path'] ?? ''); $category = $error['extensions']['category'] ?? ''; @@ -84,24 +68,8 @@ protected function matches(mixed $other): bool return false; } - /** - * @return array|null - */ - private function resolveResponse(mixed $other): ?array - { - foreach (self::$responseResolvers as $resolver) { - try { - return $resolver($other); - } catch (TypeError) { - continue; - } - } - - return null; - } - public function toString(): string { - return "has error on `$this->path` of category `$this->category`"; + return "has error on `{$this->path}` of category `{$this->category}`"; } } diff --git a/src/GraphQL/Constraints/HasErrorOnPathTest.php b/src/GraphQL/Constraints/HasErrorOnPathTest.php index cc3d11b..6f65d55 100644 --- a/src/GraphQL/Constraints/HasErrorOnPathTest.php +++ b/src/GraphQL/Constraints/HasErrorOnPathTest.php @@ -5,10 +5,8 @@ namespace Craftzing\TestBench\GraphQL\Constraints; use Faker\Factory; -use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use InvalidArgumentException; -use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; @@ -20,12 +18,6 @@ */ final class HasErrorOnPathTest extends TestCase { - #[Before] - public function setupHasErrorOnPath(): void - { - HasErrorOnPath::resolveResponseUsing(null); - } - #[Test] public function itCanExpectErrorsOfCategoryAuthentication(): void { @@ -85,7 +77,7 @@ public function ifFailsWhenNoError(): void $response = ['data' => 'ok']; $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("has error on `$path` of category `$category`"); + $this->expectExceptionMessage("has error on `{$path}` of category `{$category}`"); $this->assertThat($response, new HasErrorOnPath($path, $category)); } @@ -130,7 +122,7 @@ public function itFailsWhenErrorNotOnGivenPath(): void ]; $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("has error on `$path` of category `$category`"); + $this->expectExceptionMessage("has error on `{$path}` of category `{$category}`"); $this->assertThat($response, new HasErrorOnPath($path, $category)); } @@ -152,7 +144,7 @@ public function itFailsWhenErrorNotOfGivenCategory(): void ]; $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("has error on `$path` of category `$category`"); + $this->expectExceptionMessage("has error on `{$path}` of category `{$category}`"); $this->assertThat($response, new HasErrorOnPath($path, $category)); } @@ -190,54 +182,4 @@ public function itPassesWhenErrorOnPathIsOfGivenCategory(string $path, string $c { $this->assertThat($response, new HasErrorOnPath($path, $category)); } - - #[Test] - public function itFailsWhenResponseCouldNotBeResolvedForSubject(): void - { - HasErrorOnPath::resolveResponseUsing(fn (Arrayable $subject): array => $subject->toArray()); - $response = new readonly class - { - public function toArray(): array - { - return []; - } - }; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(HasErrorOnPath::class . ' can only be evaluated for iterable values'); - - $this->assertThat($response, new HasErrorOnPath('somePath')); - } - - #[Test] - public function itPassesResponseCouldBeResolvedForSubject(): void - { - HasErrorOnPath::resolveResponseUsing(fn (Arrayable $subject): array => $subject->toArray()); - $category = Factory::create()->word(); - $path = 'somePath'; - - $response = new readonly class ($path, $category) implements Arrayable - { - public function __construct( - private string $path, - private string $category, - ) {} - - public function toArray(): array - { - return [ - 'errors' => [ - [ - 'path' => [$this->path], - 'extensions' => [ - 'category' => $this->category, - ], - ], - ], - ]; - } - }; - - $this->assertThat($response, new HasErrorOnPath($path, $category)); - } } diff --git a/src/Laravel/Constraint/Bus/HasHandler.php b/src/Laravel/Constraint/Bus/HasHandler.php index 667d793..22abfa9 100644 --- a/src/Laravel/Constraint/Bus/HasHandler.php +++ b/src/Laravel/Constraint/Bus/HasHandler.php @@ -6,13 +6,14 @@ use Craftzing\TestBench\PHPUnit\Constraint\ProvidesAdditionalFailureDescription; use Illuminate\Contracts\Bus\Dispatcher; -use Illuminate\Support\Facades\Bus; use InvalidArgumentException; use PHPUnit\Framework\Constraint\Constraint; use ReflectionClass; +use function app; use function class_exists; use function gettype; +use function is_object; use function is_string; final class HasHandler extends Constraint @@ -22,31 +23,40 @@ final class HasHandler extends Constraint private readonly Dispatcher $bus; public function __construct( - /* @var class-string */ + /** @var class-string */ private readonly string $handlerClassFQN, ) { - $this->bus = Bus::getFacadeRoot(); + $this->bus = app(Dispatcher::class); } protected function matches(mixed $other): bool { - is_string($other) or throw new InvalidArgumentException( - self::class . ' can only be evaluated for strings, got ' . gettype($other) . '.', - ); - class_exists($other) or throw new InvalidArgumentException( - self::class . " can only be evaluated for existing classes, got $other.", - ); + if (is_string($other) === false) { + throw new InvalidArgumentException( + self::class . ' can only be evaluated for strings, got ' . gettype($other) . '.', + ); + } + + if (class_exists($other) === false) { + throw new InvalidArgumentException( + self::class . " can only be evaluated for existing classes, got {$other}.", + ); + } + $message = new ReflectionClass($other)->newInstanceWithoutConstructor(); + /** @var object|string|false $actualHandler */ $actualHandler = $this->bus->getCommandHandler($message); if ($actualHandler === false) { - $this->additionalFailureDescriptions[] = "$other has no handler mapped to it."; + $this->additionalFailureDescriptions[] = "{$other} has no handler mapped to it."; return false; } - if ($actualHandler::class !== $this->handlerClassFQN) { - $this->additionalFailureDescriptions[] = "$other has a different handler mapped to it."; + $actualHandlerClass = is_object($actualHandler) ? $actualHandler::class : $actualHandler; + + if ($actualHandlerClass !== $this->handlerClassFQN) { + $this->additionalFailureDescriptions[] = "{$other} has a different handler mapped to it."; return false; } diff --git a/src/Laravel/Constraint/Bus/HasHandlerTest.php b/src/Laravel/Constraint/Bus/HasHandlerTest.php index 91b0a74..4ce569a 100644 --- a/src/Laravel/Constraint/Bus/HasHandlerTest.php +++ b/src/Laravel/Constraint/Bus/HasHandlerTest.php @@ -36,7 +36,7 @@ public function itCannotEvaluateStringThatAreNotExistingClasses(): void $value = 'NotAClass'; $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(HasHandler::class . " can only be evaluated for existing classes, got $value."); + $this->expectExceptionMessage(HasHandler::class . " can only be evaluated for existing classes, got {$value}."); $this->assertThat($value, new HasHandler('SomeHandlerClassFCN')); } diff --git a/src/Laravel/Constraint/Bus/RequiresBusFake.php b/src/Laravel/Constraint/Bus/RequiresBusFake.php index da2e119..359ee1b 100644 --- a/src/Laravel/Constraint/Bus/RequiresBusFake.php +++ b/src/Laravel/Constraint/Bus/RequiresBusFake.php @@ -24,9 +24,11 @@ private function resolveBusFake(): BusFake { $bus = Bus::getFacadeRoot(); - $bus instanceof BusFake or throw new LogicException( - 'To use the ' . self::class . ' constraint, make sure to call ' . self::class . '::spy() first.', - ); + if (!$bus instanceof BusFake) { + throw new LogicException( + 'To use the ' . self::class . ' constraint, make sure to call ' . self::class . '::spy() first.', + ); + } return $bus; } diff --git a/src/Laravel/Constraint/Bus/WasDispatched.php b/src/Laravel/Constraint/Bus/WasDispatched.php index 92bcc4b..6f8cd13 100644 --- a/src/Laravel/Constraint/Bus/WasDispatched.php +++ b/src/Laravel/Constraint/Bus/WasDispatched.php @@ -68,7 +68,7 @@ protected function matches(mixed $other): bool $matchingDispatchedCommands = $this->busFake->dispatched($commandName); $dispatchedEventsMatchingConstraints = $matchingDispatchedCommands->filter( - fn (object $dispatchedCommand): bool => $this->matchesCommandConstraints( + fn(object $dispatchedCommand): bool => $this->matchesCommandConstraints( $other, $dispatchedCommand, // When the command was dispatched exactly once, we should add all nested expectation failures to the @@ -114,7 +114,7 @@ protected function failureDescription(mixed $other): string $message = parent::failureDescription($other); if ($this->times !== null) { - $message .= " $this->times time(s)"; + $message .= " {$this->times} time(s)"; } $message .= match (true) { diff --git a/src/Laravel/Constraint/Bus/WasDispatchedTest.php b/src/Laravel/Constraint/Bus/WasDispatchedTest.php index db5c923..537074c 100644 --- a/src/Laravel/Constraint/Bus/WasDispatchedTest.php +++ b/src/Laravel/Constraint/Bus/WasDispatchedTest.php @@ -111,7 +111,7 @@ public function itPassesWhenDispatched(QuantableConstraint $quantise): void WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->assertThat($command::class, $quantise(new WasDispatched())); } @@ -127,7 +127,7 @@ public function itFailsWhenDispatchedButNotWithGivenCommandConstraints(): void $this->expectExceptionMessage('command was dispatched with given command constraints.'); $this->assertThat($command, new WasDispatched()->withConstraints( - new Callback(fn () => false), + new Callback(static fn() => false), )); } @@ -140,7 +140,7 @@ public function itPassesWhenDispatchedWithGivenCommandConstraints(): void Bus::dispatch($command); $this->assertThat($command, new WasDispatched()->withConstraints( - new Callback(fn () => true), + new Callback(static fn() => true), )); } @@ -150,10 +150,10 @@ public function itFailsWhenDispatchedButNotGivenTimes(QuantableConstraint $quant { WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("command was dispatched $quantise->expected time(s)."); + $this->expectExceptionMessage("command was dispatched {$quantise->expected} time(s)."); $this->assertThat($command::class, new WasDispatched()->times($quantise->expected)); } @@ -165,7 +165,7 @@ public function itPassesWhenDispatchedGivenTimes(QuantableConstraint $quantise): WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->assertThat($command::class, $quantise(new WasDispatched())); } @@ -177,14 +177,16 @@ public function itFailsWhenDispatchedWithGivenCommandConstrainsButNotGivenTimes( ): void { WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("command was dispatched $quantise->expected time(s)"); + $this->expectExceptionMessage("command was dispatched {$quantise->expected} time(s)"); - $this->assertThat($command, new WasDispatched()->times($quantise->expected)->withConstraints( - new Callback(fn () => true), - )); + $this->assertThat($command, new WasDispatched() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => true), + )); } #[Test] @@ -194,16 +196,18 @@ public function itFailsWhenDispatchedGivenTimesButNotWithGivenCommandConstrains( ): void { WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage( - "command was dispatched $quantise->expected time(s) with given command constraints.", + "command was dispatched {$quantise->expected} time(s) with given command constraints.", ); - $this->assertThat($command, new WasDispatched()->times($quantise->expected)->withConstraints( - new Callback(fn () => false), - )); + $this->assertThat($command, new WasDispatched() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => false), + )); } #[Test] @@ -214,19 +218,21 @@ public function itPassesWhenDispatchedGivenTimesWithGivenCommandConstraints( WasDispatched::spy(); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); - $this->assertThat($command, $quantise(new WasDispatched()->withConstraints( - new Callback(fn () => true), - ))); + $this->assertThat( + $command, + $quantise(new WasDispatched()->withConstraints( + new Callback(static fn() => true), + )), + ); } #[Test] public function itCannotDeriveCommandConstraintsFromCommandStrings(): void { WasDispatched::spy(); - $command = new readonly class - { + $command = new readonly class { public function __construct( public string $first = 'first', ) {} @@ -241,8 +247,7 @@ public function __construct( public function itCanDeriveCommandConstraintsFromCommandObjects(): void { WasDispatched::spy(); - $command = new readonly class - { + $command = new readonly class { public function __construct( public string $first = 'first', ) {} diff --git a/src/Laravel/Constraint/Bus/WasHandled.php b/src/Laravel/Constraint/Bus/WasHandled.php index a1295dd..fa813e8 100644 --- a/src/Laravel/Constraint/Bus/WasHandled.php +++ b/src/Laravel/Constraint/Bus/WasHandled.php @@ -11,7 +11,6 @@ use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Container\Container; -use Illuminate\Support\Facades\Bus; use Illuminate\Support\Traits\ReflectsClosures; use InvalidArgumentException; use LogicException; @@ -21,6 +20,7 @@ use PHPUnit\Framework\ExpectationFailedException; use ReflectionClass; +use function app; use function class_basename; use function class_exists; use function gettype; @@ -39,7 +39,7 @@ public function __construct( public readonly ?int $times = null, Constraint ...$constraints, ) { - $this->bus = Bus::getFacadeRoot(); + $this->bus = app(Dispatcher::class); $this->objectConstraints = $constraints; } @@ -77,6 +77,7 @@ protected function matches(mixed $other): bool $commandName => new ReflectionClass($other)->newInstanceWithoutConstructor(), default => $other, }; + // @mago-expect analyzer:possibly-invalid-argument $handler = $this->handler($command); try { @@ -98,7 +99,7 @@ private function handler(object $command): SpyCallable { $handler = $this->bus->getCommandHandler($command); - if (! $handler instanceof SpyCallable) { + if (!$handler instanceof SpyCallable) { throw new LogicException( 'To use the ' . self::class . ' constraint, make sure to call ' . self::class . '::using() first.', ); @@ -117,7 +118,7 @@ protected function failureDescription(mixed $other): string $message = parent::failureDescription($other); if ($this->times !== null) { - $message .= " $this->times time(s)"; + $message .= " {$this->times} time(s)"; } $message .= match (true) { diff --git a/src/Laravel/Constraint/Bus/WasHandledTest.php b/src/Laravel/Constraint/Bus/WasHandledTest.php index cd13903..372d60b 100644 --- a/src/Laravel/Constraint/Bus/WasHandledTest.php +++ b/src/Laravel/Constraint/Bus/WasHandledTest.php @@ -36,10 +36,9 @@ public function resetDeriveConstraintsFromObjectUsing(): void public static function callableHandlers(): iterable { - yield 'Closure' => [fn (stdClass $object): string => 'handled']; + yield 'Closure' => [static fn(stdClass $object): string => 'handled']; yield 'Invokable class' => [ - new readonly class - { + new readonly class { public function __invoke(stdClass $command): string { return 'handled'; @@ -101,7 +100,7 @@ public function itFailsWhenNotUsingGivenCallables(): void #[Test] public function itFailsWhenNotHandled(): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('command was handled.'); @@ -113,10 +112,10 @@ public function itFailsWhenNotHandled(): void #[DataProviderExternal(QuantableConstraint::class, 'cases')] public function itPassesWhenHandled(QuantableConstraint $quantise): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->assertThat($command::class, $quantise(new WasHandled())); } @@ -124,7 +123,7 @@ public function itPassesWhenHandled(QuantableConstraint $quantise): void #[Test] public function itFailsWhenHandledButNotWithGivenCommandConstraints(): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); Bus::dispatch($command); @@ -132,20 +131,20 @@ public function itFailsWhenHandledButNotWithGivenCommandConstraints(): void $this->expectExceptionMessage('command was handled with given command constraints.'); $this->assertThat($command::class, new WasHandled()->withConstraints( - new Callback(fn () => false), + new Callback(static fn() => false), )); } #[Test] public function itPassesWhenHandledWithGivenCommandConstraints(): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); Bus::dispatch($command); $this->assertThat($command::class, new WasHandled()->withConstraints( - new Callback(fn () => true), + new Callback(static fn() => true), )); } @@ -153,12 +152,12 @@ public function itPassesWhenHandledWithGivenCommandConstraints(): void #[DataProviderExternal(QuantableConstraint::class, 'tooFewOrTooManyTimes')] public function itFailsWhenHandledButNotGivenTimes(QuantableConstraint $quantise): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("command was handled $quantise->expected time(s)."); + $this->expectExceptionMessage("command was handled {$quantise->expected} time(s)."); $this->assertThat($command::class, new WasHandled()->times($quantise->expected)); } @@ -167,10 +166,10 @@ public function itFailsWhenHandledButNotGivenTimes(QuantableConstraint $quantise #[DataProviderExternal(QuantableConstraint::class, 'cases')] public function itPassesWhenHandledGivenTimes(QuantableConstraint $quantise): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->assertThat($command::class, $quantise(new WasHandled())); } @@ -180,16 +179,18 @@ public function itPassesWhenHandledGivenTimes(QuantableConstraint $quantise): vo public function itFailsWhenHandledWithGivenCommandConstrainsButNotGivenTimes( QuantableConstraint $quantise, ): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("command was handled $quantise->expected time(s)"); + $this->expectExceptionMessage("command was handled {$quantise->expected} time(s)"); - $this->assertThat($command, new WasHandled()->times($quantise->expected)->withConstraints( - new Callback(fn () => true), - )); + $this->assertThat($command, new WasHandled() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => true), + )); } #[Test] @@ -197,18 +198,20 @@ public function itFailsWhenHandledWithGivenCommandConstrainsButNotGivenTimes( public function itFailsWhenHandledGivenTimesButNotWithGivenCommandConstrains( QuantableConstraint $quantise, ): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage( - "command was handled $quantise->expected time(s) with given command constraints.", + "command was handled {$quantise->expected} time(s) with given command constraints.", ); - $this->assertThat($command, new WasHandled()->times($quantise->expected)->withConstraints( - new Callback(fn () => false), - )); + $this->assertThat($command, new WasHandled() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => false), + )); } #[Test] @@ -216,21 +219,23 @@ public function itFailsWhenHandledGivenTimesButNotWithGivenCommandConstrains( public function itPassesWhenHandledGivenTimesWithGivenCommandConstraints( QuantableConstraint $quantise, ): void { - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); $command = new stdClass(); - $quantise->applyTo(fn () => Bus::dispatch($command)); + $quantise->applyTo(static fn() => Bus::dispatch($command)); - $this->assertThat($command, $quantise(new WasHandled()->withConstraints( - new Callback(fn () => true), - ))); + $this->assertThat( + $command, + $quantise(new WasHandled()->withConstraints( + new Callback(static fn() => true), + )), + ); } #[Test] public function itCannotDeriveCommandConstraintsFromCommandStrings(): void { - $command = new readonly class - { + $command = new readonly class { public function __construct( public string $first = 'first', ) {} @@ -244,8 +249,7 @@ public function __construct( #[Test] public function itCanDeriveCommandConstraintsFromCommandObjects(): void { - $command = new readonly class - { + $command = new readonly class { public function __construct( public string $first = 'first', ) {} @@ -274,7 +278,7 @@ public function itCanDeriveCommandConstraintsFromCommandObjectsUsingCustomImplem public function itFailsWhenHandledButNotWithDerivedCommandConstraints(): void { $command = new stdClass(); - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); WasHandled::deriveConstraintsFromObjectUsing(DeriveConstraintsFromObjectUsingFakes::failingConstraints()); Bus::dispatch($command); @@ -288,7 +292,7 @@ public function itFailsWhenHandledButNotWithDerivedCommandConstraints(): void public function itPassesWhenDispatchedWithDerivedCommandConstraints(): void { $command = new stdClass(); - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); WasHandled::deriveConstraintsFromObjectUsing(DeriveConstraintsFromObjectUsingFakes::passingConstraints()); Bus::dispatch($command); @@ -304,15 +308,23 @@ public function itDerivesConstraintsFromExpectedCommandsAndMatchesItAgainstActua $constraint = Spy::passing(); $deriveConstraints = new DeriveConstraintsFromObjectUsingFakes([$constraint]); WasHandled::deriveConstraintsFromObjectUsing($deriveConstraints); - WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + WasHandled::using(static fn(stdClass $command): string => 'handled', $this->app); Bus::dispatch($actual); $this->assertThat($expected, new WasHandled()); $deriveConstraints->invoke->assert(new WasCalled()->withSame($expected)); - $deriveConstraints->invoke->assert(new WasCalled()->never()->withSame($actual)); + $deriveConstraints->invoke->assert( + new WasCalled() + ->never() + ->withSame($actual), + ); $constraint->matches->assert(new WasCalled()->withSame($actual)); - $constraint->matches->assert(new WasCalled()->never()->withSame($expected)); + $constraint->matches->assert( + new WasCalled() + ->never() + ->withSame($expected), + ); } private function command(array $properties): stdClass diff --git a/src/Laravel/Constraint/Bus/WithoutBusMiddleware.php b/src/Laravel/Constraint/Bus/WithoutBusMiddleware.php index d3035af..83ba9f5 100644 --- a/src/Laravel/Constraint/Bus/WithoutBusMiddleware.php +++ b/src/Laravel/Constraint/Bus/WithoutBusMiddleware.php @@ -8,9 +8,11 @@ trait WithoutBusMiddleware { + abstract public function afterApplicationCreated(callable $callback): void; + public function setUpWithoutBusMiddleware(): void { - $this->afterApplicationCreated(function (): void { + $this->afterApplicationCreated(static function (): void { Bus::pipeThrough([]); }); } diff --git a/src/Laravel/Constraint/Bus/WithoutBusMiddlewareTest.php b/src/Laravel/Constraint/Bus/WithoutBusMiddlewareTest.php index ce35700..cc541ab 100644 --- a/src/Laravel/Constraint/Bus/WithoutBusMiddlewareTest.php +++ b/src/Laravel/Constraint/Bus/WithoutBusMiddlewareTest.php @@ -17,7 +17,7 @@ final class WithoutBusMiddlewareTest extends TestCase public function itCanRemoveBusMiddleware(): void { Bus::pipeThrough([ - fn (mixed $command, mixed $next): mixed => throw new LogicException('This should not happen'), + static fn(mixed $command, mixed $next): mixed => throw new LogicException('This should not happen'), ]); $this->setUpWithoutBusMiddleware(); diff --git a/src/Laravel/Constraint/Eloquent/ModelComparator.php b/src/Laravel/Constraint/Eloquent/ModelComparator.php index 5142c78..2db93c8 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparator.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparator.php @@ -26,22 +26,29 @@ public function assertEquals( bool $canonicalize = false, bool $ignoreCase = false, ): void { - $expected instanceof Model or throw self::notInstanceOfModel('expected', $expected); - $actual instanceof Model or throw self::notInstanceOfModel('actual', $actual); - - $actual->is($expected) or throw new ComparisonFailure( - $expected, - $actual, - $this->serializeModelForException($expected), - $this->serializeModelForException($actual), - 'Failed asserting that two Eloquent models are equal.', - ); + if (!$expected instanceof Model) { + throw self::notInstanceOfModel('expected', $expected); + } + + if (!$actual instanceof Model) { + throw self::notInstanceOfModel('actual', $actual); + } + + if ($actual->isNot($expected)) { + throw new ComparisonFailure( + $expected, + $actual, + $this->serializeModelForException($expected), + $this->serializeModelForException($actual), + 'Failed asserting that two Eloquent models are equal.', + ); + } } private static function notInstanceOfModel(string $argumentName, mixed $value): AssertionError { return new AssertionError( - "Argument $argumentName must be an instance of " . Model::class . ', received ' . gettype($value) . '.', + "Argument {$argumentName} must be an instance of " . Model::class . ', received ' . gettype($value) . '.', ); } @@ -55,6 +62,6 @@ private function serializeModelForException(Model $model): string 'table' => $properties['table'], 'primaryKey' => $properties['primaryKey'], 'attributes' => $properties['attributes'], - ], JSON_PRETTY_PRINT); + ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); } } diff --git a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php index 0d5c82a..753e9bb 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php @@ -123,8 +123,8 @@ public function itPassesWhenEqual(Model $expected, Model $actual): void private static function model(string $table, string $connection, string $id = ''): Model { - $id ??= new Randomizer()->getInt(1, 10000000000); - $model = new class(compact('id')) extends Model { + $id ??= new Randomizer()->getInt(1, 10_000_000_000); + $model = new class(compact('id')) extends Model { protected $fillable = ['id']; protected $keyType = 'string'; }; diff --git a/src/Laravel/Constraint/Events/HasListener.php b/src/Laravel/Constraint/Events/HasListener.php index 244dbce..ff40c18 100644 --- a/src/Laravel/Constraint/Events/HasListener.php +++ b/src/Laravel/Constraint/Events/HasListener.php @@ -27,16 +27,16 @@ final class HasListener extends Constraint public function __construct( public string $listener { set(string $listener) { - class_exists($listener) or throw new InvalidArgumentException("$listener is not an existing class."); + class_exists($listener) or throw new InvalidArgumentException("{$listener} is not an existing class."); $this->listener = $listener; } }, public string $method = self::DEFAULT_METHOD { set(string $method) { - method_exists($this->listener, $method) or throw new InvalidArgumentException( - "Method $this->listener::$method does not exist.", - ); + if (method_exists($this->listener, $method) === false) { + throw new InvalidArgumentException("Method {$this->listener}::{$method} does not exist."); + } $this->method = $method; } @@ -50,12 +50,13 @@ public static function instance(object $listener): self return new self($listener::class); } - /** - * @param array{object|class-string, string} $listen - */ + /** @param array{object|class-string, string} $listen */ public static function method(array $listen): self { - is_callable($listen) or throw new InvalidArgumentException('The given listener method is not callable.'); + if (!is_callable($listen)) { + throw new InvalidArgumentException('The given listener method is not callable.'); + } + [$listener, $method] = $listen; if (is_object($listener)) { @@ -68,9 +69,11 @@ public static function method(array $listen): self #[Override] protected function matches(mixed $other): bool { - is_string($other) or throw new InvalidArgumentException( - self::class . ' can only be evaluated for strings, got ' . gettype($other) . '.', - ); + if (is_string($other) === false) { + throw new InvalidArgumentException( + self::class . ' can only be evaluated for strings, got ' . gettype($other) . '.', + ); + } try { Event::assertListening($other, match ($this->method) { diff --git a/src/Laravel/Constraint/Events/HasListenerTest.php b/src/Laravel/Constraint/Events/HasListenerTest.php index 2c4891f..a562828 100644 --- a/src/Laravel/Constraint/Events/HasListenerTest.php +++ b/src/Laravel/Constraint/Events/HasListenerTest.php @@ -36,7 +36,7 @@ public function itFailsToConstructWithNonListenerMethods(): void $method = 'thisIsNotTheMethodYouAreLookingFor'; $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("$method does not exist."); + $this->expectExceptionMessage("{$method} does not exist."); new HasListener(DummyListener::class, $method); } @@ -120,7 +120,7 @@ public function itCannotEvaluateValuesThatAreNotStrings(): void HasListener::class . ' can only be evaluated for strings', ); - $this->assertThat(new DummyEvent(), new HasListener(DummyListener::class)); + $this->assertThat(new DummyEvent(), new HasListener(DummyListener::class)); } #[Test] diff --git a/src/Laravel/Constraint/Events/RequiresEventFake.php b/src/Laravel/Constraint/Events/RequiresEventFake.php index afb4bfb..cbe6062 100644 --- a/src/Laravel/Constraint/Events/RequiresEventFake.php +++ b/src/Laravel/Constraint/Events/RequiresEventFake.php @@ -22,10 +22,13 @@ public static function spy(string ...$eventsToSpyOn): void private function resolveEventFake(): EventFake { - Event::isFake() or throw new LogicException( - 'To use the ' . self::class . ' constraint, make sure to call ' . self::class . '::spy() first.', - ); + if (Event::isFake() === false) { + throw new LogicException( + 'To use the ' . self::class . ' constraint, make sure to call ' . self::class . '::spy() first.', + ); + } + // @mago-expect analyzer:mixed-return-statement return Event::getFacadeRoot(); } } diff --git a/src/Laravel/Constraint/Events/WasDispatched.php b/src/Laravel/Constraint/Events/WasDispatched.php index a4d119c..eae4815 100644 --- a/src/Laravel/Constraint/Events/WasDispatched.php +++ b/src/Laravel/Constraint/Events/WasDispatched.php @@ -70,7 +70,7 @@ protected function matches(mixed $other): bool $dispatchedEventsMatchingConstraints = array_filter( $matchingDispatchedEvents, - fn (array $dispatchedEvent): bool => $this->matchesEventConstraints( + fn(array $dispatchedEvent): bool => $this->matchesEventConstraints( $other, $dispatchedEvent[0], // When the event was dispatched exactly once, we should add all nested expectation failures to the @@ -116,7 +116,7 @@ protected function failureDescription(mixed $other): string $message = parent::failureDescription($other); if ($this->times !== null) { - $message .= " $this->times time(s)"; + $message .= " {$this->times} time(s)"; } $message .= match (true) { diff --git a/src/Laravel/Constraint/Events/WasDispatchedTest.php b/src/Laravel/Constraint/Events/WasDispatchedTest.php index 8463c29..623b1a7 100644 --- a/src/Laravel/Constraint/Events/WasDispatchedTest.php +++ b/src/Laravel/Constraint/Events/WasDispatchedTest.php @@ -111,7 +111,7 @@ public function itPassesWhenDispatched(QuantableConstraint $quantise): void WasDispatched::spy(); $event = 'some.event'; - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); $this->assertThat($event, $quantise(new WasDispatched())); } @@ -127,7 +127,7 @@ public function itFailsWhenDispatchedButNotWithGivenEventConstraints(): void $this->expectExceptionMessage('event was dispatched with given event constraints.'); $this->assertThat($event, new WasDispatched()->withConstraints( - new Callback(fn () => false), + new Callback(static fn() => false), )); } @@ -140,7 +140,7 @@ public function itPassesWhenDispatchedWithGivenEventConstraints(): void Event::dispatch($event); $this->assertThat($event, new WasDispatched()->withConstraints( - new Callback(fn () => true), + new Callback(static fn() => true), )); } @@ -150,10 +150,10 @@ public function itFailsWhenDispatchedButNotGivenTimes(QuantableConstraint $quant { WasDispatched::spy(); $event = 'some.event'; - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("event was dispatched $quantise->expected time(s)."); + $this->expectExceptionMessage("event was dispatched {$quantise->expected} time(s)."); $this->assertThat($event, new WasDispatched()->times($quantise->expected)); } @@ -165,7 +165,7 @@ public function itPassesWhenDispatchedGivenTimes(QuantableConstraint $quantise): WasDispatched::spy(); $event = 'some.event'; - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); $this->assertThat($event, $quantise(new WasDispatched())); } @@ -177,14 +177,16 @@ public function itFailsWhenDispatchedWithGivenEventConstrainsButNotGivenTimes( ): void { WasDispatched::spy(); $event = new DummyEvent('first', 'last'); - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("event was dispatched $quantise->expected time(s)"); + $this->expectExceptionMessage("event was dispatched {$quantise->expected} time(s)"); - $this->assertThat($event, new WasDispatched()->times($quantise->expected)->withConstraints( - new Callback(fn () => true), - )); + $this->assertThat($event, new WasDispatched() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => true), + )); } #[Test] @@ -194,14 +196,16 @@ public function itFailsWhenDispatchedGivenTimesButNotWithGivenEventConstrains( ): void { WasDispatched::spy(); $event = new DummyEvent('first', 'last'); - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("event was dispatched $quantise->expected time(s) with given event constraints."); + $this->expectExceptionMessage("event was dispatched {$quantise->expected} time(s) with given event constraints."); - $this->assertThat($event, new WasDispatched()->times($quantise->expected)->withConstraints( - new Callback(fn () => false), - )); + $this->assertThat($event, new WasDispatched() + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => false), + )); } #[Test] @@ -212,11 +216,14 @@ public function itPassesWhenDispatchedGivenTimesWithGivenEventConstraints( WasDispatched::spy(); $event = new DummyEvent('first', 'last'); - $quantise->applyTo(fn () => Event::dispatch($event)); + $quantise->applyTo(static fn() => Event::dispatch($event)); - $this->assertThat($event, $quantise(new WasDispatched()->withConstraints( - new Callback(fn () => true), - ))); + $this->assertThat( + $event, + $quantise(new WasDispatched()->withConstraints( + new Callback(static fn() => true), + )), + ); } #[Test] diff --git a/src/Laravel/Constraint/Scheduling/IsScheduled.php b/src/Laravel/Constraint/Scheduling/IsScheduled.php index 8e3dd83..7b69b69 100644 --- a/src/Laravel/Constraint/Scheduling/IsScheduled.php +++ b/src/Laravel/Constraint/Scheduling/IsScheduled.php @@ -35,12 +35,13 @@ public function __construct( #[Override] protected function matches(mixed $other): bool { - is_string($other) && class_exists($other) or throw new InvalidArgumentException( - self::class . ' can only be evaluated for classnames of scheduled tasks.', - ); + if (is_string($other) === false || class_exists($other) === false) { + throw new InvalidArgumentException( + self::class . ' can only be evaluated for classnames of scheduled tasks.', + ); + } - $matchingScheduledTask = new Collection($this->schedule->events()) - ->first(fn (Event $event): bool => $event->description === $other); + $matchingScheduledTask = new Collection($this->schedule->events())->first(static fn(Event $event): bool => $event->description === $other); if ($matchingScheduledTask === null) { $this->additionalFailureDescriptions[] = 'Not scheduled.'; @@ -62,6 +63,6 @@ protected function matches(mixed $other): bool public function toString(): string { - return "is scheduled $this->frequency"; + return "is scheduled {$this->frequency}"; } } diff --git a/src/Laravel/Doubles/Events/DummyEvent.php b/src/Laravel/Doubles/Events/DummyEvent.php index 0bf2c0e..800e998 100644 --- a/src/Laravel/Doubles/Events/DummyEvent.php +++ b/src/Laravel/Doubles/Events/DummyEvent.php @@ -9,9 +9,7 @@ */ final readonly class DummyEvent { - /** - * @var array - */ + /** @var array */ public array $arguments; public function __construct(mixed ...$arguments) diff --git a/src/Laravel/Doubles/Events/DummyListener.php b/src/Laravel/Doubles/Events/DummyListener.php index 43491d6..ce29b11 100644 --- a/src/Laravel/Doubles/Events/DummyListener.php +++ b/src/Laravel/Doubles/Events/DummyListener.php @@ -11,11 +11,9 @@ { public function __invoke(mixed ...$arguments): void { - // } public function handle(): void { - // } } diff --git a/src/Laravel/ServiceProvider.php b/src/Laravel/ServiceProvider.php deleted file mode 100644 index 487838d..0000000 --- a/src/Laravel/ServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ - $response->json()); - } -} diff --git a/src/Laravel/ServiceProviderTest.php b/src/Laravel/ServiceProviderTest.php deleted file mode 100644 index c425c82..0000000 --- a/src/Laravel/ServiceProviderTest.php +++ /dev/null @@ -1,45 +0,0 @@ - [ - [ - 'path' => [$path], - 'extensions' => [ - 'category' => $category, - ], - ], - ], - ]))); - - $this->assertThat($response, new HasErrorOnPath($path, $category)); - } -} diff --git a/src/PHPUnit/Constraint/Callables/WasCalled.php b/src/PHPUnit/Constraint/Callables/WasCalled.php index 396d82b..37751de 100644 --- a/src/PHPUnit/Constraint/Callables/WasCalled.php +++ b/src/PHPUnit/Constraint/Callables/WasCalled.php @@ -29,7 +29,7 @@ public function __construct( public function withSame(mixed ...$expected): self { - return new self(function (mixed ...$actual) use ($expected): void { + return new self(static function (mixed ...$actual) use ($expected): void { Assert::assertCount(count($expected), $actual); foreach ($actual as $key => $value) { @@ -56,9 +56,11 @@ public function once(): self #[Override] protected function matches(mixed $other): bool { - $other instanceof SpyCallable or throw new InvalidArgumentException( - self::class . ' can only be evaluated for instances of ' . SpyCallable::class, - ); + if (!$other instanceof SpyCallable) { + throw new InvalidArgumentException( + self::class . ' can only be evaluated for instances of ' . SpyCallable::class, + ); + } $matchingInvocations = array_filter($other->invocations, $this->matchesInvocationAssertions(...)); @@ -71,6 +73,7 @@ protected function matches(mixed $other): bool private function matchesInvocationAssertions(CallableInvocation $invocation): bool { try { + // @mago-expect analyzer:invalid-method-access $this->assertInvocation?->__invoke(...$invocation->arguments); } catch (ExpectationFailedException $expectationFailed) { $this->additionalFailureDescriptions[] = $expectationFailed->getMessage(); @@ -86,7 +89,7 @@ public function toString(): string $message = 'was called'; if ($this->times !== null) { - $message .= " $this->times time(s)"; + $message .= " {$this->times} time(s)"; } if ($this->assertInvocation !== null) { diff --git a/src/PHPUnit/Constraint/Callables/WasCalledTest.php b/src/PHPUnit/Constraint/Callables/WasCalledTest.php index 9f76843..c31a1c9 100644 --- a/src/PHPUnit/Constraint/Callables/WasCalledTest.php +++ b/src/PHPUnit/Constraint/Callables/WasCalledTest.php @@ -29,7 +29,7 @@ public function itCanConstructWithoutArguments(): void #[Test] public function itCanConstructWithInvocationAssertions(): void { - $assertInvocation = function (): void {}; + $assertInvocation = static function (): void {}; $instance = new WasCalled($assertInvocation); @@ -41,7 +41,7 @@ public function itCanConstructWithInvocationAssertions(): void #[DataProviderExternal(QuantableConstraint::class, 'cases')] public function itImplementsTheQuantableInterface(QuantableConstraint $quantise): void { - $assertInvocation = function (): void {}; + $assertInvocation = static function (): void {}; $instance = new WasCalled($assertInvocation); $quantisedInstance = $quantise($instance); @@ -57,7 +57,7 @@ public function itFailsWhenEvaluatingForNonSpyCallableInstances(): void { $this->expectException(InvalidArgumentException::class); - $this->assertThat(function (): void {}, new WasCalled()); + $this->assertThat(static function (): void {}, new WasCalled()); } #[Test] @@ -116,7 +116,7 @@ public function itFailsWhenNotCalledExpectedTimes(QuantableConstraint $quantise) $quantise->applyTo($callable(...)); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was called $quantise->expected time(s)."); + $this->expectExceptionMessage("was called {$quantise->expected} time(s)."); $this->assertThat($callable, new WasCalled()->times($quantise->expected)); } @@ -137,15 +137,18 @@ public function itPassesWhenCalledExpectedTimes(QuantableConstraint $quantise): public function itFailsWhenNotCalledExpectedTimesWithExpectedArguments(QuantableConstraint $quantise): void { $callable = new SpyCallable(); - $quantise->applyTo(fn () => $callable('first', 'last')); + $quantise->applyTo(static fn() => $callable('first', 'last')); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was called $quantise->expected time(s) with given invocation assertions."); - - $this->assertThat($callable, new WasCalled(function (string $first, string $last): void { - $this->assertSame('first', $first); - $this->assertSame('last', $last); - })->times($quantise->expected)); + $this->expectExceptionMessage("was called {$quantise->expected} time(s) with given invocation assertions."); + + $this->assertThat( + $callable, + new WasCalled(function (string $first, string $last): void { + $this->assertSame('first', $first); + $this->assertSame('last', $last); + })->times($quantise->expected), + ); } #[Test] @@ -154,7 +157,7 @@ public function itPassesWhenCalledExpectedTimesWithExpectedArguments(QuantableCo { $callable = new SpyCallable(); - $quantise->applyTo(fn () => $callable('first', 'last')); + $quantise->applyTo(static fn() => $callable('first', 'last')); $this->assertThat($callable, $quantise(new WasCalled(function (string $first, string $last): void { $this->assertSame('first', $first); diff --git a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php index e2d6dbb..37d8eb7 100644 --- a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php +++ b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php @@ -23,14 +23,14 @@ public function __construct( public static function failingConstraints(): self { return new self([ - new Callback(fn () => false), + new Callback(static fn() => false), ]); } public static function passingConstraints(): self { return new self([ - new Callback(fn () => true), + new Callback(static fn() => true), ]); } diff --git a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflection.php b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflection.php index 30cbed2..5e09add 100644 --- a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflection.php +++ b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflection.php @@ -18,8 +18,6 @@ */ public function __invoke(object $object): array { - return array_map(function (ReflectionProperty $property) use ($object): Constraint { - return new PropertyValue($property->name, new IsEqual($property->getValue($object))); - }, new ReflectionClass($object)->getProperties(ReflectionProperty::IS_PUBLIC)); + return array_map(static fn(ReflectionProperty $property): PropertyValue => new PropertyValue($property->name, new IsEqual($property->getValue($object))), new ReflectionClass($object)->getProperties(ReflectionProperty::IS_PUBLIC)); } } diff --git a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflectionTest.php b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflectionTest.php index 5da7b55..b10a5f7 100644 --- a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflectionTest.php +++ b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingReflectionTest.php @@ -16,8 +16,7 @@ final class DeriveConstraintsFromObjectUsingReflectionTest extends TestCase #[Test] public function itShouldNotDeriveConstraintsFromNonPublicProperties(): void { - $object = new readonly class - { + $object = new readonly class { public function __construct( protected mixed $protected = 'protected', protected mixed $private = 'private', @@ -33,8 +32,7 @@ public function __construct( public function itCanDeriveConstraintsFromPublicProperties(): void { $value = 'SomePublicValue'; - $object = new readonly class ($value) - { + $object = new readonly class($value) { public function __construct( public mixed $somePublicProperty, ) {} @@ -42,8 +40,11 @@ public function __construct( $results = new DeriveConstraintsFromObjectUsingReflection()->__invoke($object); - $this->assertEquals([ - new PropertyValue('somePublicProperty', new IsEqual($value)), - ], $results); + $this->assertEquals( + [ + new PropertyValue('somePublicProperty', new IsEqual($value)), + ], + $results, + ); } } diff --git a/src/PHPUnit/Constraint/Objects/DerivesConstraintsFromObjects.php b/src/PHPUnit/Constraint/Objects/DerivesConstraintsFromObjects.php index 3254e37..4335462 100644 --- a/src/PHPUnit/Constraint/Objects/DerivesConstraintsFromObjects.php +++ b/src/PHPUnit/Constraint/Objects/DerivesConstraintsFromObjects.php @@ -6,21 +6,18 @@ use PHPUnit\Framework\Constraint\Constraint; +use function is_object; use function is_string; trait DerivesConstraintsFromObjects { private static ?DeriveConstraintsFromObject $deriveConstraintsFromObject = null; - /** - * @var array - */ + /** @var array */ public readonly array $objectConstraints; - /** - * @return array - */ - public function givenOrDerivedObjectConstraints(string|object $expected): array + /** @return array */ + public function givenOrDerivedObjectConstraints(mixed $expected): array { if ($this->objectConstraints !== []) { return $this->objectConstraints; @@ -30,7 +27,11 @@ public function givenOrDerivedObjectConstraints(string|object $expected): array return []; } - return (self::$deriveConstraintsFromObject ?: new DeriveConstraintsFromObjectUsingReflection())($expected); + if (is_object($expected)) { + return ( self::$deriveConstraintsFromObject ?? new DeriveConstraintsFromObjectUsingReflection() )($expected); + } + + return []; } public static function deriveConstraintsFromObjectUsing( diff --git a/src/PHPUnit/Constraint/Objects/PropertyValue.php b/src/PHPUnit/Constraint/Objects/PropertyValue.php index d560fdc..dee70f5 100644 --- a/src/PHPUnit/Constraint/Objects/PropertyValue.php +++ b/src/PHPUnit/Constraint/Objects/PropertyValue.php @@ -45,10 +45,8 @@ protected function matches(mixed $other): bool public function toString(): string { - $comparesTo = Str::of($this->constraint::class) - ->classBasename() - ->snake(' '); + $comparesTo = Str::of($this->constraint::class)->classBasename()->snake(' '); - return "`$this->name` property value $comparesTo"; + return "`{$this->name}` property value {$comparesTo}"; } } diff --git a/src/PHPUnit/Constraint/Objects/PropertyValueTest.php b/src/PHPUnit/Constraint/Objects/PropertyValueTest.php index 922df35..06b5799 100644 --- a/src/PHPUnit/Constraint/Objects/PropertyValueTest.php +++ b/src/PHPUnit/Constraint/Objects/PropertyValueTest.php @@ -60,7 +60,7 @@ public function itFailsWhenPropertiesDontExist(): void #[Test] public function itFailsWhenPropertiesDontMatchGivenConstraints(): void { - $constraint = new Callback(fn (): bool => false); + $constraint = new Callback(static fn(): bool => false); $object = $this->objectWithIdProperty(); $this->expectException(ExpectationFailedException::class); @@ -71,7 +71,7 @@ public function itFailsWhenPropertiesDontMatchGivenConstraints(): void #[Test] public function itPassesWhenPropertiesMatchGivenConstraints(): void { - $constraint = new Callback(fn (): bool => true); + $constraint = new Callback(static fn(): bool => true); $object = $this->objectWithIdProperty(); $this->assertThat($object, new PropertyValue('id', $constraint)); @@ -82,8 +82,7 @@ public function itPassesWhenPropertiesMatchGivenConstraints(): void */ private function objectWithIdProperty(): object { - return new class - { + return new class { public function __construct( public string $id = 'SomeId', ) {} diff --git a/src/PHPUnit/Constraint/ProvidesAdditionalFailureDescription.php b/src/PHPUnit/Constraint/ProvidesAdditionalFailureDescription.php index b321bb8..3efe079 100644 --- a/src/PHPUnit/Constraint/ProvidesAdditionalFailureDescription.php +++ b/src/PHPUnit/Constraint/ProvidesAdditionalFailureDescription.php @@ -9,16 +9,14 @@ trait ProvidesAdditionalFailureDescription { - /** - * @var array - */ + /** @var array */ private array $additionalFailureDescriptions = []; #[Override] protected function additionalFailureDescription(mixed $other): string { return new Collection($this->additionalFailureDescriptions) - ->map(fn (string $description): string => "\n* $description\n") + ->map(static fn(string $description): string => "\n* {$description}\n") ->implode(''); } } diff --git a/src/PHPUnit/Constraint/Quantable.php b/src/PHPUnit/Constraint/Quantable.php index 6732285..d6b9ae0 100644 --- a/src/PHPUnit/Constraint/Quantable.php +++ b/src/PHPUnit/Constraint/Quantable.php @@ -7,6 +7,8 @@ interface Quantable { public function times(int $count): self; + public function never(): self; + public function once(): self; } diff --git a/src/PHPUnit/Constraint/Spy.php b/src/PHPUnit/Constraint/Spy.php index a82a42c..e7e9378 100644 --- a/src/PHPUnit/Constraint/Spy.php +++ b/src/PHPUnit/Constraint/Spy.php @@ -10,6 +10,7 @@ final class Spy extends Constraint { private function __construct( + /** @var SpyCallable */ public readonly SpyCallable $matches, ) {} diff --git a/src/PHPUnit/DataProviders/EnumCase.php b/src/PHPUnit/DataProviders/EnumCase.php index a5f5f34..df2f41b 100644 --- a/src/PHPUnit/DataProviders/EnumCase.php +++ b/src/PHPUnit/DataProviders/EnumCase.php @@ -4,6 +4,7 @@ namespace Craftzing\TestBench\PHPUnit\DataProviders; +use InvalidArgumentException; use LogicException; use ReflectionEnum; use ReflectionEnumUnitCase; @@ -16,14 +17,10 @@ use function count; use function in_array; -/** - * @template TValue of UnitEnum - */ +/** @template TValue of UnitEnum */ final readonly class EnumCase { - /** - * @var array - */ + /** @var array */ private array $options; /** @@ -34,39 +31,49 @@ public function __construct( public UnitEnum $instance, UnitEnum ...$options, ) { - in_array($instance, $options, true) or throw new ValueError('Options should contain the given instance.'); + if (in_array($instance, $options, strict: true) === false) { + throw new ValueError('Options should contain the given instance.'); + } foreach ($options as $option) { - $option::class === $instance::class or throw new ValueError( - 'Given options should have the same type as the given instance.', - ); + if ($option::class !== $instance::class) { + throw new ValueError( + 'Given options should have the same type as the given instance.', + ); + } } $this->options = $options; } - /** - * @return TValue - */ + /** @return TValue */ public function differentInstance(): UnitEnum { - count($this->options) > 1 or throw new LogicException( - self::class . ' was configured with a single option and can therefore not return a different instance.', - ); - $differentOptions = array_filter($this->options, fn (UnitEnum $option): bool => $option !== $this->instance); + if (count($this->options) <= 1) { + throw new LogicException( + self::class . ' was configured with a single option and can therefore not return a different instance.', + ); + } + + $differentOptions = array_filter($this->options, fn(UnitEnum $option): bool => $option !== $this->instance); return $differentOptions[array_rand($differentOptions)]; } /** * @param class-string $enumFQCN - * @return iterable}> + * @return iterable>> */ public static function cases(string $enumFQCN): iterable { + if (!enum_exists($enumFQCN)) { + throw new InvalidArgumentException("Expected a concrete Enum class string, got: {$enumFQCN}"); + } + foreach (new ReflectionEnum($enumFQCN)->getCases() as $case) { - // @phpstan-ignore generator.valueType - yield "$enumFQCN::$case->name" => [ + // @mago-expect analyzer:invalid-yield-value-type + yield "{$enumFQCN}::{$case->name}" => [ + // @mago-expect analyzer:possibly-static-access-on-interface new self($case->getValue(), ...$enumFQCN::cases()), ]; } @@ -74,25 +81,26 @@ public static function cases(string $enumFQCN): iterable /** * @param TValue ...$options - * @return iterable}> + * @return iterable>> */ public static function options(UnitEnum ...$options): iterable { foreach ($options as $case) { - yield "$case->name" => [new self($case, ...$options)]; + yield $case->name => [new self($case, ...$options)]; } } /** * @param class-string $enumFQCN - * @return iterable}> + * @return iterable>> */ public static function except(string $enumFQCN, UnitEnum ...$except): iterable { - $options = array_map(function (ReflectionEnumUnitCase $reflection) use ($except): ?UnitEnum { + /** @var TValue[] $options */ + $options = array_map(static function (ReflectionEnumUnitCase $reflection) use ($except): ?UnitEnum { $case = $reflection->getValue(); - return match (in_array($case, $except, true)) { + return match (in_array($case, $except, strict: true)) { true => null, false => $case, }; diff --git a/src/PHPUnit/DataProviders/EnumCaseTest.php b/src/PHPUnit/DataProviders/EnumCaseTest.php index e2d22bb..61465fc 100644 --- a/src/PHPUnit/DataProviders/EnumCaseTest.php +++ b/src/PHPUnit/DataProviders/EnumCaseTest.php @@ -10,11 +10,11 @@ use Faker\Factory; use Faker\Generator; use Illuminate\Support\Arr; +use InvalidArgumentException; use LogicException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use ReflectionException; use stdClass; use UnitEnum as UnitEnumInterface; use ValueError; @@ -68,7 +68,7 @@ public function itCannotConstructWhenOptionsHaveDifferentTypeComparedToGivenInst $instance = $this->faker->randomElement($options); $differentEnumFQCN = $this->faker->randomElement(array_filter( self::ENUM_FQCNS, - fn (string $enumFQCN): bool => $enumFQCN !== $instance::class, + static fn(string $enumFQCN): bool => $enumFQCN !== $instance::class, )); $differentEnumInstance = $this->faker->randomElement($differentEnumFQCN::cases()); @@ -130,7 +130,7 @@ public function itCannotReturnDifferentInstancesWithASingleOption(string $enumFQ #[Test] public function itCannotProvideFromNonEnumFQCNs(): void { - $this->expectException(ReflectionException::class); + $this->expectException(InvalidArgumentException::class); iterator_to_array(EnumCase::cases(stdClass::class)); } diff --git a/src/PHPUnit/DataProviders/QuantableConstraint.php b/src/PHPUnit/DataProviders/QuantableConstraint.php index 7f9fd21..fc6e67b 100644 --- a/src/PHPUnit/DataProviders/QuantableConstraint.php +++ b/src/PHPUnit/DataProviders/QuantableConstraint.php @@ -7,6 +7,7 @@ use Craftzing\TestBench\PHPUnit\Constraint\Quantable; use Illuminate\Support\Collection; +use function method_exists; use function random_int; final readonly class QuantableConstraint @@ -31,6 +32,11 @@ public function __construct( */ public function __invoke(Quantable $constraint): Quantable { + if (!method_exists($constraint, $this->method)) { + throw new \InvalidArgumentException(Quantable::class . "::{$this->method}() does not exist."); + } + + // @mago-expect analyzer:mixed-return-statement,string-member-selector return $constraint->{$this->method}($this->times); } @@ -39,27 +45,21 @@ public function applyTo(callable $callback): void Collection::times($this->times, $callback(...)); } - /** - * @return iterable - */ + /** @return iterable> */ public static function cases(): iterable { yield 'Never' => [new self('never', 0)]; yield from self::atLeastOnce(); } - /** - * @return iterable - */ + /** @return iterable> */ public static function atLeastOnce(): iterable { - yield 'Multiple times' => [new self('times', random_int(2, 10))]; + yield 'Multiple times' => [new self('times', random_int(2, max: 10))]; yield 'Once' => [new self('once', 1)]; } - /** - * @return iterable - */ + /** @return iterable> */ public static function tooFewOrTooManyTimes(): iterable { yield 'Too few times' => [new self('times', 2, 1)]; diff --git a/src/PHPUnit/DataProviders/QuantableConstraintTest.php b/src/PHPUnit/DataProviders/QuantableConstraintTest.php index 1ddcf14..4b78a46 100644 --- a/src/PHPUnit/DataProviders/QuantableConstraintTest.php +++ b/src/PHPUnit/DataProviders/QuantableConstraintTest.php @@ -63,10 +63,11 @@ public function itCanBeInvokedOnConstraints(string $method, int $times): void $instance($constraint); - $constraint->spy->assert(new WasCalled(function (string $method, int $times) use ($instance): void { - $this->assertSame($instance->method, $method); - $this->assertSame($instance->times, $times); - })->once()); + $constraint->spy->assert( + new WasCalled() + ->withSame($instance->method, $instance->times) + ->once(), + ); } #[Test] @@ -110,8 +111,7 @@ public function itCanConstructTooFewOrTooManyTimesCases(): void private function constraint(): object { - return new class implements Quantable - { + return new class implements Quantable { public function __construct( public SpyCallable $spy = new SpyCallable(), ) {} diff --git a/src/PHPUnit/Doubles/CallableInvocation.php b/src/PHPUnit/Doubles/CallableInvocation.php index e371aef..7b965f2 100644 --- a/src/PHPUnit/Doubles/CallableInvocation.php +++ b/src/PHPUnit/Doubles/CallableInvocation.php @@ -6,9 +6,7 @@ final readonly class CallableInvocation { - /** - * @var array - */ + /** @var array */ public array $arguments; public function __construct(mixed ...$arguments) diff --git a/src/PHPUnit/Doubles/SpyCallable.php b/src/PHPUnit/Doubles/SpyCallable.php index f170260..a67af29 100644 --- a/src/PHPUnit/Doubles/SpyCallable.php +++ b/src/PHPUnit/Doubles/SpyCallable.php @@ -7,28 +7,28 @@ use Craftzing\TestBench\PHPUnit\AssertsConstraints; use function call_user_func_array; -use function func_get_args; use function is_callable; +/** @template TReturn */ final class SpyCallable { use AssertsConstraints; - /** - * @var array - */ - public private(set) array $invocations = []; + /** @var array */ + private(set) array $invocations = []; public function __construct( + /** @var TReturn */ public readonly mixed $return = null, ) {} - public function __invoke(): mixed + /** @return TReturn */ + public function __invoke(mixed ...$arguments): mixed { - $arguments = func_get_args(); $this->invocations[] = new CallableInvocation(...$arguments); if (is_callable($this->return)) { + // @mago-expect analyzer:mixed-return-statement return call_user_func_array($this->return, $arguments); } diff --git a/src/PHPUnit/Doubles/SpyCallableTest.php b/src/PHPUnit/Doubles/SpyCallableTest.php index 22ec0aa..ab681bd 100644 --- a/src/PHPUnit/Doubles/SpyCallableTest.php +++ b/src/PHPUnit/Doubles/SpyCallableTest.php @@ -35,7 +35,7 @@ public static function returnValues(): iterable yield 'Array' => [['foo']]; yield 'Zero' => [0]; yield 'Integer' => [PHP_INT_MAX]; - yield 'Callable' => [fn (): null => null]; + yield 'Callable' => [static fn(): null => null]; yield 'Class' => [new stdClass()]; } @@ -66,8 +66,7 @@ public function itReturnsGivenReturnValuesWhenCalled(SpyCallable $instance, mixe #[Test] public function itCanAssertConstraints(): void { - $constraint = new class extends Constraint - { + $constraint = new class extends Constraint { public function toString(): string { return 'was constrained'; diff --git a/src/Saloon/Constraints/WasSent.php b/src/Saloon/Constraints/WasSent.php index 20ae6e4..d13af45 100644 --- a/src/Saloon/Constraints/WasSent.php +++ b/src/Saloon/Constraints/WasSent.php @@ -35,9 +35,10 @@ public function __construct( public readonly ?int $times = null, Constraint ...$constraints, ) { - $this->client = $connector->getMockClient() ?: MockClient::getGlobal() ?: throw new LogicException( - 'Missing either a global or connector specific ' . MockClient::class . '.', - ); + $this->client = + $connector->getMockClient() ?? MockClient::getGlobal() ?? throw new LogicException( + 'Missing either a global or connector specific ' . MockClient::class . '.', + ); $this->objectConstraints = $constraints; } @@ -72,21 +73,22 @@ protected function matches(mixed $other): bool ), }; - $matchingSentRequests = array_reduce($this->client->getRecordedResponses(), function ( - array $matchingSentRequests, - Response $response, - ) use ($requestName): array { - $request = $response->getPendingRequest()->getRequest(); + $matchingSentRequests = array_reduce( + $this->client->getRecordedResponses(), + static function (array $matchingSentRequests, Response $response) use ($requestName): array { + $request = $response->getPendingRequest()->getRequest(); - if ($request::class === $requestName) { - $matchingSentRequests[] = $request; - } + if ($request::class === $requestName) { + $matchingSentRequests[] = $request; + } - return $matchingSentRequests; - }, []); + return $matchingSentRequests; + }, + [], + ); $sentRequestsMatchingConstraints = array_filter( $matchingSentRequests, - fn (Request $request): bool => $this->matchesRequestConstraints( + fn(Request $request): bool => $this->matchesRequestConstraints( $other, $request, // When the request was sent exactly once, we should add all nested expectation failures to the @@ -132,7 +134,7 @@ protected function failureDescription(mixed $other): string $message = parent::failureDescription($other); if ($this->times !== null) { - $message .= " $this->times time(s)"; + $message .= " {$this->times} time(s)"; } $message .= match (true) { diff --git a/src/Saloon/Constraints/WasSentTest.php b/src/Saloon/Constraints/WasSentTest.php index 7275cff..c30b486 100644 --- a/src/Saloon/Constraints/WasSentTest.php +++ b/src/Saloon/Constraints/WasSentTest.php @@ -133,7 +133,7 @@ public function itPassesWhenSent(QuantableConstraint $quantise): void { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); $this->assertThat(FakeRequest::class, $quantise(new WasSent($connector))); } @@ -162,7 +162,7 @@ public function itFailsWhenSentButNotWithGivenConstraints(): void $this->expectExceptionMessage('was sent with given constraints.'); $this->assertThat(FakeRequest::class, new WasSent($connector)->withConstraints( - new Callback(fn () => false), + new Callback(static fn() => false), )); } @@ -174,7 +174,7 @@ public function itPassesWhenSentWithGivenConstraints(): void $connector->send(new FakeRequest()); $this->assertThat(FakeRequest::class, new WasSent($connector)->withConstraints( - new Callback(fn () => true), + new Callback(static fn() => true), )); } @@ -183,10 +183,10 @@ public function itPassesWhenSentWithGivenConstraints(): void public function itFailsWhenSentButNotGivenTimes(QuantableConstraint $quantise): void { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was sent $quantise->expected time(s)."); + $this->expectExceptionMessage("was sent {$quantise->expected} time(s)."); $this->assertThat(FakeRequest::class, new WasSent($connector)->times($quantise->expected)); } @@ -197,7 +197,7 @@ public function itPassesWhenSentGivenTimes(QuantableConstraint $quantise): void { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); $this->assertThat(FakeRequest::class, $quantise(new WasSent($connector))); } @@ -207,14 +207,16 @@ public function itPassesWhenSentGivenTimes(QuantableConstraint $quantise): void public function itFailsWhenSentWithGivenConstrainsButNotGivenTimes(QuantableConstraint $quantise): void { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was sent $quantise->expected time(s)"); + $this->expectExceptionMessage("was sent {$quantise->expected} time(s)"); - $this->assertThat(FakeRequest::class, new WasSent($connector)->times($quantise->expected)->withConstraints( - new Callback(fn () => true), - )); + $this->assertThat(FakeRequest::class, new WasSent($connector) + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => true), + )); } #[Test] @@ -222,14 +224,16 @@ public function itFailsWhenSentWithGivenConstrainsButNotGivenTimes(QuantableCons public function itFailsWhenSentGivenTimesButNotWithGivenConstrains(QuantableConstraint $quantise): void { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was sent $quantise->expected time(s) with given constraints."); + $this->expectExceptionMessage("was sent {$quantise->expected} time(s) with given constraints."); - $this->assertThat(FakeRequest::class, new WasSent($connector)->times($quantise->expected)->withConstraints( - new Callback(fn () => false), - )); + $this->assertThat(FakeRequest::class, new WasSent($connector) + ->times($quantise->expected) + ->withConstraints( + new Callback(static fn() => false), + )); } #[Test] @@ -238,11 +242,14 @@ public function itPassesWhenSentGivenTimesWithGivenConstraints(QuantableConstrai { $connector = new FakeConnector()->withMockClient($this->mockClient(FakeRequest::class)); - $quantise->applyTo(fn () => $connector->send(new FakeRequest())); + $quantise->applyTo(static fn() => $connector->send(new FakeRequest())); - $this->assertThat(FakeRequest::class, $quantise(new WasSent($connector)->withConstraints( - new Callback(fn () => true), - ))); + $this->assertThat( + FakeRequest::class, + $quantise(new WasSent($connector)->withConstraints( + new Callback(static fn() => true), + )), + ); } #[Test] diff --git a/src/Saloon/DataProviders/FakeResponse.php b/src/Saloon/DataProviders/FakeResponse.php index 400e0ef..4f8c902 100644 --- a/src/Saloon/DataProviders/FakeResponse.php +++ b/src/Saloon/DataProviders/FakeResponse.php @@ -67,16 +67,15 @@ public function __invoke(string $requestFQCN, ?Connector $connector = null): voi $client = match ($connector) { // When not given a connector instance, we should use the global // MockClient to fake responses for all connectors... - null => MockClient::getGlobal() ?: MockClient::global(), - + null => MockClient::getGlobal() ?? MockClient::global(), // When given a connector instance, we should use its specific MockClient // to only fake responses for that specific connector instance... - default => $connector->getMockClient() ?: new MockClient(), + default => $connector->getMockClient() ?? new MockClient(), }; $client->addResponses([ $requestFQCN => $this->response, - '*' => function (PendingRequest $pendingRequest): void { + '*' => static function (PendingRequest $pendingRequest): void { throw new LogicException("Missing response mock for {$pendingRequest->getUrl()}."); }, ]); diff --git a/src/Saloon/DataProviders/FakeResponseTest.php b/src/Saloon/DataProviders/FakeResponseTest.php index 3c04a1e..53441c2 100644 --- a/src/Saloon/DataProviders/FakeResponseTest.php +++ b/src/Saloon/DataProviders/FakeResponseTest.php @@ -89,8 +89,7 @@ public function itShouldFailRequestsWithoutResponseMock(?Connector $connector): $this->expectException(LogicException::class); - new FakeConnector()->send(new class extends Request - { + new FakeConnector()->send(new class extends Request { public function resolveEndpoint(): string { return 'not-faked'; @@ -103,19 +102,25 @@ public function itCanProvideCommonErrors(): void { $cases = iterator_to_array(FakeResponse::commonErrors()); - $this->assertEquals([ - 'Bad request' => [FakeResponse::badRequest()], - 'Forbidden' => [FakeResponse::forbidden()], - 'Not found' => [FakeResponse::notFound()], - 'Server error' => [FakeResponse::serverError()], - ], $cases); + $this->assertEquals( + [ + 'Bad request' => [FakeResponse::badRequest()], + 'Forbidden' => [FakeResponse::forbidden()], + 'Not found' => [FakeResponse::notFound()], + 'Server error' => [FakeResponse::serverError()], + ], + $cases, + ); } private function assertBody(string|array $body, SaloonResponse $response): void { - $this->assertSame(match (is_array($body)) { - true => json_encode($body), - false => $body, - }, $response->body()); + $this->assertSame( + match (is_array($body)) { + true => json_encode($body), + false => $body, + }, + $response->body(), + ); } }