Skip to content

Commit db848c6

Browse files
vjikTigrov
andauthored
PHP 8.4 support (#105)
Co-authored-by: Sergei Tigrov <[email protected]>
1 parent 6012aae commit db848c6

24 files changed

+176
-86
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ jobs:
3030
os: >-
3131
['ubuntu-latest', 'windows-latest']
3232
php: >-
33-
['8.1', '8.2', '8.3']
33+
['8.1', '8.2', '8.3', '8.4']

.github/workflows/composer-require-checker.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232
os: >-
3333
['ubuntu-latest']
3434
php: >-
35-
['8.1', '8.2', '8.3']
35+
['8.1', '8.2', '8.3', '8.4']

.github/workflows/mutation.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ jobs:
2929
os: >-
3030
['ubuntu-latest']
3131
php: >-
32-
['8.3']
32+
['8.4']
3333
secrets:
3434
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

.github/workflows/static.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ jobs:
3030
os: >-
3131
['ubuntu-latest']
3232
php: >-
33-
['8.1', '8.2', '8.3']
33+
['8.1', '8.2', '8.3', '8.4']

.github/workflows/yiisoft-di.yml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
- "8.1"
3838
- "8.2"
3939
- "8.3"
40+
- "8.4"
4041

4142
steps:
4243
- name: Checkout Yii Definitions

.github/workflows/yiisoft-factory.yml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
- "8.1"
3838
- "8.2"
3939
- "8.3"
40+
- "8.4"
4041

4142
steps:
4243
- name: Checkout Yii Definitions

.php-cs-fixer.dist.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
use PhpCsFixer\Finder;
77
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
88

9-
$finder = (new Finder())->in([
10-
__DIR__ . '/src',
11-
__DIR__ . '/tests',
12-
]);
9+
$finder = (new Finder())
10+
->in([
11+
__DIR__ . '/src',
12+
__DIR__ . '/tests',
13+
])
14+
->exclude('Php8_4/');
1315

1416
return (new Config())
1517
->setParallelConfig(ParallelConfigFactory::detect())

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
## 3.3.2 under development
44

5+
- Chg #105: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
56
- Chg #106: Bump minimal required PHP version to 8.1 (@vjik)
67
- Enh #106: Minor performance optimization: use FQN for PHP functions, remove unnecessary conditions (@vjik)
78
- Enh #106: Mark readonly properties (@vjik)
9+
- Bug #105: Explicitly mark nullable parameters (@vjik)
10+
- Enh #105: Improve definition validation for readonly properties and properties with asymmetric visibility (@vjik)
811

912
## 3.3.1 December 16, 2024
1013

composer.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727
],
2828
"require": {
29-
"php": "^8.1",
29+
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
3030
"psr/container": "^1.0 || ^2.0"
3131
},
3232
"require-dev": {
@@ -37,7 +37,7 @@
3737
"roave/infection-static-analysis-plugin": "^1.35",
3838
"spatie/phpunit-watcher": "^1.24",
3939
"vimeo/psalm": "^5.26.1 || ^6.7.1",
40-
"yiisoft/test-support": "^3.0"
40+
"yiisoft/test-support": "^3.0.1"
4141
},
4242
"autoload": {
4343
"psr-4": {
@@ -58,7 +58,7 @@
5858
}
5959
},
6060
"scripts": {
61-
"php-cs-fixer": "php-cs-fixer fix",
61+
"php-cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix",
6262
"rector": "rector",
6363
"test": "phpunit --testdox --no-interaction",
6464
"test-watch": "phpunit-watcher watch"

infection.json.dist

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
}
1212
},
1313
"mutators": {
14-
"@default": true
14+
"@default": true,
15+
"LessThan": {
16+
"ignoreSourceCodeByRegex": [
17+
".*\\(PHP_VERSION_ID .*"
18+
]
19+
}
1520
}
1621
}

phpunit.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<testsuite name="Yii Definitions tests">
2222
<directory>./tests/Unit</directory>
2323
<directory phpVersion="8.2" phpVersionOperator=">=">./tests/Php8_2</directory>
24+
<directory phpVersion="8.4" phpVersionOperator=">=">./tests/Php8_4</directory>
2425
</testsuite>
2526
</testsuites>
2627

