Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
['8.1', '8.2', '8.3']
['8.1', '8.2', '8.3', '8.4']
2 changes: 1 addition & 1 deletion .github/workflows/composer-require-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1', '8.2', '8.3']
['8.1', '8.2', '8.3', '8.4']
2 changes: 1 addition & 1 deletion .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.3']
['8.4']
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
2 changes: 1 addition & 1 deletion .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1', '8.2', '8.3']
['8.1', '8.2', '8.3', '8.4']
1 change: 1 addition & 0 deletions .github/workflows/yiisoft-di.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"

steps:
- name: Checkout Yii Definitions
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/yiisoft-factory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"

steps:
- name: Checkout Yii Definitions
Expand Down
10 changes: 6 additions & 4 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;

$finder = (new Finder())->in([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
$finder = (new Finder())
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->exclude('Php8_4/');

return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

## 3.3.2 under development

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

## 3.3.1 December 16, 2024

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
}
],
"require": {
"php": "^8.1",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"psr/container": "^1.0 || ^2.0"
},
"require-dev": {
Expand All @@ -37,7 +37,7 @@
"roave/infection-static-analysis-plugin": "^1.35",
"spatie/phpunit-watcher": "^1.24",
"vimeo/psalm": "^5.26.1 || ^6.7.1",
"yiisoft/test-support": "^3.0"
"yiisoft/test-support": "^3.0.1"
},
"autoload": {
"psr-4": {
Expand All @@ -58,7 +58,7 @@
}
},
"scripts": {
"php-cs-fixer": "php-cs-fixer fix",
"php-cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix",
"rector": "rector",
"test": "phpunit --testdox --no-interaction",
"test-watch": "phpunit-watcher watch"
Expand Down
7 changes: 6 additions & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
}
},
"mutators": {
"@default": true
"@default": true,
"LessThan": {
"ignoreSourceCodeByRegex": [
".*\\(PHP_VERSION_ID .*"
]
}
}
}
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<testsuite name="Yii Definitions tests">
<directory>./tests/Unit</directory>
<directory phpVersion="8.2" phpVersionOperator=">=">./tests/Php8_2</directory>
<directory phpVersion="8.4" phpVersionOperator=">=">./tests/Php8_4</directory>
</testsuite>
</testsuites>

Expand Down
2 changes: 1 addition & 1 deletion src/Exception/NotInstantiableClassException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
final class NotInstantiableClassException extends NotInstantiableException
{
public function __construct(string $class, string $message = null, int $code = 0, Exception $previous = null)
public function __construct(string $class, ?string $message = null, int $code = 0, ?Exception $previous = null)
{
if ($message === null) {
$message = "Can not instantiate $class.";
Expand Down
28 changes: 26 additions & 2 deletions src/Helpers/DefinitionValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
use Yiisoft\Definitions\ArrayDefinition;
use Yiisoft\Definitions\Contract\DefinitionInterface;
use Yiisoft\Definitions\Contract\ReferenceInterface;
use Yiisoft\Definitions\Exception\InvalidConfigException;

use function count;
use function in_array;
use function is_array;
use function is_callable;
Expand Down Expand Up @@ -85,7 +87,7 @@ public static function validateArrayDefinition(array $definition, ?string $id =
}
$classPublicProperties = [];
foreach ($classReflection->getProperties() as $reflectionProperty) {
if ($reflectionProperty->isPublic()) {
if (self::isPublicWritableProperty($reflectionProperty)) {
$classPublicProperties[] = $reflectionProperty->getName();
}
}
Expand Down Expand Up @@ -261,7 +263,7 @@ private static function validateProperty(
} elseif (!in_array($parsedKey, $classPublicProperties, true)) {
throw new InvalidConfigException(
sprintf(
'Invalid definition: property "%s" must be public.',
'Invalid definition: property "%s" must be public and writable.',
$className . '::' . $key,
),
);
Expand Down Expand Up @@ -327,4 +329,26 @@ private static function validateString(mixed $class): void
throw new InvalidConfigException('Invalid definition: class name must be a non-empty string.');
}
}

private static function isPublicWritableProperty(ReflectionProperty $property): bool
{
if (!$property->isPublic()) {
return false;
}

if ($property->isReadOnly()) {
return false;
}

if (PHP_VERSION_ID < 80400) {
return true;
}

$modifiers = $property->getModifiers();

/**
* @psalm-suppress UndefinedConstant, MixedOperand Needs for PHP 8.3 or lower
*/
return ($modifiers & (ReflectionProperty::IS_PRIVATE_SET | ReflectionProperty::IS_PROTECTED_SET)) === 0;
}
}
5 changes: 1 addition & 4 deletions src/ParameterDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,7 @@ private function getCallable(): string
if ($class !== null) {
$callable[] = $class->getName();
}
$callable[] = $this->parameter
->getDeclaringFunction()
->getName() .
'()';
$callable[] = $this->parameter->getDeclaringFunction()->getName() . '()';

return implode('::', $callable);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Php8_2/ParameterDefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function testResolveUnionTypeWithIntersectionType(): void
]);

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

$result = $definition->resolve($container);
Expand Down
40 changes: 40 additions & 0 deletions tests/Php8_4/AsymmetricVisibility/DefinitionValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility;

use PHPUnit\Framework\TestCase;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Helpers\DefinitionValidator;

