Skip to content

Commit

Permalink
feat: test architecture (#361)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rgomezcasas authored Oct 3, 2023
1 parent ff40d42 commit 0e52a8b
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 19 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
- name: 🏁 Static analysis
run: make static-analysis

- name: 🏗️ Architecture
run: make test-architecture

- name: 🦭 Wait for the database to get up
run: |
while ! make ping-mysql &>/dev/null; do
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ static-analysis:
lint:
docker exec codely-php_ddd_skeleton-mooc_backend-php ./vendor/bin/ecs check

test-architecture:
docker exec codely-php_ddd_skeleton-mooc_backend-php php -d memory_limit=4G ./vendor/bin/phpstan analyse

start:
@if [ ! -f .env.local ]; then echo '' > .env.local; fi
UID=${shell id -u} GID=${shell id -g} docker compose up --build -d
Expand Down
12 changes: 10 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@
"rector/rector": "^0.18.4",
"psalm/plugin-mockery": "^1.1",
"psalm/plugin-symfony": "^5.0",
"psalm/plugin-phpunit": "^0.18.4"
"psalm/plugin-phpunit": "^0.18.4",
"phpstan/phpstan": "^1.10",
"phpat/phpat": "dev-add-has_one_public_method"
},
"autoload": {
"psr-4": {
Expand All @@ -80,5 +82,11 @@
"allow-plugins": {
"ocramius/package-versions": true
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/CodelyTV/phpat"
}
]
}
75 changes: 68 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
includes:
- vendor/phpat/phpat/extension.neon

parameters:
level: 0
paths:
- ./apps
- ./src
- ./tests
excludePaths:
- ./apps/backoffice/backend/var
- ./apps/backoffice/frontend/var
- ./apps/mooc/backend/var
- ./apps/mooc/frontend/var

services:
-
class: CodelyTv\Tests\Shared\SharedArchitectureTest
tags:
- phpat.test

-
class: CodelyTv\Tests\Mooc\MoocArchitectureTest
tags:
- phpat.test
9 changes: 0 additions & 9 deletions src/Shared/Domain/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

use DateTimeImmutable;
use DateTimeInterface;
use ReflectionClass;
use RuntimeException;

use function Lambdish\Phunctional\filter;

final class Utils
Expand Down Expand Up @@ -81,13 +79,6 @@ public static function filesIn(string $path, string $fileType): array
);
}

public static function extractClassName(object $object): string
{
$reflect = new ReflectionClass($object);

return $reflect->getShortName();
}

public static function iterableToArray(iterable $iterable): array
{
if (is_array($iterable)) {
Expand Down
10 changes: 9 additions & 1 deletion src/Shared/Infrastructure/Symfony/ApiExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use CodelyTv\Shared\Domain\DomainError;
use CodelyTv\Shared\Domain\Utils;
use ReflectionClass;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Throwable;
Expand Down Expand Up @@ -35,6 +36,13 @@ private function exceptionCodeFor(Throwable $error): string

return $error instanceof $domainErrorClass
? $error->errorCode()
: Utils::toSnakeCase(Utils::extractClassName($error));
: Utils::toSnakeCase($this->extractClassName($error));
}

private function extractClassName(object $object): string
{
$reflect = new ReflectionClass($object);

return $reflect->getShortName();
}
}
56 changes: 56 additions & 0 deletions tests/Mooc/MoocArchitectureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace CodelyTv\Tests\Mooc;

use CodelyTv\Tests\Shared\Infrastructure\ArchitectureTest;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class MoocArchitectureTest
{
public function test_mooc_domain_should_only_import_itself_and_shared(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true))
->canOnlyDependOn()
->classes(...array_merge(ArchitectureTest::languageClasses(), [
// Itself
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true),
// Shared
Selector::inNamespace('CodelyTv\Shared\Domain'),
]))
->because('mooc domain can only import itself and shared domain');
}