src/Exception/NotInstantiableClassException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
final class NotInstantiableClassException extends NotInstantiableException
1313
{
14-
public function __construct(string $class, string $message = null, int $code = 0, Exception $previous = null)
14+
public function __construct(string $class, ?string $message = null, int $code = 0, ?Exception $previous = null)
1515
{
1616
if ($message === null) {
1717
$message = "Can not instantiate $class.";

src/Helpers/DefinitionValidator.php

+26-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use ReflectionClass;
88
use ReflectionException;
9+
use ReflectionProperty;
910
use Yiisoft\Definitions\ArrayDefinition;
1011
use Yiisoft\Definitions\Contract\DefinitionInterface;
1112
use Yiisoft\Definitions\Contract\ReferenceInterface;
1213
use Yiisoft\Definitions\Exception\InvalidConfigException;
1314

15+
use function count;
1416
use function in_array;
1517
use function is_array;
1618
use function is_callable;
@@ -85,7 +87,7 @@ public static function validateArrayDefinition(array $definition, ?string $id =
8587
}
8688
$classPublicProperties = [];
8789
foreach ($classReflection->getProperties() as $reflectionProperty) {
88-
if ($reflectionProperty->isPublic()) {
90+
if (self::isPublicWritableProperty($reflectionProperty)) {
8991
$classPublicProperties[] = $reflectionProperty->getName();
9092
}
9193
}
@@ -261,7 +263,7 @@ private static function validateProperty(
261263
} elseif (!in_array($parsedKey, $classPublicProperties, true)) {
262264
throw new InvalidConfigException(
263265
sprintf(
264-
'Invalid definition: property "%s" must be public.',
266+
'Invalid definition: property "%s" must be public and writable.',
265267
$className . '::' . $key,
266268
),
267269
);
@@ -327,4 +329,26 @@ private static function validateString(mixed $class): void
327329
throw new InvalidConfigException('Invalid definition: class name must be a non-empty string.');
328330
}
329331
}
332+
333+
private static function isPublicWritableProperty(ReflectionProperty $property): bool
334+
{
335+
if (!$property->isPublic()) {
336+
return false;
337+
}
338+
339+
if ($property->isReadOnly()) {
340+
return false;
341+
}
342+
343+
if (PHP_VERSION_ID < 80400) {
344+
return true;
345+
}
346+
347+
$modifiers = $property->getModifiers();
348+
349+
/**
350+
* @psalm-suppress UndefinedConstant, MixedOperand Needs for PHP 8.3 or lower
351+
*/
352+
return ($modifiers & (ReflectionProperty::IS_PRIVATE_SET | ReflectionProperty::IS_PROTECTED_SET)) === 0;
353+
}
330354
}

src/ParameterDefinition.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ private function getCallable(): string
216216
if ($class !== null) {
217217
$callable[] = $class->getName();
218218
}
219-
$callable[] = $this->parameter
220-
->getDeclaringFunction()
221-
->getName() .
222-
'()';
219+
$callable[] = $this->parameter->getDeclaringFunction()->getName() . '()';
223220

224221
return implode('::', $callable);
225222
}

tests/Php8_2/ParameterDefinitionTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function testResolveUnionTypeWithIntersectionType(): void
2222
]);
2323

2424
$definition = new ParameterDefinition(
25-
$this->getFirstParameter(fn (Bike|(GearBox&stdClass)|Chair $class) => true)
25+
$this->getFirstParameter(fn(Bike|(GearBox&stdClass)|Chair $class) => true),
2626
);
2727

2828
$result = $definition->resolve($container);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Yiisoft\Definitions\Exception\InvalidConfigException;
9+
use Yiisoft\Definitions\Helpers\DefinitionValidator;
10+
11+
final class DefinitionValidatorTest extends TestCase
12+
{
13+
public function testPrivateSet(): void
14+
{
15+
$definition = [
16+
'class' => PublicGet::class,
17+
'$privateVar' => 'test',
18+
];
19+
20+
$this->expectException(InvalidConfigException::class);
21+
$this->expectExceptionMessage(
22+
'Invalid definition: property "Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility\PublicGet::$privateVar" must be public and writable.',
23+
);
24+
DefinitionValidator::validate($definition);
25+
}
26+
27+
public function testProtectedSet(): void
28+
{
29+
$definition = [
30+
'class' => PublicGet::class,
31+
'$protectedVar' => 'test',
32+
];
33+
34+
$this->expectException(InvalidConfigException::class);
35+
$this->expectExceptionMessage(
36+
'Invalid definition: property "Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility\PublicGet::$protectedVar" must be public and writable.',
37+
);
38+
DefinitionValidator::validate($definition);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility;
6+
7+
final class PublicGet
8+
{
9+
public private(set) string $privateVar = '';
10+
public protected(set) string $protectedVar = '';
11+
}

tests/Support/OptionalConcreteDependency.php

-10
This file was deleted.

tests/Support/OptionalInterfaceDependency.php

-10
This file was deleted.

tests/Support/ReadonlyProperty.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Definitions\Tests\Support;
6+
7+
final class ReadonlyProperty
8+
{
9+
public readonly string $var;
10+
}

tests/Support/UnionTypeWithIntersectionTypeDependency.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
final class UnionTypeWithIntersectionTypeDependency
88
{
99
public function __construct(
10-
public Bike|(GearBox&stdClass)|Chair $dependency
11-
) {
12-
}
10+
public Bike|(GearBox&stdClass)|Chair $dependency,
11+
) {}
1312
}

tests/Unit/Helpers/DefinitionExtractorTest.php

-21
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
use Yiisoft\Definitions\Tests\Support\GearBox;
2121
use Yiisoft\Definitions\Tests\Support\NullableConcreteDependency;
2222
use Yiisoft\Definitions\Tests\Support\NullableInterfaceDependency;
23-
use Yiisoft\Definitions\Tests\Support\OptionalConcreteDependency;
24-
use Yiisoft\Definitions\Tests\Support\OptionalInterfaceDependency;
2523
use Yiisoft\Definitions\Tests\Support\NullableOptionalConcreteDependency;
2624
use Yiisoft\Definitions\Tests\Support\NullableOptionalInterfaceDependency;
2725
use Yiisoft\Definitions\Tests\Support\RedChair;
@@ -102,15 +100,6 @@ public function testResolveGearBoxConstructor(): void
102100
$this->assertEquals(5, $dependencies['maxGear']->resolve($container));
103101
}
104102

