From e833060c99137c91f5959876bcabdc3cc228becf Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 21 Aug 2025 13:44:06 +0200 Subject: [PATCH 1/4] PHP 8.5 compatibility --- .github/workflows/static.yml | 16 +-- .github/workflows/tests.yml | 8 +- Makefile | 16 +-- composer.json | 2 +- tests/PhpOption/Tests/EnsureTest.php | 5 - tests/PhpOption/Tests/LazyOptionTest.php | 123 ++++++++++++++++++++++- vendor-bin/phpstan/composer.json | 2 +- vendor-bin/psalm/composer.json | 2 +- 8 files changed, 143 insertions(+), 31 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3fb0712..bd9cf65 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -7,11 +7,11 @@ on: jobs: phpstan: name: PHPStan - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -23,14 +23,14 @@ jobs: update: true - name: Install Dependencies - uses: nick-invision/retry@v2 + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: composer update --no-interaction --no-progress - name: Install PHPStan - uses: nick-invision/retry@v2 + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -41,11 +41,11 @@ jobs: psalm: name: Psalm - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -57,14 +57,14 @@ jobs: update: true - name: Install Dependencies - uses: nick-invision/retry@v2 + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: composer update --no-interaction --no-progress - name: Install Psalm - uses: nick-invision/retry@v2 + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac73b00..2c30df9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,15 +7,15 @@ on: jobs: tests: name: PHP ${{ matrix.php }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -30,7 +30,7 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install PHP Dependencies - uses: nick-invision/retry@v2 + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/Makefile b/Makefile index 07abb41..b56bc42 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,24 @@ install: - @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.3-base update - @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.3-base bin all update + @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.4-base update + @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.4-base bin all update phpunit: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpunit --rm registry.gitlab.com/grahamcampbell/php:8.3-cli + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpunit --rm registry.gitlab.com/grahamcampbell/php:8.4-cli phpstan-analyze: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.3-cli analyze + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.4-cli analyze phpstan-baseline: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.3-cli analyze --generate-baseline + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.4-cli analyze --generate-baseline psalm-analyze: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.3-cli + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli psalm-baseline: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.3-cli --set-baseline=psalm-baseline.xml + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli --set-baseline=psalm-baseline.xml psalm-show-info: - @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.3-cli --show-info=true + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli --show-info=true test: phpunit phpstan-analyze psalm-analyze diff --git a/composer.json b/composer.json index 91dd6fb..c077040 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "autoload": { "psr-4": { diff --git a/tests/PhpOption/Tests/EnsureTest.php b/tests/PhpOption/Tests/EnsureTest.php index 7384cea..82ffc6c 100644 --- a/tests/PhpOption/Tests/EnsureTest.php +++ b/tests/PhpOption/Tests/EnsureTest.php @@ -7,11 +7,6 @@ use PhpOption\Some; use PHPUnit\Framework\TestCase; -/** - * Tests for Option::ensure() method. - * - * @covers Option::ensure - */ class EnsureTest extends TestCase { private static function ensure($value, $noneValue = null): Option diff --git a/tests/PhpOption/Tests/LazyOptionTest.php b/tests/PhpOption/Tests/LazyOptionTest.php index e1b5128..63110b2 100644 --- a/tests/PhpOption/Tests/LazyOptionTest.php +++ b/tests/PhpOption/Tests/LazyOptionTest.php @@ -178,11 +178,12 @@ public function testFoldLeftRight(): void $callback = function () { }; - $option = self::getMockForAbstractClass(Option::class); + // Use TestOption as a concrete implementation to test with + $option = self::createPartialMock(TestOption::class, ['foldLeft', 'foldRight']); $option->expects(self::once()) ->method('foldLeft') ->with(5, $callback) - ->will(self::returnValue(6)); + ->willReturn(6); $lazyOption = new LazyOption(function () use ($option) { return $option; }); @@ -191,10 +192,126 @@ public function testFoldLeftRight(): void $option->expects(self::once()) ->method('foldRight') ->with(5, $callback) - ->will(self::returnValue(6)); + ->willReturn(6); $lazyOption = new LazyOption(function () use ($option) { return $option; }); self::assertSame(6, $lazyOption->foldRight(5, $callback)); } } + +class TestOption extends Option +{ + private $value; + + public function __construct($value = null) + { + $this->value = $value; + } + + public function get() + { + return $this->value; + } + + public function getOrElse($default) + { + return $this->isDefined() ? $this->value : $default; + } + + public function getOrCall($callable) + { + return $this->isDefined() ? $this->value : call_user_func($callable); + } + + public function getOrThrow(\Exception $ex) + { + if ($this->isDefined()) { + return $this->value; + } + throw $ex; + } + + public function isEmpty() + { + return $this->value === null; + } + + public function isDefined() + { + return $this->value !== null; + } + + public function orElse(Option $else) + { + return $this->isDefined() ? $this : $else; + } + + public function ifDefined($callable) + { + if ($this->isDefined()) { + call_user_func($callable, $this->value); + } + } + + public function forAll($callable) + { + if ($this->isDefined()) { + call_user_func($callable, $this->value); + } + } + + public function map($callable) + { + return $this->isDefined() ? new self(call_user_func($callable, $this->value)) : $this; + } + + public function flatMap($callable) + { + return $this->isDefined() ? call_user_func($callable, $this->value) : $this; + } + + public function filter($callable) + { + return $this->isDefined() && call_user_func($callable, $this->value) ? $this : None::create(); + } + + public function filterNot($callable) + { + return $this->isDefined() && !call_user_func($callable, $this->value) ? $this : None::create(); + } + + public function select($value) + { + return $this->isDefined() && $this->value === $value ? $this : None::create(); + } + + public function reject($value) + { + return $this->isDefined() && $this->value !== $value ? $this : None::create(); + } + + public function foldLeft($initialValue, $callable) + { + if ($this->isDefined()) { + return call_user_func($callable, $initialValue, $this->value); + } + return $initialValue; + } + + public function foldRight($initialValue, $callable) + { + if ($this->isDefined()) { + return call_user_func($callable, $this->value, $initialValue); + } + return $initialValue; + } + + public function getIterator(): \Traversable + { + if ($this->isDefined()) { + return new \ArrayIterator([$this->value]); + } + return new \ArrayIterator([]); + } +} diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json index e24f982..6cbc11b 100644 --- a/vendor-bin/phpstan/composer.json +++ b/vendor-bin/phpstan/composer.json @@ -1,6 +1,6 @@ { "require": { - "phpstan/phpstan": "1.11.7" + "phpstan/phpstan": "2.122" }, "config": { "preferred-install": "dist" diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json index 5aceaa6..5e841f5 100644 --- a/vendor-bin/psalm/composer.json +++ b/vendor-bin/psalm/composer.json @@ -1,6 +1,6 @@ { "require": { - "psalm/phar": "5.25.0" + "psalm/phar": "6.13.1" }, "config": { "preferred-install": "dist" From 87fbdc0e7d9fce0629523f136a650e02805575af Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 21 Aug 2025 13:48:30 +0200 Subject: [PATCH 2/4] Fixes --- phpstan-baseline.neon | 24 ++++++++++++++++++------ psalm-baseline.xml | 2 +- psalm.xml | 4 ++++ vendor-bin/phpstan/composer.json | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5310bd2..c1077ba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,32 +1,44 @@ parameters: ignoreErrors: - - message: "#^Method PhpOption\\\\Option\\:\\:ensure\\(\\) should return PhpOption\\\\Option\\ but returns PhpOption\\\\LazyOption\\\\.$#" + message: '#^Call to function is_callable\(\) with callable\(mixed \.\.\.\)\: PhpOption\\Option\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: src/PhpOption/LazyOption.php + + - + message: '#^Method PhpOption\\Option\:\:ensure\(\) should return PhpOption\\Option\ but returns PhpOption\\LazyOption\\.$#' + identifier: return.type count: 1 path: src/PhpOption/Option.php - - message: "#^Method PhpOption\\\\Option\\:\\:fromReturn\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" + message: '#^Method PhpOption\\Option\:\:fromReturn\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 path: src/PhpOption/Option.php - - message: "#^Method PhpOption\\\\Option\\:\\:fromReturn\\(\\) should return PhpOption\\\\LazyOption\\ but returns PhpOption\\\\LazyOption\\\\.$#" + message: '#^Method PhpOption\\Option\:\:fromReturn\(\) should return PhpOption\\LazyOption\ but returns PhpOption\\LazyOption\\.$#' + identifier: return.type count: 1 path: src/PhpOption/Option.php - - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, Closure\\(PhpOption\\\\Option\\)\\: T given\\.$#" + message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, Closure\(PhpOption\\Option\)\: T given\.$#' + identifier: argument.type count: 1 path: src/PhpOption/Option.php - - message: "#^Parameter \\#2 \\$callback of function array_reduce expects callable\\(bool\\|TReturn, mixed\\)\\: \\(bool\\|TReturn\\), Closure\\(mixed, PhpOption\\\\Option\\)\\: \\(bool\\|TReturn\\) given\\.$#" + message: '#^Parameter \#2 \$callback of function array_reduce expects callable\(bool\|TReturn, mixed\)\: \(bool\|TReturn\), Closure\(mixed, PhpOption\\Option\)\: \(bool\|TReturn\) given\.$#' + identifier: argument.type count: 1 path: src/PhpOption/Option.php - - message: "#^Template type S of method PhpOption\\\\Option\\:\\:lift\\(\\) is not referenced in a parameter\\.$#" + message: '#^Template type S of method PhpOption\\Option\:\:lift\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter count: 1 path: src/PhpOption/Option.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7bf39a8..d04c5aa 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,2 +1,2 @@ - + diff --git a/psalm.xml b/psalm.xml index 7ee17a8..0d745a3 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,4 +12,8 @@ + + + + diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json index 6cbc11b..7c3fa69 100644 --- a/vendor-bin/phpstan/composer.json +++ b/vendor-bin/phpstan/composer.json @@ -1,6 +1,6 @@ { "require": { - "phpstan/phpstan": "2.122" + "phpstan/phpstan": "2.1.22" }, "config": { "preferred-install": "dist" From 3fcf41629005542de15a497576903cfe62380ef3 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 21 Aug 2025 13:51:26 +0200 Subject: [PATCH 3/4] Update LazyOptionTest.php --- tests/PhpOption/Tests/LazyOptionTest.php | 58 ++++++++++++++++++++---- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/tests/PhpOption/Tests/LazyOptionTest.php b/tests/PhpOption/Tests/LazyOptionTest.php index 63110b2..4ecf850 100644 --- a/tests/PhpOption/Tests/LazyOptionTest.php +++ b/tests/PhpOption/Tests/LazyOptionTest.php @@ -216,12 +216,20 @@ public function get() public function getOrElse($default) { - return $this->isDefined() ? $this->value : $default; + if ($this->isDefined()) { + return $this->value; + } + + return $default; } public function getOrCall($callable) { - return $this->isDefined() ? $this->value : call_user_func($callable); + if ($this->isDefined()) { + return $this->value; + } + + return call_user_func($callable); } public function getOrThrow(\Exception $ex) @@ -229,6 +237,7 @@ public function getOrThrow(\Exception $ex) if ($this->isDefined()) { return $this->value; } + throw $ex; } @@ -244,7 +253,11 @@ public function isDefined() public function orElse(Option $else) { - return $this->isDefined() ? $this : $else; + if ($this->isDefined()) { + return $this; + } + + return $else; } public function ifDefined($callable) @@ -263,32 +276,56 @@ public function forAll($callable) public function map($callable) { - return $this->isDefined() ? new self(call_user_func($callable, $this->value)) : $this; + if ($this->isDefined()) { + return new self(call_user_func($callable, $this->value)); + } + + return $this; } public function flatMap($callable) { - return $this->isDefined() ? call_user_func($callable, $this->value) : $this; + if ($this->isDefined()) { + return call_user_func($callable, $this->value); + } + + return $this; } public function filter($callable) { - return $this->isDefined() && call_user_func($callable, $this->value) ? $this : None::create(); + if ($this->isDefined() && call_user_func($callable, $this->value)) { + return $this; + } + + return None::create(); } public function filterNot($callable) { - return $this->isDefined() && !call_user_func($callable, $this->value) ? $this : None::create(); + if ($this->isDefined() && !call_user_func($callable, $this->value)) { + return $this; + } + + return None::create(); } public function select($value) { - return $this->isDefined() && $this->value === $value ? $this : None::create(); + if ($this->isDefined() && $this->value === $value) { + return $this; + } + + return None::create(); } public function reject($value) { - return $this->isDefined() && $this->value !== $value ? $this : None::create(); + if ($this->isDefined() && $this->value !== $value) { + return $this; + } + + return None::create(); } public function foldLeft($initialValue, $callable) @@ -296,6 +333,7 @@ public function foldLeft($initialValue, $callable) if ($this->isDefined()) { return call_user_func($callable, $initialValue, $this->value); } + return $initialValue; } @@ -304,6 +342,7 @@ public function foldRight($initialValue, $callable) if ($this->isDefined()) { return call_user_func($callable, $this->value, $initialValue); } + return $initialValue; } @@ -312,6 +351,7 @@ public function getIterator(): \Traversable if ($this->isDefined()) { return new \ArrayIterator([$this->value]); } + return new \ArrayIterator([]); } } From 88fde57a91a2026258bb3d9b173e7b6696ffc734 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 21 Aug 2025 13:52:11 +0200 Subject: [PATCH 4/4] Update static.yml --- .github/workflows/static.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index bd9cf65..3d42914 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -16,7 +16,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' tools: composer:v2 coverage: none env: @@ -50,7 +50,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' tools: composer:v2 coverage: none env: