Skip to content

Commit 0e52a8b

Browse files
authored
feat: test architecture (#361)
* chore(phpat): install codely fork * chore(phpat): test shared domain architecture * ci: test architecture * chore(phpat): test shared infrastructure architecture * chore(phpat): test shared infrastructure architecture * chore(phpat): test application services only have one public method
1 parent ff40d42 commit 0e52a8b

File tree

10 files changed

+275
-19
lines changed

10 files changed

+275
-19
lines changed

Diff for: .github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
- name: 🏁 Static analysis
2929
run: make static-analysis
3030

31+
- name: 🏗️ Architecture
32+
run: make test-architecture
33+
3134
- name: 🦭 Wait for the database to get up
3235
run: |
3336
while ! make ping-mysql &>/dev/null; do

Diff for: Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ static-analysis:
1818
lint:
1919
docker exec codely-php_ddd_skeleton-mooc_backend-php ./vendor/bin/ecs check
2020

21+
test-architecture:
22+
docker exec codely-php_ddd_skeleton-mooc_backend-php php -d memory_limit=4G ./vendor/bin/phpstan analyse
23+
2124
start:
2225
@if [ ! -f .env.local ]; then echo '' > .env.local; fi
2326
UID=${shell id -u} GID=${shell id -g} docker compose up --build -d

Diff for: composer.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@
5757
"rector/rector": "^0.18.4",
5858
"psalm/plugin-mockery": "^1.1",
5959
"psalm/plugin-symfony": "^5.0",
60-
"psalm/plugin-phpunit": "^0.18.4"
60+
"psalm/plugin-phpunit": "^0.18.4",
61+
"phpstan/phpstan": "^1.10",
62+
"phpat/phpat": "dev-add-has_one_public_method"
6163
},
6264
"autoload": {
6365
"psr-4": {
@@ -80,5 +82,11 @@
8082
"allow-plugins": {
8183
"ocramius/package-versions": true
8284
}
83-
}
85+
},
86+
"repositories": [
87+
{
88+
"type": "vcs",
89+
"url": "https://github.com/CodelyTV/phpat"
90+
}
91+
]
8492
}

Diff for: composer.lock

+68-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: phpstan.neon

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
includes:
2+
- vendor/phpat/phpat/extension.neon
3+
4+
parameters:
5+
level: 0
6+
paths:
7+
- ./apps
8+
- ./src
9+
- ./tests
10+
excludePaths:
11+
- ./apps/backoffice/backend/var
12+
- ./apps/backoffice/frontend/var
13+
- ./apps/mooc/backend/var
14+
- ./apps/mooc/frontend/var
15+
16+
services:
17+
-
18+
class: CodelyTv\Tests\Shared\SharedArchitectureTest
19+
tags:
20+
- phpat.test
21+
22+
-
23+
class: CodelyTv\Tests\Mooc\MoocArchitectureTest
24+
tags:
25+
- phpat.test

Diff for: src/Shared/Domain/Utils.php

-9
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
use DateTimeImmutable;
88
use DateTimeInterface;
9-
use ReflectionClass;
109
use RuntimeException;
11-
1210
use function Lambdish\Phunctional\filter;
1311

1412
final class Utils
@@ -81,13 +79,6 @@ public static function filesIn(string $path, string $fileType): array
8179
);
8280
}
8381

84-
public static function extractClassName(object $object): string
85-
{
86-
$reflect = new ReflectionClass($object);
87-
88-
return $reflect->getShortName();
89-
}
90-
9182
public static function iterableToArray(iterable $iterable): array
9283
{
9384
if (is_array($iterable)) {

Diff for: src/Shared/Infrastructure/Symfony/ApiExceptionListener.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use CodelyTv\Shared\Domain\DomainError;
88
use CodelyTv\Shared\Domain\Utils;
9+
use ReflectionClass;
910
use Symfony\Component\HttpFoundation\JsonResponse;
1011
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1112
use Throwable;
@@ -35,6 +36,13 @@ private function exceptionCodeFor(Throwable $error): string
3536

3637
return $error instanceof $domainErrorClass
3738
? $error->errorCode()
38-
: Utils::toSnakeCase(Utils::extractClassName($error));
39+
: Utils::toSnakeCase($this->extractClassName($error));
40+
}
41+
42+
private function extractClassName(object $object): string
43+
{
44+
$reflect = new ReflectionClass($object);
45+
46+
return $reflect->getShortName();
3947
}
4048
}