105-
public function testOptionalInterfaceDependency(): void
106-
{
107-
$container = new SimpleContainer();
108-
/** @var DefinitionInterface[] $dependencies */
109-
$dependencies = DefinitionExtractor::fromClassName(OptionalInterfaceDependency::class);
110-
$this->assertCount(1, $dependencies);
111-
$this->assertEquals(null, $dependencies['engine']->resolve($container));
112-
}
113-
114103
public function testNullableInterfaceDependency(): void
115104
{
116105
$container = new SimpleContainer();
@@ -121,15 +110,6 @@ public function testNullableInterfaceDependency(): void
121110
$dependencies['engine']->resolve($container);
122111
}
123112

124-
public function testOptionalConcreteDependency(): void
125-
{
126-
$container = new SimpleContainer();
127-
/** @var DefinitionInterface[] $dependencies */
128-
$dependencies = DefinitionExtractor::fromClassName(OptionalConcreteDependency::class);
129-
$this->assertCount(1, $dependencies);
130-
$this->assertEquals(null, $dependencies['car']->resolve($container));
131-
}
132-
133113
public function testNullableConcreteDependency(): void
134114
{
135115
$container = new SimpleContainer();
@@ -152,7 +132,6 @@ public function testNullableOptionalConcreteDependency(): void
152132
public function testNullableOptionalInterfaceDependency(): void
153133
{
154134
$container = new SimpleContainer();
155-
/** @var DefinitionInterface[] $dependencies */
156135
$dependencies = DefinitionExtractor::fromClassName(NullableOptionalInterfaceDependency::class);
157136
$this->assertCount(1, $dependencies);
158137
$this->assertEquals(null, $dependencies['engine']->resolve($container));

tests/Unit/Helpers/DefinitionValidatorTest.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Yiisoft\Definitions\Tests\Support\GearBox;
1818
use Yiisoft\Definitions\Tests\Support\MagicCall;
1919
use Yiisoft\Definitions\Tests\Support\Phone;
20+
use Yiisoft\Definitions\Tests\Support\ReadonlyProperty;
2021
use Yiisoft\Definitions\Tests\Support\Recorder;
2122
use Yiisoft\Definitions\Tests\Support\UTF8User;
2223
use Yiisoft\Definitions\ValueDefinition;
@@ -73,15 +74,15 @@ public static function dataInvalidProperty(): array
7374
$object1::class,
7475
'$invisible',
7576
sprintf(
76-
'Invalid definition: property "%s" must be public.',
77+
'Invalid definition: property "%s" must be public and writable.',
7778
$object1::class . '::$invisible',
7879
),
7980
],
8081
[
8182
UTF8User::class,
8283
'$имя',
8384
sprintf(
84-
'Invalid definition: property "%s" must be public.',
85+
'Invalid definition: property "%s" must be public and writable.',
8586
UTF8User::class . '::$имя',
8687
),
8788
],
@@ -367,4 +368,18 @@ public function testIncorrectMethodName(array $config, string $message): void
367368
$this->expectExceptionMessage($message);
368369
DefinitionValidator::validate($config);
369370
}
371+
372+
public function testReadonlyProperty(): void
373+
{
374+
$definition = [
375+
'class' => ReadonlyProperty::class,
376+
'$var' => 'test',
377+
];
378+
379+
$this->expectException(InvalidConfigException::class);
380+
$this->expectExceptionMessage(
381+
'Invalid definition: property "Yiisoft\Definitions\Tests\Support\ReadonlyProperty::$var" must be public and writable.',
382+
);
383+
DefinitionValidator::validate($definition);
384+
}
370385
}

0 commit comments

Comments
 (0)