public function test_mooc_application_should_only_import_itself_and_domain(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Application/', true))
->canOnlyDependOn()
->classes(...array_merge(ArchitectureTest::languageClasses(), [
// Itself
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Application/', true),
Selector::inNamespace('/^CodelyTv\\\\Mooc\\\\.+\\\\Domain/', true),
// Shared
Selector::inNamespace('CodelyTv\Shared'),
]))
->because('mooc application can only import itself and shared');
}

public function test_mooc_infrastructure_should_not_import_other_contexts_beside_shared(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('CodelyTv\Mooc'))
->shouldNotDependOn()
->classes(Selector::inNamespace('CodelyTv'))
->excluding(
// Itself
Selector::inNamespace('CodelyTv\Mooc'),
// Shared
Selector::inNamespace('CodelyTv\Shared'),
);
}
}
40 changes: 40 additions & 0 deletions tests/Shared/Infrastructure/ArchitectureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace CodelyTv\Tests\Shared\Infrastructure;

use ArrayIterator;
use BackedEnum;
use Countable;
use DateTimeImmutable;
use DateTimeInterface;
use DomainException;
use InvalidArgumentException;
use IteratorAggregate;
use PHPat\Selector\Selector;
use RuntimeException;
use Stringable;
use Throwable;
use Traversable;

final class ArchitectureTest
{
public static function languageClasses(): array
{
return [
Selector::classname(Throwable::class),
Selector::classname(InvalidArgumentException::class),
Selector::classname(RuntimeException::class),
Selector::classname(DateTimeImmutable::class),
Selector::classname(DateTimeInterface::class),
Selector::classname(DomainException::class),
Selector::classname(Stringable::class),
Selector::classname(BackedEnum::class),
Selector::classname(Countable::class),
Selector::classname(IteratorAggregate::class),
Selector::classname(Traversable::class),
Selector::classname(ArrayIterator::class),
];
}
}
61 changes: 61 additions & 0 deletions tests/Shared/SharedArchitectureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace CodelyTv\Tests\Shared;

use CodelyTv\Backoffice\Auth\Application\Authenticate\AuthenticateUserCommand;
use CodelyTv\Shared\Domain\Bus\Event\DomainEventSubscriber;
use CodelyTv\Shared\Domain\Bus\Query\Response;
use CodelyTv\Tests\Shared\Infrastructure\ArchitectureTest;
use CodelyTv\Tests\Shared\Infrastructure\Doctrine\MySqlDatabaseCleaner;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;
use Ramsey\Uuid\Uuid;

final class SharedArchitectureTest
{
public function test_shared_domain_should_not_import_from_outside(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('CodelyTv\Shared\Domain'))
->canOnlyDependOn()
->classes(...array_merge(ArchitectureTest::languageClasses(), [
// Itself
Selector::inNamespace('CodelyTv\Shared\Domain'),
// Dependencies treated as domain
Selector::classname(Uuid::class),
]))
->because('shared domain cannot import from outside');
}

public function test_shared_infrastructure_should_not_import_from_other_contexts(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('CodelyTv\Shared\Infrastructure'))
->shouldNotDependOn()
->classes(Selector::inNamespace('CodelyTv'))
->excluding(
// Itself
Selector::inNamespace('CodelyTv\Shared'),
// This need to be refactored
Selector::classname(MySqlDatabaseCleaner::class),
Selector::classname(AuthenticateUserCommand::class),
);
}

public function test_all_use_cases_can_only_have_one_public_method(): Rule
{
return PHPat::rule()
->classes(
Selector::classname('/^CodelyTv\\\\.+\\\\.+\\\\Application\\\\.+\\\\(?!.*(?:Command|Query)$).*$/', true)
)
->excluding(
Selector::implements(Response::class),
Selector::implements(DomainEventSubscriber::class),
Selector::inNamespace('/.*\\\\Tests\\\\.*/', true)
)
->shouldHaveOnlyOnePublicMethod();
}
}

0 comments on commit 0e52a8b

Please sign in to comment.