diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 375e7b9daa..cd0653d998 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -44,7 +44,7 @@ jobs: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: composer-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composercache.outputs.dir }} key: composer-${{ hashFiles('**/composer.json') }}-${{ matrix.install-args }} @@ -61,7 +61,7 @@ jobs: run: "vendor/bin/phpunit" - name: phpstan-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: phpstan-${{ matrix.php-version }}-${{ matrix.install-args }}-${{ github.ref }}-${{ github.sha }} path: .phpstan-cache @@ -79,13 +79,13 @@ jobs: run: composer cs-check - name: "Archive code coverage results" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: codeCoverage-${{ matrix.php-version }}-${{ github.run_id }} path: "build" overwrite: true - - uses: codecov/codecov-action@v5.5.0 # upload the coverage to codecov + - uses: codecov/codecov-action@v5.5.1 # upload the coverage to codecov with: fail_ci_if_error: false # optional (default = false) - Need CODECOV_TOKEN # Do not upload in forks, and only on php8.4, latest deps diff --git a/.github/workflows/doc_generation.yml b/.github/workflows/doc_generation.yml index 97caa41546..f96e79e728 100644 --- a/.github/workflows/doc_generation.yml +++ b/.github/workflows/doc_generation.yml @@ -22,7 +22,7 @@ jobs: uses: "actions/checkout@v5" - name: "Setup NodeJS" - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '20.x' @@ -36,7 +36,7 @@ jobs: - name: "Deploy website" if: "${{ github.event_name == 'push' || github.event_name == 'release' }}" - uses: JamesIves/github-pages-deploy-action@v4.7.3 + uses: JamesIves/github-pages-deploy-action@v4.7.6 with: token: ${{ secrets.GITHUB_TOKEN }} branch: gh-pages # The branch the action should deploy to. diff --git a/composer.json b/composer.json index 520f02bbc5..00ccdb862c 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ "psr/http-server-handler": "^1", "psr/http-server-middleware": "^1", "psr/simple-cache": "^1.0.1 || ^2 || ^3", - "symfony/cache": "^4.3 || ^5 || ^6 || ^7", - "symfony/expression-language": "^4 || ^5 || ^6 || ^7", + "symfony/cache": "^4.3 || ^5 || ^6 || ^7 || ^8", + "symfony/expression-language": "^4 || ^5 || ^6 || ^7 || ^8", "webonyx/graphql-php": "^v15.0", "kcs/class-finder": "^0.6.0" }, @@ -35,7 +35,7 @@ "php-coveralls/php-coveralls": "^2.7", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5 || ^11.0", + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0", "symfony/var-dumper": "^6.4" }, "suggest": { diff --git a/src/Cache/FilesSnapshot.php b/src/Cache/FilesSnapshot.php index 7ac70861ac..afed10f0c3 100644 --- a/src/Cache/FilesSnapshot.php +++ b/src/Cache/FilesSnapshot.php @@ -7,6 +7,7 @@ use ReflectionClass; use function array_unique; +use function is_file; use function Safe\filemtime; class FilesSnapshot @@ -24,6 +25,10 @@ public static function for(array $files): self $dependencies = []; foreach (array_unique($files) as $file) { + if (! is_file($file)) { + continue; + } + $dependencies[$file] = filemtime($file); } diff --git a/src/ResolveUtils.php b/src/ResolveUtils.php index b3aa657668..2f95d35f32 100644 --- a/src/ResolveUtils.php +++ b/src/ResolveUtils.php @@ -24,7 +24,7 @@ final class ResolveUtils public static function assertInnerReturnType(mixed $result, Type $type): void { if ($type instanceof NonNull && $result === null) { - throw TypeMismatchRuntimeException::unexpectedNullValue(); + throw TypeMismatchRuntimeException::unexpectedNullValue($type); } if ($result === null) { return; @@ -56,7 +56,7 @@ public static function assertInnerReturnType(mixed $result, Type $type): void public static function assertInnerInputType(mixed $input, Type $type): void { if ($type instanceof NonNull && $input === null) { - throw TypeMismatchRuntimeException::unexpectedNullValue(); + throw TypeMismatchRuntimeException::unexpectedNullValue($type); } if ($input === null) { return; diff --git a/src/TypeMismatchRuntimeException.php b/src/TypeMismatchRuntimeException.php index c73e323dac..d121bb4809 100644 --- a/src/TypeMismatchRuntimeException.php +++ b/src/TypeMismatchRuntimeException.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite; +use GraphQL\Type\Definition\Type; + use function gettype; /** @@ -11,9 +13,15 @@ */ class TypeMismatchRuntimeException extends GraphQLRuntimeException { - public static function unexpectedNullValue(): self + public static function unexpectedNullValue(Type|null $expectedType = null): self { - return new self('Unexpected null value for non nullable field.'); + $expectedMessageTail = ''; + if ($expectedType !== null) { + $expectedMessageTail = ' Expected: "' . $expectedType->toString() . '"'; + // ToDo: support for NULL $expectedType should be dropped in the next major + } + + return new self('Unexpected null value for non nullable field.' . $expectedMessageTail); } public static function expectedIterable(mixed $result): self diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php index 1347d1151c..2d0b4bd76d 100644 --- a/src/Utils/PropertyAccessor.php +++ b/src/Utils/PropertyAccessor.php @@ -94,6 +94,7 @@ public static function setValue(object $instance, string $propertyName, mixed $v throw AccessPropertyException::createForUnwritableProperty($class, $propertyName); } + /** @param class-string $class */ private static function isPublicProperty(string $class, string $propertyName): bool { if (! property_exists($class, $propertyName)) { diff --git a/tests/Cache/FilesSnapshotTest.php b/tests/Cache/FilesSnapshotTest.php index f45aa2c75e..a82baeb95a 100644 --- a/tests/Cache/FilesSnapshotTest.php +++ b/tests/Cache/FilesSnapshotTest.php @@ -69,9 +69,37 @@ public function testTracksChangesInFile(): void self::assertTrue($snapshot->changed()); } + public function testIgnoresNonExistentFiles(): void + { + $nonExistentFile = '/path/to/non/existent/file.php'; + + $snapshot = FilesSnapshot::for([$nonExistentFile]); + + self::assertFalse($snapshot->changed()); + } + + public function testIgnoresNonExistentFilesInMixedList(): void + { + $existingFile = (new \ReflectionClass(FooType::class))->getFileName(); + $nonExistentFile1 = '/path/to/non/existent/file1.php'; + $nonExistentFile2 = '/path/to/non/existent/file2.php'; + + $snapshot = FilesSnapshot::for([ + $nonExistentFile1, + $existingFile, + $nonExistentFile2, + ]); + + self::assertFalse($snapshot->changed()); + + $this->touch($existingFile); + + self::assertTrue($snapshot->changed()); + } + private function touch(string $fileName): void { touch($fileName, filemtime($fileName) + 1); clearstatcache(); } -} \ No newline at end of file +} diff --git a/tests/Loggers/ExceptionLogger.php b/tests/Loggers/ExceptionLogger.php index 612d0226ce..3a0a65861a 100644 --- a/tests/Loggers/ExceptionLogger.php +++ b/tests/Loggers/ExceptionLogger.php @@ -1,12 +1,15 @@ expectException(TypeMismatchRuntimeException::class); + $this->expectExceptionMessage('Unexpected null value for non nullable field. Expected: "String!"'); ResolveUtils::assertInnerReturnType(null, Type::nonNull(Type::string())); } @@ -30,6 +31,7 @@ public function testAssertObjectOk(): void public function testAssertInputNull(): void { $this->expectException(TypeMismatchRuntimeException::class); + $this->expectExceptionMessage('Unexpected null value for non nullable field. Expected: "String!"'); ResolveUtils::assertInnerInputType(null, Type::nonNull(Type::string())); }