Diff for: tests/Mooc/MoocArchitectureTest.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodelyTv\Tests\Mooc;
6+
7+
use CodelyTv\Tests\Shared\Infrastructure\ArchitectureTest;
8+
use PHPat\Selector\Selector;
9+
use PHPat\Test\Builder\Rule;
10+
use PHPat\Test\PHPat;
11+
12+
final class MoocArchitectureTest
13+
{
14+
public function test_mooc_domain_should_only_import_itself_and_shared(): Rule
15+
{
16+
return PHPat::rule()
17+
->classes(Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true))
18+
->canOnlyDependOn()
19+
->classes(...array_merge(ArchitectureTest::languageClasses(), [
20+
// Itself
21+
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true),
22+
// Shared
23+
Selector::inNamespace('CodelyTv\Shared\Domain'),
24+
]))
25+
->because('mooc domain can only import itself and shared domain');
26+
}
27+
28+
public function test_mooc_application_should_only_import_itself_and_domain(): Rule
29+
{
30+
return PHPat::rule()
31+
->classes(Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Application/', true))
32+
->canOnlyDependOn()
33+
->classes(...array_merge(ArchitectureTest::languageClasses(), [
34+
// Itself
35+
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Application/', true),
36+
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true),
37+
// Shared
38+
Selector::inNamespace('CodelyTv\Shared'),
39+
]))
40+
->because('mooc application can only import itself and shared');
41+
}
42+
43+
public function test_mooc_infrastructure_should_not_import_other_contexts_beside_shared(): Rule
44+
{
45+
return PHPat::rule()
46+
->classes(Selector::inNamespace('CodelyTv\Mooc'))
47+
->shouldNotDependOn()
48+
->classes(Selector::inNamespace('CodelyTv'))
49+
->excluding(
50+
// Itself
51+
Selector::inNamespace('CodelyTv\Mooc'),
52+
// Shared
53+
Selector::inNamespace('CodelyTv\Shared'),
54+
);
55+
}
56+
}

Diff for: tests/Shared/Infrastructure/ArchitectureTest.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodelyTv\Tests\Shared\Infrastructure;
6+
7+
use ArrayIterator;
8+
use BackedEnum;
9+
use Countable;
10+
use DateTimeImmutable;
11+
use DateTimeInterface;
12+
use DomainException;
13+
use InvalidArgumentException;
14+
use IteratorAggregate;
15+
use PHPat\Selector\Selector;
16+
use RuntimeException;
17+
use Stringable;
18+
use Throwable;
19+
use Traversable;
20+
21+
final class ArchitectureTest
22+
{
23+
public static function languageClasses(): array
24+
{
25+
return [
26+
Selector::classname(Throwable::class),
27+
Selector::classname(InvalidArgumentException::class),
28+
Selector::classname(RuntimeException::class),
29+
Selector::classname(DateTimeImmutable::class),
30+
Selector::classname(DateTimeInterface::class),
31+
Selector::classname(DomainException::class),
32+
Selector::classname(Stringable::class),
33+
Selector::classname(BackedEnum::class),
34+
Selector::classname(Countable::class),
35+
Selector::classname(IteratorAggregate::class),
36+
Selector::classname(Traversable::class),
37+
Selector::classname(ArrayIterator::class),
38+
];
39+
}
40+
}

Diff for: tests/Shared/SharedArchitectureTest.php

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodelyTv\Tests\Shared;
6+
7+
use CodelyTv\Backoffice\Auth\Application\Authenticate\AuthenticateUserCommand;
8+
use CodelyTv\Shared\Domain\Bus\Event\DomainEventSubscriber;
9+
use CodelyTv\Shared\Domain\Bus\Query\Response;
10+
use CodelyTv\Tests\Shared\Infrastructure\ArchitectureTest;
11+
use CodelyTv\Tests\Shared\Infrastructure\Doctrine\MySqlDatabaseCleaner;
12+
use PHPat\Selector\Selector;
13+
use PHPat\Test\Builder\Rule;
14+
use PHPat\Test\PHPat;
15+
use Ramsey\Uuid\Uuid;
16+
17+
final class SharedArchitectureTest
18+
{
19+
public function test_shared_domain_should_not_import_from_outside(): Rule
20+
{
21+
return PHPat::rule()
22+
->classes(Selector::inNamespace('CodelyTv\Shared\Domain'))
23+
->canOnlyDependOn()
24+
->classes(...array_merge(ArchitectureTest::languageClasses(), [
25+
// Itself
26+
Selector::inNamespace('CodelyTv\Shared\Domain'),
27+
// Dependencies treated as domain
28+
Selector::classname(Uuid::class),
29+
]))
30+
->because('shared domain cannot import from outside');
31+
}
32+
33+
public function test_shared_infrastructure_should_not_import_from_other_contexts(): Rule
34+
{
35+
return PHPat::rule()
36+
->classes(Selector::inNamespace('CodelyTv\Shared\Infrastructure'))
37+
->shouldNotDependOn()
38+
->classes(Selector::inNamespace('CodelyTv'))
39+
->excluding(
40+
// Itself
41+
Selector::inNamespace('CodelyTv\Shared'),
42+
// This need to be refactored
43+
Selector::classname(MySqlDatabaseCleaner::class),
44+
Selector::classname(AuthenticateUserCommand::class),
45+
);
46+
}
47+
48+
public function test_all_use_cases_can_only_have_one_public_method(): Rule
49+
{
50+
return PHPat::rule()
51+
->classes(
52+
Selector::classname('/^CodelyTv\\\\.+\\\\.+\\\\Application\\\\.+\\\\(?!.*(?:Command|Query)$).*$/', true)
53+
)
54+
->excluding(
55+
Selector::implements(Response::class),
56+
Selector::implements(DomainEventSubscriber::class),
57+
Selector::inNamespace('/.*\\\\Tests\\\\.*/', true)
58+
)
59+
->shouldHaveOnlyOnePublicMethod();
60+
}
61+
}

0 commit comments

Comments
 (0)