From d669f775c9798d731715e5aeda9e76eb39c4acdf Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Wed, 29 Apr 2026 10:10:33 -0500 Subject: [PATCH 1/2] Fix PHP 8.5 reflection deprecations --- src/Nelmio/Alice/Loader/Base.php | 19 ++++++++++---- tests/Nelmio/Alice/FixturesTest.php | 1 - tests/Nelmio/Alice/Loader/BaseTest.php | 36 ++++++++++++++++++++++++++ tests/Nelmio/Alice/Loader/YamlTest.php | 3 +-- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/Nelmio/Alice/Loader/Base.php b/src/Nelmio/Alice/Loader/Base.php index e144de2b8..f0daa88f5 100644 --- a/src/Nelmio/Alice/Loader/Base.php +++ b/src/Nelmio/Alice/Loader/Base.php @@ -515,7 +515,6 @@ private function populateObject($instance, $class, $name, $data, $classFlags, $i $generatedVal = $this->checkTypeHints($instance, 'set'.$key, $generatedVal); if(!is_callable(array($instance, 'set'.$key))) { $refl = new \ReflectionMethod($instance, 'set'.$key); - $refl->setAccessible(true); $refl->invoke($instance, $generatedVal); } else { $instance->{'set'.$key}($generatedVal); @@ -523,7 +522,6 @@ private function populateObject($instance, $class, $name, $data, $classFlags, $i $variables[$key] = $generatedVal; } elseif (property_exists($instance, $key)) { $refl = new \ReflectionProperty($instance, $key); - $refl->setAccessible(true); $refl->setValue($instance, $generatedVal); $variables[$key] = $generatedVal; @@ -559,12 +557,12 @@ private function checkTypeHints($obj, $method, $value, $pNum = 0) $reflection = new \ReflectionMethod($obj, $method); $params = $reflection->getParameters(); - if (!$params[$pNum]->getClass()) { + $hintedClass = $this->getClassTypeName($params[$pNum]); + + if (!$hintedClass) { return $value; } - $hintedClass = $params[$pNum]->getClass()->getName(); - if ($hintedClass === 'DateTime') { try { if (preg_match('{^[0-9]+$}', $value)) { @@ -587,6 +585,17 @@ private function checkTypeHints($obj, $method, $value, $pNum = 0) return $value; } + private function getClassTypeName(\ReflectionParameter $parameter) + { + $type = $parameter->getType(); + + if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) { + return null; + } + + return $type->getName(); + } + private function process($data, array $variables) { if (is_array($data)) { diff --git a/tests/Nelmio/Alice/FixturesTest.php b/tests/Nelmio/Alice/FixturesTest.php index 72d24a192..b6dbda9a9 100644 --- a/tests/Nelmio/Alice/FixturesTest.php +++ b/tests/Nelmio/Alice/FixturesTest.php @@ -184,7 +184,6 @@ public function testThatNewLoaderIsCreatedForDifferingOptions() } $prop = new \ReflectionProperty('\Nelmio\Alice\Fixtures', 'loaders'); - $prop->setAccessible(true); $loaders = $prop->getValue(); $this->assertEquals(12, count($loaders)); diff --git a/tests/Nelmio/Alice/Loader/BaseTest.php b/tests/Nelmio/Alice/Loader/BaseTest.php index 915a14bdb..d340eafbf 100644 --- a/tests/Nelmio/Alice/Loader/BaseTest.php +++ b/tests/Nelmio/Alice/Loader/BaseTest.php @@ -441,6 +441,42 @@ public function testLoadCoercesDatesForDateTimeHints() $this->assertInstanceOf('DateTime', $res['group1']->getCreationDate()); } + public function testLoadCoercesTimestampForConstructorDateTimeHints() + { + $res = $this->loadData(array( + self::USER => array( + 'user0' => array( + '__construct' => array(null, null, '1325721600'), + ), + ), + )); + + $this->assertInstanceOf('DateTime', $res['user0']->birthDate); + $this->assertEquals('1325721600', $res['user0']->birthDate->format('U')); + } + + public function testLoadFetchesScalarIdsForClassHints() + { + $owner = new User(); + $orm = $this->getMockBuilder('Nelmio\Alice\ORMInterface')->getMock(); + $orm->expects($this->once()) + ->method('find') + ->with(self::USER, '42') + ->will($this->returnValue($owner)); + + $loader = $this->createLoader(); + $loader->setORM($orm); + $res = $loader->load(array( + self::GROUP => array( + 'group0' => array( + 'owner' => '42', + ), + ), + )); + + $this->assertSame($owner, $res['group0']->getOwner()); + } + public function testLoadParsesFakerData() { $res = $this->loadData(array( diff --git a/tests/Nelmio/Alice/Loader/YamlTest.php b/tests/Nelmio/Alice/Loader/YamlTest.php index 76ffc85cc..fb28720e0 100644 --- a/tests/Nelmio/Alice/Loader/YamlTest.php +++ b/tests/Nelmio/Alice/Loader/YamlTest.php @@ -20,7 +20,6 @@ public function testIncludeFiles() $file = __DIR__ . '/../fixtures/include.yml'; $loader = new \Nelmio\Alice\Loader\Yaml(); $reflMethod = new \ReflectionMethod($loader, 'parse'); - $reflMethod->setAccessible(true); $data = $reflMethod->invoke($loader, $file); $expectedData = array( 'Nelmio\\Alice\\fixtures\\Product' => @@ -73,4 +72,4 @@ public function testIncludeFiles() $this->assertEquals($expectedData, $data); } -} \ No newline at end of file +} From 227e7f05761ed5fbce2cf7dc742ef5e0703106d4 Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Wed, 29 Apr 2026 10:41:45 -0500 Subject: [PATCH 2/2] Add PHP 8.4 and 8.5 test coverage --- .github/workflows/tests.yml | 39 +++++++++++ .gitignore | 3 +- composer.json | 4 +- phpunit.xml.dist | 29 ++++---- tests/Nelmio/Alice/FixturesTest.php | 25 +++---- tests/Nelmio/Alice/Loader/BaseTest.php | 93 +++++++++++--------------- tests/Nelmio/Alice/fixtures/User.php | 7 +- 7 files changed, 109 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..1e2f3e8dd --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + +jobs: + phpunit: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: + - '8.4' + - '8.5' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Validate composer.json + run: composer validate --strict --no-check-lock + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Run tests + run: vendor/bin/phpunit --display-all-issues --fail-on-phpunit-deprecation diff --git a/.gitignore b/.gitignore index a725465ae..c15afe246 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -vendor/ \ No newline at end of file +vendor/ +.phpunit.cache/ diff --git a/composer.json b/composer.json index e59aaa177..8230047a2 100644 --- a/composer.json +++ b/composer.json @@ -11,14 +11,14 @@ } ], "require": { - "php": ">=5.3", + "php": "^8.4", "fakerphp/faker": "^1.19", "symfony/yaml": "^6.4 || ^7.0" }, "require-dev": { "doctrine/common": "^3.0", "symfony/property-access": "~5.0|~6.0", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^11.5" }, "autoload": { "psr-0": { "Nelmio\\Alice\\": "src/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c849a8a68..931e12d8e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,22 @@ - - + tests/ - - + + src - - + + diff --git a/tests/Nelmio/Alice/FixturesTest.php b/tests/Nelmio/Alice/FixturesTest.php index b6dbda9a9..52d506997 100644 --- a/tests/Nelmio/Alice/FixturesTest.php +++ b/tests/Nelmio/Alice/FixturesTest.php @@ -48,9 +48,9 @@ public function testLoadLoadsYamlFilesAndDoctrineORM() public function testThatNewLoaderIsCreatedForDifferingOptions() { - $om = $this->getMock('Doctrine\Persistence\ObjectManager'); + $om = $this->createMock('Doctrine\Persistence\ObjectManager'); $om->expects($this->any()) - ->method('find')->will($this->returnValue(new User())); + ->method('find')->willReturn(new User()); $optionsBatch = array( // default options @@ -191,14 +191,12 @@ public function testThatNewLoaderIsCreatedForDifferingOptions() public function testThatExceptionIsThrownForInvalidProvider() { - $om = $this->getMock('Doctrine\Persistence\ObjectManager'); + $om = $this->createMock('Doctrine\Persistence\ObjectManager'); $om->expects($this->any()) - ->method('find')->will($this->returnValue(new User())); + ->method('find')->willReturn(new User()); - $this->setExpectedException( - '\InvalidArgumentException', - 'The provider should be a string or an object, got array instead' - ); + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('The provider should be a string or an object, got array instead'); Fixtures::load( __DIR__.'/fixtures/complete.yml', @@ -270,12 +268,11 @@ public function testLoadLoadsPHPfiles() $this->assertEquals(42, $user->favoriteNumber); } - /** - * @expectedException \RuntimeException - */ public function testLoadWithLogger() { - $om = $this->getMock('Doctrine\Persistence\ObjectManager'); + $om = $this->createMock('Doctrine\Persistence\ObjectManager'); + + $this->expectException('\RuntimeException'); $objects = Fixtures::load(__DIR__.'/fixtures/basic.php', $om, array( 'logger' => 'not callable' @@ -316,7 +313,7 @@ public function testMakesOnlyOneFlushWithPersistOnce() protected function getDoctrineManagerMock($objects = null) { - $om = $this->getMock('Doctrine\Persistence\ObjectManager'); + $om = $this->createMock('Doctrine\Persistence\ObjectManager'); $om->expects($objects ? $this->exactly($objects) : $this->any()) ->method('persist'); @@ -325,7 +322,7 @@ protected function getDoctrineManagerMock($objects = null) ->method('flush'); $om->expects($this->once()) - ->method('find')->will($this->returnValue(new User())); + ->method('find')->willReturn(new User()); return $om; } diff --git a/tests/Nelmio/Alice/Loader/BaseTest.php b/tests/Nelmio/Alice/Loader/BaseTest.php index d340eafbf..b11751173 100644 --- a/tests/Nelmio/Alice/Loader/BaseTest.php +++ b/tests/Nelmio/Alice/Loader/BaseTest.php @@ -79,12 +79,11 @@ public function testGetReference() $this->assertSame($user, $this->loader->getReference('bob')); } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage Reference foo is not defined - */ public function testGetBadReference() { + $this->expectException('UnexpectedValueException'); + $this->expectExceptionMessage('Reference foo is not defined'); + $res = $this->loadData(array( self::USER => array( 'bob' => array(), @@ -245,12 +244,11 @@ public function testLoadParsesReferencesInFakerProviders() $this->assertEquals($res['bob'], $res['user']->username); } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage Property doesnotexist is not defined for reference user1 - */ public function testLoadParsesPropertyReferencesDoesNotExist() { + $this->expectException('UnexpectedValueException'); + $this->expectExceptionMessage('Property doesnotexist is not defined for reference user1'); + $res = $this->loadData(array( self::USER => array( 'user1' => array( @@ -337,12 +335,11 @@ public function testLoadParsesMultiReferencesWithProperty() } } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage Reference mask "user*" did not match any existing reference, make sure the object is created after its references - */ public function testLoadFailsMultiReferencesIfNoneMatch() { + $this->expectException('UnexpectedValueException'); + $this->expectExceptionMessage('Reference mask "user*" did not match any existing reference, make sure the object is created after its references'); + $usernames = range('a', 'z'); $data = array( self::GROUP => array( @@ -458,11 +455,11 @@ public function testLoadCoercesTimestampForConstructorDateTimeHints() public function testLoadFetchesScalarIdsForClassHints() { $owner = new User(); - $orm = $this->getMockBuilder('Nelmio\Alice\ORMInterface')->getMock(); + $orm = $this->createMock('Nelmio\Alice\ORMInterface'); $orm->expects($this->once()) ->method('find') ->with(self::USER, '42') - ->will($this->returnValue($owner)); + ->willReturn($owner); $loader = $this->createLoader(); $loader->setORM($orm); @@ -502,7 +499,7 @@ public function testLoadParsesFakerDataMultiple() )); $this->assertNotEquals(' ', $res['user0']->username); - $this->assertRegExp('{^[\w\']+ [\w\']+$}i', $res['user0']->username); + $this->assertMatchesRegularExpression('{^[\w\']+ [\w\']+$}i', $res['user0']->username); } public function testLoadParsesFakerDataWithArgs() @@ -561,15 +558,14 @@ public function testLoadParsesFakerDataWithLocale() ), )); - $this->assertRegExp('{^\d{3} \d{3} \d{3}$}', $res['user0']->username); + $this->assertMatchesRegularExpression('{^\d{3} \d{3} \d{3}$}', $res['user0']->username); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Unknown formatter "siren" - */ public function testLoadParsesFakerDataUsesDefaultLocale() { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Unknown format "siren"'); + $res = $this->loadData(array( self::USER => array( 'user0' => array( @@ -1020,12 +1016,11 @@ public function testMultipleInheritanceInInstance() $this->assertSame($this->loader->getReference('user')->username, 'my_very_long_name'); } - /** - * @expectedException \UnexpectedValueException - * @expectedExceptionMessage Template user_not_base is not defined - */ public function testInheritedObjectDoesntExist() { + $this->expectException('\UnexpectedValueException'); + $this->expectExceptionMessage('Template user_not_base is not defined'); + $res = $this->loadData(array( self::USER => array( 'user_base (template)' => array( @@ -1079,12 +1074,11 @@ public function testObjectsInheritProviders() $this->assertSame($this->loader->getReference('user2')->favoriteNumber, 42); } - /** - * @expectedException \UnexpectedValueException - * @expectedExceptionMessage Cannot use out of fixtures ranges - */ public function testCurrentProviderFailsOutOfRanges() { + $this->expectException('\UnexpectedValueException'); + $this->expectExceptionMessage('Cannot use out of fixtures ranges'); + $res = $this->loadData(array( self::USER => array( 'user1' => array( @@ -1094,12 +1088,11 @@ public function testCurrentProviderFailsOutOfRanges() )); } - /** - * @expectedException \UnexpectedValueException - * @expectedExceptionMessage Could not determine how to assign inexistent to a Nelmio\Alice\fixtures\User object - */ public function testArbitraryPropertyNamesFail() { + $this->expectException('\UnexpectedValueException'); + $this->expectExceptionMessage('Could not determine how to assign inexistent to a Nelmio\Alice\fixtures\User object'); + $res = $this->loadData(array( self::USER => array( 'user1' => array( @@ -1109,12 +1102,10 @@ public function testArbitraryPropertyNamesFail() )); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage - */ public function testLoadFailsOnConstructorsWithRequiredArgs() { + $this->expectException('RuntimeException'); + $res = $this->loadData(array( self::CONTACT => array( 'contact' => array( @@ -1186,11 +1177,9 @@ public function testLoadCallsStaticConstructorIfProvided() $this->assertSame('alice', $res['user']->username); } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage - */ public function testLoadFailsOnInvalidStaticConstructor() { + $this->expectException('UnexpectedValueException'); + $res = $this->loadData(array( self::USER => array( 'user' => array( @@ -1200,11 +1189,9 @@ public function testLoadFailsOnInvalidStaticConstructor() { )); } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage - */ public function testLoadFailsOnScalarStaticConstructorArgs() { + $this->expectException('UnexpectedValueException'); + $res = $this->loadData(array( self::USER => array( 'user' => array( @@ -1214,11 +1201,9 @@ public function testLoadFailsOnScalarStaticConstructorArgs() { )); } - /** - * @expectedException UnexpectedValueException - * @expectedExceptionMessage - */ public function testLoadFailsIfStaticMethodDoesntReturnAnInstance() { + $this->expectException('UnexpectedValueException'); + $res = $this->loadData(array( self::USER => array( 'user' => array( @@ -1327,11 +1312,10 @@ public function testGeneratedValuesAreUniqueAcrossAClass() $this->assertEquals($usernames, array_unique($usernames)); } - /** - * @expectedException \RuntimeException - */ public function testUniqueValuesException() { + $this->expectException('\RuntimeException'); + $loader = new Base("en_US", array(new FakerProvider)); $res = $loader->load(array( self::USER => array( @@ -1395,12 +1379,11 @@ public function testCustomSetFunction() $this->assertEquals('foo set by custom setter', $loader->getReference('user')->test_variable); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Setter customNonexistantSetter not found in object - */ public function testCustomNonexistantSetFunction() { + $this->expectException('\RuntimeException'); + $this->expectExceptionMessage('Setter customNonexistantSetter not found in object'); + $this->loadData( array( self::USER => array( diff --git a/tests/Nelmio/Alice/fixtures/User.php b/tests/Nelmio/Alice/fixtures/User.php index 7f3f688c4..8f4fbaceb 100644 --- a/tests/Nelmio/Alice/fixtures/User.php +++ b/tests/Nelmio/Alice/fixtures/User.php @@ -10,20 +10,21 @@ class User public $email; public $favoriteNumber; public $friends; + public $test_variable; - public function __construct($username = null, $email = null, \DateTime $birthDate = null) + public function __construct($username = null, $email = null, ?\DateTime $birthDate = null) { $this->username = $username; $this->email = $email; $this->birthDate = $birthDate; } - public static function create($username = null, $email = null, \DateTime $birthDate = null) + public static function create($username = null, $email = null, ?\DateTime $birthDate = null) { return new static($username, $email, $birthDate); } - public static function bogusCreate($username = null, $email = null, \DateTime $birthDate = null) + public static function bogusCreate($username = null, $email = null, ?\DateTime $birthDate = null) { }