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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2852,7 +2852,7 @@ public function handle(): void

### NoNullableServiceInConstructorRule

A constructor service dependency must not be nullable - a service is always provided by the container, so `?SomeService` only hides that it is really required. Nullable is allowed on an abstract class (a child fills the dependency) and on values that are not services: scalars, arrays, exceptions (`?Throwable $previous`) and date value objects.
A constructor service dependency must not be nullable - a service is always provided by the container, so `?SomeService` only hides that it is really required. Nullable is allowed on an abstract class (a child fills the dependency), on values that are not services (scalars, arrays, exceptions like `?Throwable $previous`, date value objects), and on data-holder classes in an `Entity`, `Event`, `DTO`, `Message`, `DAO`, `Token`, `Exception`, `Helper`, `ValueObject`, `Form\Type` or `Badge` namespace.

```yaml
rules:
Expand Down
35 changes: 34 additions & 1 deletion src/Rules/Symfony/NoNullableServiceInConstructorRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
* A service is always provided by the container, so "?SomeService $service" or "SomeService|null $service" only hides
* that it is really required. Nullable is allowed on an abstract class, whose optional dependency is filled by a child.
* A nullable scalar, array, exception ("$previous" is nullable by PHP convention) or date value object is left alone,
* as those are values, not services.
* as those are values, not services. Data-holder classes in an Entity, Event, DTO, Message, DAO, Token, Exception,
* Helper, ValueObject, Form\Type or Badge namespace are skipped whole - their constructors carry values, not services.
*
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoNullableServiceInConstructorRule\NoNullableServiceInConstructorRuleTest
*
Expand All @@ -38,6 +39,25 @@
{
public const string ERROR_MESSAGE = 'Constructor service "%s" of type "%s" is nullable. A service is always provided, make it non-nullable';

/**
* Data-holder namespaces whose constructors carry nullable values, not container services.
*
* @var string[]
*/
private const array SKIPPED_NAMESPACE_PARTS = [
'\\Entity\\',
'\\Event\\',
'\\DTO\\',
'\\Message\\',
'\\DAO\\',
'\\Token\\',
'\\Exception\\',
'\\Helper\\',
'\\ValueObject\\',
'\\Form\\Type\\',
'\\Badge\\',
];

public function __construct(
private ReflectionProvider $reflectionProvider,
) {
Expand Down Expand Up @@ -70,6 +90,11 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

// a data-holder namespace (event, DTO, token, exception, helper, form type, badge, ...) carries values, not services
if ($this->isSkippedNamespace($classReflection->getName())) {
return [];
}

$paramTypes = $this->resolveParamClassTypes($node, $scope);

$ruleErrors = [];
Expand Down Expand Up @@ -167,6 +192,14 @@ private function matchNullableServiceName(Identifier|Name|ComplexType|null $type
return null;
}

private function isSkippedNamespace(string $className): bool
{
return array_any(
self::SKIPPED_NAMESPACE_PARTS,
static fn (string $skippedNamespacePart): bool => str_contains($className, $skippedNamespacePart)
);
}

/**
* A nullable class type that is not really a service: an exception ("$previous") or a date value object.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoNullableServiceInConstructorRule\Fixture\Event;

use Symplify\PHPStanRules\Tests\Rules\Symfony\NoNullableServiceInConstructorRule\Source\SomeService;

final class SkipEventDataHolder
{
public function __construct(
private readonly ?SomeService $someService,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static function provideData(): Iterator
yield [__DIR__ . '/Fixture/SkipNullableException.php', []];
yield [__DIR__ . '/Fixture/SkipNullableDateTime.php', []];
yield [__DIR__ . '/Fixture/SkipAbstractClass.php', []];
yield [__DIR__ . '/Fixture/Event/SkipEventDataHolder.php', []];
yield [__DIR__ . '/Fixture/SkipDuplicateType.php', []];
yield [__DIR__ . '/Fixture/SkipAnonymousClass.php', []];
}
Expand Down
Loading