From 72c8622222d9bc813eb9749ccb20735b2b206a33 Mon Sep 17 00:00:00 2001 From: Alexander Kurilo Date: Mon, 4 May 2026 16:33:26 +0200 Subject: [PATCH 1/3] Support PHP 8.5 --- .github/workflows/ci.yml | 3 +++ composer | 14 ++++++++++++-- tests/BaseTestCase.php | 8 ++++++-- tests/ComposerWrapperParamsTest.php | 17 +++++++++++++++-- tests/ComposerWrapperTest.php | 6 ++++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fda8b6d..bd6c80b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: - php: "8.0" - php: "8.1" - php: "8.2" + - php: "8.3" + - php: "8.4" + - php: "8.5" steps: - name: Checkout diff --git a/composer b/composer index 9508812..e66b0db 100755 --- a/composer +++ b/composer @@ -118,9 +118,19 @@ if (!class_exists('ComposerWrapper')) { $modified = clone $now; //hack: DateTime $modifier argument validation. Taken from https://stackoverflow.com/a/34899724/2165434 - $success = @$modified->modify($updateFreq); + try { + $success = @$modified->modify($updateFreq); + } catch (\Exception $e) { + $success = false; + } if ($success === false) { - throw new \Exception(sprintf('Wrong update frequency is requested: %s; should be valid DateTime modifier (follow %s for the help)', $updateFreq,'https://www.php.net/manual/en/datetime.modify.php')); + throw new \Exception( + sprintf( + 'Wrong update frequency is requested: %s; should be valid DateTime modifier (follow %s for the help)', + $updateFreq, + 'https://www.php.net/manual/en/datetime.modify.php' + ) + ); } //endhack diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 661b6b0..5f21729 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -20,7 +20,9 @@ private function getExpectedShebang() public static function callNonPublic($object, $method, $args) { $method = new ReflectionMethod($object, $method); - $method->setAccessible(true); + if (PHP_VERSION_ID < 80500) { + $method->setAccessible(true); + } return $method->invokeArgs($object, $args); } @@ -28,7 +30,9 @@ public static function callNonPublic($object, $method, $args) protected static function setNonPublic($object, $property, $arg) { $property = new ReflectionProperty($object, $property); - $property->setAccessible(true); + if (PHP_VERSION_ID < 80500) { + $property->setAccessible(true); + } $property->setValue($object, $arg); } diff --git a/tests/ComposerWrapperParamsTest.php b/tests/ComposerWrapperParamsTest.php index 49f42bf..bcb8509 100644 --- a/tests/ComposerWrapperParamsTest.php +++ b/tests/ComposerWrapperParamsTest.php @@ -132,10 +132,9 @@ public function setComposerDirHandlesGoodValues() self::assertSame(__DIR__, $actual); } - public function composerDirBadDataProvider() + public static function composerDirBadDataProvider() { return array( - "access denied" => array('/var'), "doesn't exists" => array(__DIR__ . '/i_dont_exist'), "non dir" => array(__FILE__), ); @@ -152,6 +151,20 @@ public function setComposerDirThrowsOnBadValues($input) self::callNonPublic($params, 'setComposerDir', array($input)); } + /** + * @test + */ + public function setComposerDirThrowsOnNotWritableDirectory() + { + $root = vfsStream::setup(); + $dir = vfsStream::newDirectory('readonly', 0555); + $root->addChild($dir); + + $this->expectExceptionMessageRegExpCompat('\Exception', '/Wrong composer dir is requested:.*/'); + $params = new ComposerWrapperParams(); + self::callNonPublic($params, 'setComposerDir', array($dir->url())); + } + /** * @return ComposerWrapperParams */ diff --git a/tests/ComposerWrapperTest.php b/tests/ComposerWrapperTest.php index 13637e7..563f1e4 100644 --- a/tests/ComposerWrapperTest.php +++ b/tests/ComposerWrapperTest.php @@ -386,7 +386,7 @@ public function selfUpdateWorks() ->willReturnCallback(function ($command, &$exitCode) { $exitCode = 0; }); self::callNonPublic($wrapper, 'selfUpdate', array($file->url())); - clearstatcache(null, $file->url()); + clearstatcache(true, $file->url()); $this->assertGreaterThanOrEqual($now->getTimestamp(), filemtime($file->url())); } @@ -555,7 +555,9 @@ public function passThroughWrapperWorksWithReferences() $exitCode = null; $class = new ReflectionClass($wrapper); $method = $class->getMethod('passthru'); - $method->setAccessible(true); + if (PHP_VERSION_ID < 80500) { + $method->setAccessible(true); + } $method->invokeArgs( $wrapper, array( From e0bb14238b7c0a841589562ff36f1f79eb46303b Mon Sep 17 00:00:00 2001 From: Alexander Kurilo Date: Mon, 4 May 2026 18:10:50 +0200 Subject: [PATCH 2/3] Do not swallow compile-time errors --- tests/bootstrap.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8d5cfbf..e4cd03c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,6 +3,32 @@ require __DIR__ . '/BaseTestCase.php'; // output buffer manipulations to get rid of shebang (#!/usr/bin/env php) +$bootstrapFailureMessages = array(); + +set_error_handler(function ($severity, $message, $file, $line) use (&$bootstrapFailureMessages) { + $bootstrapFailureMessages[] = sprintf('PHP Error: %s in %s:%d', $message, $file, $line); + + return true; +}); + +set_exception_handler(function ($exception) use (&$bootstrapFailureMessages) { + $bootstrapFailureMessages[] = sprintf( + 'Uncaught %s: %s in %s:%d', + get_class($exception), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ); +}); + ob_start(); require __DIR__ . '/../composer'; ob_end_clean(); + +restore_exception_handler(); +restore_error_handler(); + +if (!empty($bootstrapFailureMessages)) { + fwrite(STDERR, implode(PHP_EOL, $bootstrapFailureMessages) . PHP_EOL); + exit(1); +} From 309d8c01088ad215c773246a155043528caf3b00 Mon Sep 17 00:00:00 2001 From: Alexander Kurilo Date: Mon, 4 May 2026 22:29:10 +0200 Subject: [PATCH 3/3] Make constructor compatible with PHP 8.4+ At the same time it has to be compatible with older PHP versions that don't have the nullable types, so it seems that the only way forward is... to drop the type declaration. --- composer | 13 ++++++++--- tests/ComposerWrapperTest.php | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/composer b/composer index e66b0db..0a926db 100755 --- a/composer +++ b/composer @@ -188,13 +188,20 @@ if (!class_exists('ComposerWrapper')) { */ protected $params; - public function __construct(ComposerWrapperParams $params = null) + public function __construct($params = null) { - if (null === $params ) { + if (null === $params) { $this->params = new ComposerWrapperParams(); $this->params->loadReal(); - } else { + } elseif ($params instanceof ComposerWrapperParams) { $this->params = $params; + } else { + throw new \InvalidArgumentException( + sprintf( + "%s class can only be instantiated with ComposerWrapperParams object", + __CLASS__ + ) + ); } } diff --git a/tests/ComposerWrapperTest.php b/tests/ComposerWrapperTest.php index 563f1e4..9556e79 100644 --- a/tests/ComposerWrapperTest.php +++ b/tests/ComposerWrapperTest.php @@ -17,6 +17,48 @@ private static function getInstance() return new $class; } + /** + * @test + */ + public function acceptsParameters() + { + try { + new ComposerWrapper(new ComposerWrapperParams()); + new ComposerWrapper(); + new ComposerWrapper(null); + $passed = true; + } catch (Exception $e) { + $passed = false; + } + self::assertTrue($passed); + } + + /** + * @test + * @dataProvider invalidConstructorArguments + */ + public function throwsOnInvalidParameters(array $args) + { + $this->expectExceptionMessageCompat( + 'InvalidArgumentException', + 'ComposerWrapper class can only be instantiated with ComposerWrapperParams object' + ); + $class = new ReflectionClass('ComposerWrapper'); + $class->newInstanceArgs($args); + } + + public static function invalidConstructorArguments() + { + return array( + array(array('test')), + array(array(new \stdClass())), + array(array(true)), + array(array(false)), + array(array(123)), + array(array(123.45)), + ); + } + /** * @test */