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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,52 @@ $rules[] = Rule::allClasses()

You can use wildcards or the exact name of a class.

### Combining multiple conditions with `andThat()`

By default, `that()` selects all classes matching a single expression. When you need to narrow the selection further — applying the `should()` check only to classes that satisfy **all** conditions — you can chain one or more `andThat()` calls:

```php
// Only concrete domain events (non-abstract, named *Event) must be final
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->andThat(new HaveNameMatching('*Event'))
->andThat(new IsNotAbstract())
->should(new IsFinal())
->because('concrete domain events must be immutable value objects');
```

A class is checked against `should()` only if it satisfies **every** `that()` / `andThat()` condition. A class that matches the first condition but not the second is silently skipped — no violation is raised.

`andThat()` can be combined with `except()`:

```php
$rules[] = Rule::allClasses()
->except('App\Controller\LegacyController')
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->andThat(new HaveAttribute('Symfony\Component\HttpKernel\Attribute\AsController'))
->should(new IsFinal())
->because('active controllers must be final');
```

You can also reuse a partially-built rule to create independent branches:

```php
$domainClasses = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'));

$rules[] = $domainClasses
->andThat(new HaveNameMatching('*Event'))
->should(new IsFinal())
->because('domain events must be final');

$rules[] = $domainClasses
->andThat(new HaveNameMatching('*Service'))
->should(new IsNotAbstract())
->because('domain services must be concrete');
```

Each branch is independent — modifying one does not affect the other.

## Optional parameters and options
You can add parameters when you launch the tool. At the moment you can add these parameters and options:
* `-v` : with this option you launch Arkitect with the verbose mode to see every parsed file
Expand Down
197 changes: 197 additions & 0 deletions tests/Integration/AndThatFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Integration\PHPUnit;

use Arkitect\Expression\ForClasses\HaveAttribute;
use Arkitect\Expression\ForClasses\HaveNameMatching;
use Arkitect\Expression\ForClasses\IsFinal;
use Arkitect\Expression\ForClasses\IsNotAbstract;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;
use Arkitect\Tests\Utils\TestRunner;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;

final class AndThatFilterTest extends TestCase
{
public function test_only_classes_matching_both_namespace_and_naming_are_checked(): void
{
// Rule: classes in App\Domain AND named *Event must be final.
//
// UserCreatedEvent → matches both → not final → 1 violation
// AbstractEvent → matches namespace but IS abstract, not matching IsNotAbstract if used;
// here it matches both conditions but satisfies IsFinal → 0 violations
// OrderService → matches namespace, does NOT match *Event → not checked → 0 violations
// InfrastructureEvent → NOT in App\Domain → not checked → 0 violations
$dir = vfsStream::setup('root', null, $this->createNamespaceAndNamingStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->andThat(new HaveNameMatching('*Event'))
->should(new IsFinal())
->because('domain events must be final');

$runner->run($dir, $rule);

self::assertCount(1, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());
self::assertEquals('App\Domain\UserCreatedEvent', $runner->getViolations()->get(0)->getFqcn());
}

public function test_classes_matching_namespace_and_attribute_but_failing_should_produce_violation(): void
{
// Rule: classes in App\Controller AND having #[AsController] must be final.
//
// ProductsController (#[AsController], not final) → matches both → violation
// LegacyController (#[AsController], final) → matches both → no violation
// UtilityHelper (no attribute) → fails andThat → not checked
$dir = vfsStream::setup('root', null, $this->createNamespaceAndAttributeStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->andThat(new HaveAttribute('AsController'))
->should(new IsFinal())
->because('active controllers must be final');

$runner->run($dir, $rule);

self::assertCount(1, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());
self::assertEquals('App\Controller\ProductsController', $runner->getViolations()->get(0)->getFqcn());
}

public function test_three_chained_and_that_conditions_all_must_match(): void
{
// Rule: classes in App\Domain AND named *Event AND non-abstract must be final.
//
// UserCreatedEvent → matches all three → not final → violation
// AbstractBaseEvent → matches namespace + *Event but IS abstract → third fails → not checked
// OrderService → matches namespace + IsNotAbstract but not *Event → not checked
$dir = vfsStream::setup('root', null, $this->createThreeConditionStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->andThat(new HaveNameMatching('*Event'))
->andThat(new IsNotAbstract())
->should(new IsFinal())
->because('concrete domain events must be final');

$runner->run($dir, $rule);

self::assertCount(1, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());
self::assertEquals('App\Domain\UserCreatedEvent', $runner->getViolations()->get(0)->getFqcn());
}

public function test_except_exclusion_is_respected_with_and_that(): void
{
// Same rule as above, but UserCreatedEvent is excluded via except().
// No violations expected.
$dir = vfsStream::setup('root', null, $this->createNamespaceAndNamingStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->except('App\Domain\UserCreatedEvent')
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->andThat(new HaveNameMatching('*Event'))
->should(new IsFinal())
->because('domain events must be final');

$runner->run($dir, $rule);

self::assertCount(0, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());
}

// -------------------------------------------------------------------------
// Directory structures
// -------------------------------------------------------------------------

private function createNamespaceAndNamingStructure(): array
{
return [
'Domain' => [
'UserCreatedEvent.php' => <<<'EOT'
<?php
namespace App\Domain;
class UserCreatedEvent {}
EOT,
'OrderFinalizedEvent.php' => <<<'EOT'
<?php
namespace App\Domain;
final class OrderFinalizedEvent {}
EOT,
'OrderService.php' => <<<'EOT'
<?php
namespace App\Domain;
class OrderService {}
EOT,
],
'Infrastructure' => [
'InfrastructureEvent.php' => <<<'EOT'
<?php
namespace App\Infrastructure;
class InfrastructureEvent {}
EOT,
],
];
}

private function createNamespaceAndAttributeStructure(): array
{
return [
'Controller' => [
'ProductsController.php' => <<<'EOT'
<?php
namespace App\Controller;
#[\AsController]
class ProductsController {}
EOT,
'LegacyController.php' => <<<'EOT'
<?php
namespace App\Controller;
#[\Deprecated]
#[\AsController]
final class LegacyController {}
EOT,
'UtilityHelper.php' => <<<'EOT'
<?php
namespace App\Controller;
class UtilityHelper {}
EOT,
],
];
}

private function createThreeConditionStructure(): array
{
return [
'Domain' => [
'UserCreatedEvent.php' => <<<'EOT'
<?php
namespace App\Domain;
class UserCreatedEvent {}
EOT,
'AbstractBaseEvent.php' => <<<'EOT'
<?php
namespace App\Domain;
abstract class AbstractBaseEvent {}
EOT,
'OrderService.php' => <<<'EOT'
<?php
namespace App\Domain;
class OrderService {}
EOT,
],
];
}
}
Loading