final class DefinitionValidatorTest extends TestCase
{
public function testPrivateSet(): void
{
$definition = [
'class' => PublicGet::class,
'$privateVar' => 'test',
];

$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage(
'Invalid definition: property "Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility\PublicGet::$privateVar" must be public and writable.',
);
DefinitionValidator::validate($definition);
}

public function testProtectedSet(): void
{
$definition = [
'class' => PublicGet::class,
'$protectedVar' => 'test',
];

$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage(
'Invalid definition: property "Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility\PublicGet::$protectedVar" must be public and writable.',
);
DefinitionValidator::validate($definition);
}
}
11 changes: 11 additions & 0 deletions tests/Php8_4/AsymmetricVisibility/PublicGet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Definitions\Tests\Php8_4\AsymmetricVisibility;

final class PublicGet
{
public private(set) string $privateVar = '';
public protected(set) string $protectedVar = '';
}
10 changes: 0 additions & 10 deletions tests/Support/OptionalConcreteDependency.php

This file was deleted.

10 changes: 0 additions & 10 deletions tests/Support/OptionalInterfaceDependency.php

This file was deleted.

10 changes: 10 additions & 0 deletions tests/Support/ReadonlyProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Definitions\Tests\Support;

final class ReadonlyProperty
{
public readonly string $var;
}
5 changes: 2 additions & 3 deletions tests/Support/UnionTypeWithIntersectionTypeDependency.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
final class UnionTypeWithIntersectionTypeDependency
{
public function __construct(
public Bike|(GearBox&stdClass)|Chair $dependency
) {
}
public Bike|(GearBox&stdClass)|Chair $dependency,
) {}
}
21 changes: 0 additions & 21 deletions tests/Unit/Helpers/DefinitionExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
use Yiisoft\Definitions\Tests\Support\GearBox;
use Yiisoft\Definitions\Tests\Support\NullableConcreteDependency;
use Yiisoft\Definitions\Tests\Support\NullableInterfaceDependency;
use Yiisoft\Definitions\Tests\Support\OptionalConcreteDependency;
use Yiisoft\Definitions\Tests\Support\OptionalInterfaceDependency;
use Yiisoft\Definitions\Tests\Support\NullableOptionalConcreteDependency;
use Yiisoft\Definitions\Tests\Support\NullableOptionalInterfaceDependency;
use Yiisoft\Definitions\Tests\Support\RedChair;
Expand Down Expand Up @@ -102,15 +100,6 @@ public function testResolveGearBoxConstructor(): void
$this->assertEquals(5, $dependencies['maxGear']->resolve($container));
}

public function testOptionalInterfaceDependency(): void
{
$container = new SimpleContainer();
/** @var DefinitionInterface[] $dependencies */
$dependencies = DefinitionExtractor::fromClassName(OptionalInterfaceDependency::class);
$this->assertCount(1, $dependencies);
$this->assertEquals(null, $dependencies['engine']->resolve($container));
}

public function testNullableInterfaceDependency(): void
{
$container = new SimpleContainer();
Expand All @@ -121,15 +110,6 @@ public function testNullableInterfaceDependency(): void
$dependencies['engine']->resolve($container);
}

public function testOptionalConcreteDependency(): void
{
$container = new SimpleContainer();
/** @var DefinitionInterface[] $dependencies */
$dependencies = DefinitionExtractor::fromClassName(OptionalConcreteDependency::class);
$this->assertCount(1, $dependencies);
$this->assertEquals(null, $dependencies['car']->resolve($container));
}

public function testNullableConcreteDependency(): void
{
$container = new SimpleContainer();
Expand All @@ -152,7 +132,6 @@ public function testNullableOptionalConcreteDependency(): void
public function testNullableOptionalInterfaceDependency(): void
{
$container = new SimpleContainer();
/** @var DefinitionInterface[] $dependencies */
$dependencies = DefinitionExtractor::fromClassName(NullableOptionalInterfaceDependency::class);
$this->assertCount(1, $dependencies);
$this->assertEquals(null, $dependencies['engine']->resolve($container));
Expand Down
19 changes: 17 additions & 2 deletions tests/Unit/Helpers/DefinitionValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Yiisoft\Definitions\Tests\Support\GearBox;
use Yiisoft\Definitions\Tests\Support\MagicCall;
use Yiisoft\Definitions\Tests\Support\Phone;
use Yiisoft\Definitions\Tests\Support\ReadonlyProperty;
use Yiisoft\Definitions\Tests\Support\Recorder;
use Yiisoft\Definitions\Tests\Support\UTF8User;
use Yiisoft\Definitions\ValueDefinition;
Expand Down Expand Up @@ -73,15 +74,15 @@ public static function dataInvalidProperty(): array
$object1::class,
'$invisible',
sprintf(
'Invalid definition: property "%s" must be public.',
'Invalid definition: property "%s" must be public and writable.',
$object1::class . '::$invisible',
),
],
[
UTF8User::class,
'$имя',
sprintf(
'Invalid definition: property "%s" must be public.',
'Invalid definition: property "%s" must be public and writable.',
UTF8User::class . '::$имя',
),
],
Expand Down Expand Up @@ -367,4 +368,18 @@ public function testIncorrectMethodName(array $config, string $message): void
$this->expectExceptionMessage($message);
DefinitionValidator::validate($config);
}

public function testReadonlyProperty(): void
{
$definition = [
'class' => ReadonlyProperty::class,
'$var' => 'test',
];

$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage(
'Invalid definition: property "Yiisoft\Definitions\Tests\Support\ReadonlyProperty::$var" must be public and writable.',
);
DefinitionValidator::validate($definition);
}
}
Loading