diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 803ec21..6b76966 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,7 +6,6 @@ on: pull_request: ~ env: - PHP_VERSION: 8.1 APPLICATION_NAME: antifraud GCP_PROJECT_ID: liniopay-sandbox GCP_REGION: us-east4 @@ -16,9 +15,13 @@ env: NEW_RELIC_APP_NAME: ${{ secrets.NEW_RELIC_APP_NAME }} jobs: - test-php-81: - name: Code quality PHP 8.1 + test-php: + name: Code quality PHP ${{ matrix.php-version }} runs-on: [ubuntu-latest] + strategy: + fail-fast: false + matrix: + php-version: ['8.2.35', '8.3.36', '8.4.27', '8.5.12'] steps: - name: Checkout uses: actions/checkout@v2 @@ -26,7 +29,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: ${{ matrix.php-version }} coverage: xdebug - name: Validate composer.json and composer.lock @@ -36,6 +39,7 @@ jobs: run: composer install --prefer-dist --no-progress - name: Lint + if: matrix.php-version == '8.2.35' run: composer lint:check - name: PHP Static Analysis diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 01b8792..7527252 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -10,8 +10,8 @@ $config ->setRules([ '@Symfony' => true, - '@PHP81Migration' => true, - '@PHPUnit60Migration:risky' => true, + '@PHP8x1Migration' => true, + '@PHPUnit6x0Migration:risky' => true, 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], 'blank_line_after_opening_tag' => true, diff --git a/.travis.yml b/.travis.yml index 7840d3d..1372cce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: php php: - - 8.1 + - '8.2.35' + - '8.3.36' + - '8.4.27' + - '8.5.12' cache: directories: diff --git a/README.md b/README.md index 69f42a5..0f7857e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,35 @@ a coherent base for Linio components: ## Install +### PHP requirements + +Supported PHP branches and minimum patch versions are: + +| PHP branch | Minimum version | +| --- | --- | +| 8.2 | 8.2.35 | +| 8.3 | 8.3.36 | +| 8.4 | 8.4.27 | +| 8.5 | 8.5.12 | + +Later patch releases within these branches are supported and recommended. PHP 8.1 +and older, earlier patches, and unlisted branches are not supported. + +These minimum versions contain the fixes for CVE-2026-109618 and CVE-2026-109619 +described in the [PHP security advisory of October 21, 2026](https://www.php.net/archive/2026.php#2026-10-21-5). +The vulnerabilities are in the PHP runtime, not in this library. Updating this +package alone does not patch PHP: update the interpreter used by every deployment, +including PHP-FPM, Apache, CGI, FrankenPHP, LiteSpeed, CLI and CI, and restart all +web workers after the update. Verify the PHP version used by the web server +separately from `php -v`, which only checks the CLI runtime. + +Composer rejects versions outside the supported ranges, but it cannot update the +runtime or protect deployments that bypass platform checks. Do not use +`--ignore-platform-reqs` or a `config.platform.php` override to simulate a patched +runtime. Run `composer check-platform-reqs --no-dev` on the deployed environment +and verify the web runtime separately. Configuration workarounds are not a +complete fix and are not applied by this library. + The recommended way to install Linio Common is [through composer](http://getcomposer.org). ``` @@ -24,6 +53,15 @@ run PHPUnit. $ composer install $ vendor/bin/phpunit +Run code formatting on PHP 8.2, the lowest supported PHP branch, to avoid +introducing syntax unsupported by that branch: + + $ composer lint + $ composer lint:check + +CI checks formatting on PHP 8.2 and runs static analysis and unit tests on every +supported PHP branch. + ## Collections This component has a direct dependency on doctrine/collections. You are encouraged diff --git a/composer.json b/composer.json index 8c6c6ef..007df87 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,11 @@ "type": "library", "license": "BSD-3-Clause", "require": { - "php": "^8.1", + "php": "~8.2.35 || ~8.3.36 || ~8.4.27 || ~8.5.12", "doctrine/collections": "^1.6" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.6", + "friendsofphp/php-cs-fixer": "^3.95.27", "monolog/monolog": "^2.0", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^9.4" diff --git a/src/Exception/ClientException.php b/src/Exception/ClientException.php index dbe70dc..9ce898a 100644 --- a/src/Exception/ClientException.php +++ b/src/Exception/ClientException.php @@ -15,7 +15,7 @@ public function __construct( int $statusCode = self::DEFAULT_STATUS_CODE, string $message = ExceptionTokens::INVALID_REQUEST, array $errors = [], - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($token, $statusCode, $message, $errors, $previous); } diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php index eee168a..4298ef1 100644 --- a/src/Exception/DomainException.php +++ b/src/Exception/DomainException.php @@ -23,7 +23,7 @@ public function __construct( int $statusCode = self::DEFAULT_STATUS_CODE, string $message = ExceptionTokens::AN_ERROR_HAS_OCCURRED, array $errors = [], - Throwable $previous = null + ?Throwable $previous = null ) { if (!$this->isExceptionToken($token)) { throw new InvalidArgumentException(ExceptionTokens::INVALID_EXCEPTION_TOKEN, 500); diff --git a/src/Exception/EntityNotFoundException.php b/src/Exception/EntityNotFoundException.php index d879e20..aab742b 100644 --- a/src/Exception/EntityNotFoundException.php +++ b/src/Exception/EntityNotFoundException.php @@ -22,7 +22,7 @@ public function __construct( string $token = ExceptionTokens::ENTITY_NOT_FOUND, int $statusCode = self::DEFAULT_STATUS_CODE, array $errors = [], - Throwable $previous = null + ?Throwable $previous = null ) { $formattedIdentifier = $this->getIdentifier($identifier); $message = sprintf('Could not find [%s] identified by %s!', $entity, $formattedIdentifier); diff --git a/src/Type/Collection/FixedTypedCollection.php b/src/Type/Collection/FixedTypedCollection.php index 33c6904..d0babf8 100644 --- a/src/Type/Collection/FixedTypedCollection.php +++ b/src/Type/Collection/FixedTypedCollection.php @@ -4,6 +4,7 @@ namespace Linio\Common\Type\Collection; +use Closure; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor; use InvalidArgumentException; @@ -97,9 +98,9 @@ public function matching(Criteria $criteria) if ($expr) { $visitor = new ClosureExpressionVisitor(); - $filtered = array_filter($filtered, function () use ($visitor, $expr) { - return $visitor->dispatch($expr); - }); + /** @var Closure(mixed): bool $filter */ + $filter = $visitor->dispatch($expr); + $filtered = array_filter($filtered, $filter); } if ($orderings = $criteria->getOrderings()) { diff --git a/src/Type/Date.php b/src/Type/Date.php index 451c84f..33bd97d 100644 --- a/src/Type/Date.php +++ b/src/Type/Date.php @@ -9,7 +9,7 @@ class Date extends DateTime { - public function __construct(string $time = 'now', DateTimeZone $timezone = null) + public function __construct(string $time = 'now', ?DateTimeZone $timezone = null) { parent::__construct($time, $timezone); $this->setTime(0, 0, 0); diff --git a/src/Type/Money.php b/src/Type/Money.php index 78ae510..339e597 100644 --- a/src/Type/Money.php +++ b/src/Type/Money.php @@ -89,7 +89,7 @@ public function applyInterest(float $rate, int $duration): MoneyInterface public function equals(object $other): bool { - if (!($other instanceof static)) { + if (!$other instanceof static) { return false; } diff --git a/src/Type/PreciseMoney.php b/src/Type/PreciseMoney.php index 6369d4e..bc05ac9 100644 --- a/src/Type/PreciseMoney.php +++ b/src/Type/PreciseMoney.php @@ -91,7 +91,7 @@ public function applyInterest(float $rate, int $duration): MoneyInterface public function equals(object $other): bool { - if (!($other instanceof MoneyInterface)) { + if (!$other instanceof MoneyInterface) { return false; } diff --git a/src/Type/Time.php b/src/Type/Time.php index 83df3f0..ab59d63 100644 --- a/src/Type/Time.php +++ b/src/Type/Time.php @@ -9,7 +9,7 @@ class Time extends DateTime { - public function __construct(string $time = 'now', DateTimeZone $timezone = null) + public function __construct(string $time = 'now', ?DateTimeZone $timezone = null) { parent::__construct($time, $timezone); $this->setDate(1970, 1, 1); diff --git a/tests/Type/Collection/FixedTypedCollectionTest.php b/tests/Type/Collection/FixedTypedCollectionTest.php index 45bb207..ed9c339 100644 --- a/tests/Type/Collection/FixedTypedCollectionTest.php +++ b/tests/Type/Collection/FixedTypedCollectionTest.php @@ -13,9 +13,9 @@ class FixedTypedCollectionTest extends TestCase { public function testIsValidatingSizeOnConstructor(): void { - $object1 = new class() { + $object1 = new class { }; - $object2 = new class() { + $object2 = new class { }; $this->expectException(InvalidArgumentException::class); @@ -41,13 +41,13 @@ public function isValidType($value): bool $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Index invalid or out of range'); - $collection[] = new class() { + $collection[] = new class { }; } public function testIsValidatingSizeOnAdd(): void { - $object1 = new class() { + $object1 = new class { }; $collection = new class(0) extends FixedTypedCollection { @@ -65,7 +65,7 @@ public function isValidType($value): bool public function testIsGetting(): void { - $object1 = new class() { + $object1 = new class { }; $collection = new class(1, [$object1]) extends FixedTypedCollection { @@ -80,7 +80,7 @@ public function isValidType($value): bool public function testIsValidatingKeyOnGet(): void { - $object1 = new class() { + $object1 = new class { }; $collection = new class(1, [$object1]) extends FixedTypedCollection { @@ -98,7 +98,7 @@ public function isValidType($value): bool public function testIsValidatingKeyOnOffsetGet(): void { - $object1 = new class() { + $object1 = new class { public $test = 'foo'; }; @@ -117,11 +117,14 @@ public function isValidType($value): bool public function testIsApplyingMatchingCriteria(): void { - $object1 = new class() { + $object1 = new class { public $test = 'foo'; }; + $object2 = new class { + public $test = 'bar'; + }; - $collection = new class(1, [$object1]) extends FixedTypedCollection { + $collection = new class(2, [$object1, $object2]) extends FixedTypedCollection { public function isValidType($value): bool { return true; @@ -133,5 +136,10 @@ public function isValidType($value): bool $actual = $collection->matching($criteria); $this->assertSame([$object1], $actual->getValues()); + + $criteria = Criteria::create()->where((new ExpressionBuilder())->eq('test', 'missing')); + + $this->assertSame([], $collection->matching($criteria)->getValues()); + $this->assertSame([$object1, $object2], $collection->getValues()); } }