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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,12 @@ $rules[] = Rule::allClasses()
```php
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new NotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid'], true))
->should(new NotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid']))
->because('we want to protect our domain except for Ramsey\Uuid');
```

Note: PHP core classes (e.g., `DateTime`, `Exception`, `PDO`) are automatically excluded from dependency checks.

### Not have a name matching a pattern

```php
Expand Down
19 changes: 19 additions & 0 deletions src/Analyzer/ClassDescriptionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public function addInterface(string $FQCN, int $line): self

public function addDependency(ClassDependency $cd): self
{
if ($this->isPhpCoreClass($cd)) {
return $this;
}

$this->classDependencies[] = $cd;

return $this;
Expand Down Expand Up @@ -169,4 +173,19 @@ public function build(): ClassDescription
$this->filePath
);
}

private function isPhpCoreClass(ClassDependency $dependency): bool
{
$fqcn = $dependency->getFQCN();

try {
/** @var class-string $className */
$className = $fqcn->toString();
$reflection = new \ReflectionClass($className);

return $reflection->isInternal();
} catch (\ReflectionException $e) {
return false;
}
}
}
8 changes: 2 additions & 6 deletions src/Expression/ForClasses/DependsOnlyOnTheseNamespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,12 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str

/** @var ClassDependency $dependency */
foreach ($dependencies as $dependency) {
if ('' === $dependency->getFQCN()->namespace()) {
continue; // skip root namespace
}

if ($theClass->namespaceMatches($dependency->getFQCN()->namespace())) {
continue; // skip classes in the same namespace
continue;
}

if ($dependency->matchesOneOf(...$this->exclude)) {
continue; // skip excluded namespaces
continue;
}

if (!$dependency->matchesOneOf(...$this->namespaces)) {
Expand Down
6 changes: 1 addition & 5 deletions src/Expression/ForClasses/NotDependsOnTheseNamespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,8 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str

/** @var ClassDependency $dependency */
foreach ($dependencies as $dependency) {
if ('' === $dependency->getFQCN()->namespace()) {
continue; // skip root namespace
}

if ($dependency->matchesOneOf(...$this->exclude)) {
continue; // skip excluded namespaces
continue;
}

if ($dependency->matchesOneOf(...$this->namespaces)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ class NotHaveDependencyOutsideNamespace implements Expression
/** @var array<string> */
private array $externalDependenciesToExclude;

private bool $excludeCoreNamespace;

public function __construct(string $namespace, array $externalDependenciesToExclude = [], bool $excludeCoreNamespace = false)
public function __construct(string $namespace, array $externalDependenciesToExclude = [])
{
$this->namespace = $namespace;
$this->externalDependenciesToExclude = $externalDependenciesToExclude;
$this->excludeCoreNamespace = $excludeCoreNamespace;
}

public function describe(ClassDescription $theClass, string $because): Description
Expand All @@ -49,10 +46,6 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str
continue;
}

if ($this->excludeCoreNamespace && '' === $externalDep->getFQCN()->namespace()) {
continue;
}

$violation = Violation::createWithErrorLine(
$theClass->getFQCN(),
ViolationMessage::withDescription(
Expand Down
84 changes: 84 additions & 0 deletions tests/Unit/Analyzer/ClassDescriptionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,88 @@ public function test_it_should_create_not_trait(): void
self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertFalse($classDescription->isTrait());
}

public function test_it_should_filter_out_php_core_classes(): void
{
$FQCN = 'MyClass';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addDependency(new ClassDependency('DateTime', 10))
->addDependency(new ClassDependency('Exception', 15))
->addDependency(new ClassDependency('PDO', 20))
->build();

self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertCount(0, $classDescription->getDependencies());
}

public function test_it_should_not_filter_user_defined_classes_in_root_namespace(): void
{
$FQCN = 'MyClass';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addDependency(new ClassDependency('NonExistentUserClass', 10))
->build();

self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertCount(1, $classDescription->getDependencies());
self::assertEquals('NonExistentUserClass', $classDescription->getDependencies()[0]->getFQCN()->toString());
}

public function test_it_should_not_filter_user_defined_classes_with_namespace(): void
{
$FQCN = 'MyClass';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addDependency(new ClassDependency('Vendor\Package\SomeClass', 10))
->addDependency(new ClassDependency('App\Domain\Entity', 15))
->build();

self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertCount(2, $classDescription->getDependencies());
}

public function test_it_should_filter_mixed_dependencies_correctly(): void
{
$FQCN = 'MyClass';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addDependency(new ClassDependency('DateTime', 10))
->addDependency(new ClassDependency('Vendor\Package\SomeClass', 15))
->addDependency(new ClassDependency('Exception', 20))
->addDependency(new ClassDependency('NonExistentUserClass', 25))
->addDependency(new ClassDependency('PDO', 30))
->build();

self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertCount(2, $classDescription->getDependencies());

$dependencies = $classDescription->getDependencies();
self::assertEquals('Vendor\Package\SomeClass', $dependencies[0]->getFQCN()->toString());
self::assertEquals('NonExistentUserClass', $dependencies[1]->getFQCN()->toString());
}

public function test_it_should_filter_internal_classes_with_namespaces(): void
{
$FQCN = 'MyClass';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addDependency(new ClassDependency('ReflectionClass', 10))
->addDependency(new ClassDependency('App\MyClass', 15))
->build();

self::assertInstanceOf(ClassDescription::class, $classDescription);
self::assertCount(1, $classDescription->getDependencies());
self::assertEquals('App\MyClass', $classDescription->getDependencies()[0]->getFQCN()->toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,13 @@ public function test_it_should_return_false_if_depends_on_namespace(): void
->setClassName('HappyIsland')
->addDependency(new ClassDependency('myNamespace', 100))
->addDependency(new ClassDependency('another\class', 200))
->addDependency(new ClassDependency('\DateTime', 300))
->build();

$because = 'we want to add this rule for our software';
$violations = new Violations();
$notHaveDependencyOutsideNamespace->evaluate($classDescription, $violations, $because);

self::assertEquals(2, $violations->count());
self::assertEquals(1, $violations->count());
}

public function test_it_should_not_return_violation_error_if_dependency_excluded(): void
Expand All @@ -84,20 +83,20 @@ public function test_it_should_not_return_violation_error_if_dependency_excluded
self::assertEquals(0, $violations->count());
}

public function test_it_should_not_return_violation_error_if_core_dependency_excluded(): void
public function test_it_should_automatically_exclude_php_core_classes(): void
{
$notHaveDependencyOutsideNamespace = new NotHaveDependencyOutsideNamespace('myNamespace', [], true);
$notHaveDependencyOutsideNamespace = new NotHaveDependencyOutsideNamespace('myNamespace');

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName('HappyIsland')
->addDependency(new ClassDependency('\DateTime', 100))
->addDependency(new ClassDependency('another\class', 100))
->build();

$because = 'we want to add this rule for our software';
$violations = new Violations();
$notHaveDependencyOutsideNamespace->evaluate($classDescription, $violations, $because);

self::assertEquals(0, $violations->count());
self::assertEquals(1, $violations->count());
}
}