2026-08-12 15:45:29 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-08-13 11:25:08 +02:00
|
|
|
namespace Tests\Architecture;
|
|
|
|
|
|
2026-10-06 14:51:46 +02:00
|
|
|
use Application\Health\Dto\CheckResult;
|
2026-09-14 19:13:41 +02:00
|
|
|
use Application\Security\Models\ValueObjects\Request\HttpMethod;
|
|
|
|
|
use Shared\DDD\Event\Event;
|
|
|
|
|
use Shared\DDD\Port\Directory;
|
|
|
|
|
use Shared\DDD\Port\Repository;
|
|
|
|
|
use Shared\DDD\Usecase\Input;
|
|
|
|
|
use Shared\DDD\Usecase\Output;
|
|
|
|
|
use Shared\DDD\Usecase\Usecase;
|
2026-08-17 11:18:53 +02:00
|
|
|
|
2026-08-12 15:45:29 +02:00
|
|
|
describe('Application Architecture Rules', function (): void {
|
|
|
|
|
arch('Application layer does not depend on Presentation, Infra, or App')
|
|
|
|
|
->expect('Application')
|
|
|
|
|
->not->toUse([
|
|
|
|
|
'Presentation',
|
|
|
|
|
'Infra',
|
|
|
|
|
'Infrastructure',
|
|
|
|
|
'App',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-08-17 09:22:18 +02:00
|
|
|
arch('Application layer does not depend on Illuminate or Symfony frameworks')
|
2026-08-12 15:45:29 +02:00
|
|
|
->expect('Application')
|
2026-08-17 09:22:18 +02:00
|
|
|
->not->toUse([
|
|
|
|
|
'Illuminate',
|
|
|
|
|
'Symfony',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
arch('Application layer does not use Laravel helper functions')
|
|
|
|
|
->expect('Application')
|
|
|
|
|
->not->toUse([
|
|
|
|
|
'abort',
|
|
|
|
|
'app',
|
|
|
|
|
'auth',
|
|
|
|
|
'asset',
|
|
|
|
|
'back',
|
|
|
|
|
'bcrypt',
|
|
|
|
|
'broadcast',
|
|
|
|
|
'cache',
|
|
|
|
|
'config',
|
|
|
|
|
'cookie',
|
|
|
|
|
'csrf_token',
|
|
|
|
|
'dd',
|
|
|
|
|
'dump',
|
|
|
|
|
'decrypt',
|
|
|
|
|
'dispatch',
|
|
|
|
|
'event',
|
|
|
|
|
'factory',
|
|
|
|
|
'logger',
|
|
|
|
|
'redirect',
|
|
|
|
|
'request',
|
|
|
|
|
'response',
|
|
|
|
|
'route',
|
|
|
|
|
'session',
|
|
|
|
|
'storage',
|
|
|
|
|
'url',
|
|
|
|
|
'throw_if',
|
|
|
|
|
'throw_unless',
|
|
|
|
|
'__',
|
|
|
|
|
'trans',
|
|
|
|
|
'trans_choice',
|
|
|
|
|
'view',
|
|
|
|
|
]);
|
2026-08-12 15:45:29 +02:00
|
|
|
|
|
|
|
|
arch('Application layer uses strict types')
|
|
|
|
|
->expect('Application')
|
|
|
|
|
->toUseStrictTypes();
|
|
|
|
|
|
|
|
|
|
arch('Application Usecase classes are final and readonly')
|
2026-10-05 15:50:01 +02:00
|
|
|
->expect(function () {
|
|
|
|
|
$namespaces = [
|
|
|
|
|
'Application\Arcadia\Usecase',
|
|
|
|
|
'Application\Security\Usecases',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return array_filter(
|
|
|
|
|
get_declared_classes(),
|
|
|
|
|
fn (string $class) => ! interface_exists($class, false)
|
|
|
|
|
&& array_any($namespaces, fn ($ns) => str_starts_with($class, $ns))
|
|
|
|
|
);
|
|
|
|
|
})
|
2026-09-14 19:13:41 +02:00
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal();
|
|
|
|
|
|
|
|
|
|
arch('Classes implementing Shared\DDD\Usecase\Usecase are final and readonly')
|
|
|
|
|
->expect(Usecase::class)
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal()
|
|
|
|
|
->ignoring(Usecase::class);
|
|
|
|
|
|
|
|
|
|
arch('Application Input DTOs implement Shared\DDD\Usecase\Input and are final and readonly')
|
|
|
|
|
->expect(Input::class)
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal()
|
|
|
|
|
->ignoring(Input::class);
|
|
|
|
|
|
|
|
|
|
arch('Application Output DTOs implement Shared\DDD\Usecase\Output and are final and readonly')
|
|
|
|
|
->expect(Output::class)
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal()
|
|
|
|
|
->ignoring(Output::class);
|
|
|
|
|
|
|
|
|
|
arch('Application Health DTOs are final and readonly')
|
|
|
|
|
->expect('Application\Health\Dto')
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
2026-10-06 14:51:46 +02:00
|
|
|
->toBeFinal()
|
|
|
|
|
->ignoring([
|
|
|
|
|
CheckResult::class,
|
|
|
|
|
]);
|
2026-09-14 19:13:41 +02:00
|
|
|
|
|
|
|
|
arch('Application Config classes are final and readonly')
|
|
|
|
|
->expect([
|
|
|
|
|
'Application\Arcadia\Config',
|
|
|
|
|
'Application\Health\Config',
|
|
|
|
|
'Application\Security\Config',
|
|
|
|
|
])
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal();
|
|
|
|
|
|
|
|
|
|
arch('Application Port Directories and Repositories contain only interfaces')
|
|
|
|
|
->expect([
|
|
|
|
|
'Application\Security\Ports\Directories',
|
|
|
|
|
'Application\Security\Ports\Repositories',
|
|
|
|
|
])
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeInterface();
|
|
|
|
|
|
|
|
|
|
arch('Application Port Directories extend Shared\DDD\Port\Directory')
|
|
|
|
|
->expect('Application\Security\Ports\Directories')
|
|
|
|
|
->toExtend(Directory::class);
|
|
|
|
|
|
|
|
|
|
arch('Application Port Repositories extend Shared\DDD\Port\Repository')
|
|
|
|
|
->expect('Application\Security\Ports\Repositories')
|
|
|
|
|
->toExtend(Repository::class);
|
|
|
|
|
|
|
|
|
|
arch('Application Events implement Shared\DDD\Event\Event and are final and readonly')
|
|
|
|
|
->expect('Application\Security\Events')
|
|
|
|
|
->toImplement(Event::class)
|
|
|
|
|
->classes()
|
|
|
|
|
->toBeReadonly()
|
|
|
|
|
->toBeFinal();
|
|
|
|
|
|
|
|
|
|
arch('Application Security Value Objects are final and readonly')
|
|
|
|
|
->expect('Application\Security\Models\ValueObjects')
|
2026-08-14 11:33:29 +02:00
|
|
|
->classes()
|
2026-08-12 15:45:29 +02:00
|
|
|
->toBeReadonly()
|
2026-08-14 11:33:29 +02:00
|
|
|
->toBeFinal()
|
2026-09-14 19:13:41 +02:00
|
|
|
->ignoring([
|
|
|
|
|
HttpMethod::class,
|
|
|
|
|
]);
|
2026-08-12 17:01:09 +02:00
|
|
|
|
|
|
|
|
arch('Application layer must not contain Presenters')
|
|
|
|
|
->expect('Application')
|
|
|
|
|
->classes()
|
|
|
|
|
->not->toHaveSuffix('Presenter');
|
|
|
|
|
|
|
|
|
|
arch('Application layer must not contain Views')
|
|
|
|
|
->expect('Application')
|
|
|
|
|
->classes()
|
|
|
|
|
->not->toHaveSuffix('View');
|
2026-08-12 15:45:29 +02:00
|
|
|
});
|