Skip to content

Commit

Permalink
Add architecture test to prevent domain layer being coupled with 3rd …
Browse files Browse the repository at this point in the history
…party libraries
  • Loading branch information
coldic3 committed Sep 7, 2024
1 parent b875f76 commit b0c92f8
Show file tree
Hide file tree
Showing 18 changed files with 122 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Panda\Account\Domain\Exception\AuthorizedUserNotFoundException;
use Panda\Account\Domain\Model\UserInterface;
use Panda\Account\Domain\Provider\AuthorizedUserProvider;
use Panda\Account\Infrastructure\Symfony\Security\AuthorizedUserProvider;
use Panda\AccountOHS\Domain\Provider\AuthorizedUserProviderInterface;
use PhpSpec\ObjectBehavior;
use Symfony\Bundle\SecurityBundle\Security;
Expand Down
2 changes: 1 addition & 1 deletion src/Account/Domain/Factory/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace Panda\Account\Domain\Factory;

use Panda\Account\Domain\Hasher\UserPasswordHasherInterface;
use Panda\Account\Domain\Model\User;
use Panda\Account\Domain\Model\UserInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

final readonly class UserFactory implements UserFactoryInterface
{
Expand Down
12 changes: 12 additions & 0 deletions src/Account/Domain/Hasher/UserPasswordHasherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Panda\Account\Domain\Hasher;

use Panda\Account\Domain\Model\UserInterface;

interface UserPasswordHasherInterface
{
public function hashPassword(UserInterface $user, #[\SensitiveParameter] string $plainPassword): string;
}
14 changes: 0 additions & 14 deletions src/Account/Domain/Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,6 @@ public function setEmail(string $email): void
$this->email = $email;
}

public function getUserIdentifier(): string
{
return $this->email;
}

public function getRoles(): array
{
return ['ROLE_USER'];
}

public function getPassword(): ?string
{
return $this->password;
Expand All @@ -54,10 +44,6 @@ public function setPassword(string $password): void
$this->password = $password;
}

public function eraseCredentials(): void
{
}

public function compare(OwnerInterface $owner): bool
{
return $this->getId() === $owner->getId();
Expand Down
6 changes: 3 additions & 3 deletions src/Account/Domain/Model/UserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

use Panda\AccountOHS\Domain\Model\Owner\OwnerInterface;
use Panda\Core\Domain\Model\TimestampableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface;

interface UserInterface extends TimestampableInterface, OwnerInterface, SymfonyUserInterface, PasswordAuthenticatedUserInterface
interface UserInterface extends TimestampableInterface, OwnerInterface
{
public function getEmail(): string;

public function setEmail(string $email): void;

public function getPassword(): ?string;

public function setPassword(string $password): void;
}
3 changes: 1 addition & 2 deletions src/Account/Domain/Repository/UserRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
namespace Panda\Account\Domain\Repository;

use Panda\Account\Domain\Model\UserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Uid\Uuid;

interface UserRepositoryInterface extends PasswordUpgraderInterface
interface UserRepositoryInterface
{
public function save(UserInterface $user): void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

use Panda\Account\Domain\Provider\AuthorizedUserProvider;
use Panda\Account\Infrastructure\Symfony\Security\AuthorizedUserProvider;
use Panda\AccountOHS\Domain\Provider\AuthorizedUserProviderInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
Expand Down
2 changes: 1 addition & 1 deletion src/Account/Infrastructure/Doctrine/Orm/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Panda\Account\Domain\Model\User;
use Panda\Account\Domain\Model\UserInterface;
use Panda\Account\Domain\Repository\UserRepositoryInterface;
use Panda\Account\Infrastructure\Symfony\Security\UserRepositoryInterface;
use Panda\Core\Infrastructure\Doctrine\Orm\DoctrineRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

declare(strict_types=1);

namespace Panda\Account\Domain\Provider;
namespace Panda\Account\Infrastructure\Symfony\Security;

use Panda\Account\Domain\Exception\AuthorizedUserNotFoundException;
use Panda\Account\Domain\Model\UserInterface;
use Panda\AccountOHS\Domain\Model\Owner\OwnerInterface;
use Panda\AccountOHS\Domain\Provider\AuthorizedUserProviderInterface;
use Symfony\Bundle\SecurityBundle\Security;
Expand Down
24 changes: 24 additions & 0 deletions src/Account/Infrastructure/Symfony/Security/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Panda\Account\Infrastructure\Symfony\Security;

use Panda\Account\Domain\Model\User as DomainUser;

final class User extends DomainUser implements UserInterface
{
public function getUserIdentifier(): string
{
return $this->getEmail();
}

public function getRoles(): array
{
return ['ROLE_USER'];
}

public function eraseCredentials(): void
{
}
}
13 changes: 13 additions & 0 deletions src/Account/Infrastructure/Symfony/Security/UserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Panda\Account\Infrastructure\Symfony\Security;

use Panda\Account\Domain\Model\UserInterface as DomainUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface;

interface UserInterface extends DomainUserInterface, SymfonyUserInterface, PasswordAuthenticatedUserInterface
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Panda\Account\Infrastructure\Symfony\Security;

use Panda\Account\Domain\Repository\UserRepositoryInterface as DomainUserRepositoryInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

interface UserRepositoryInterface extends DomainUserRepositoryInterface, PasswordUpgraderInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ security:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: Panda\Account\Domain\Model\User
class: Panda\Account\Infrastructure\Symfony\Security\User
property: email
firewalls:
dev:
Expand Down
7 changes: 5 additions & 2 deletions src/Report/Domain/Calculator/OperationValueCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use Panda\AccountOHS\Domain\Model\Owner\OwnerInterface;
use Panda\Exchange\Domain\Repository\ExchangeRateLogRepositoryInterface;
use Panda\Portfolio\Domain\Model\Portfolio\PortfolioInterface;
use Panda\Report\Domain\Exception\ExchangeRateLogNotFoundException;
use Panda\Trade\Domain\Model\Transaction\OperationInterface;
use Webmozart\Assert\Assert;

final readonly class OperationValueCalculator implements OperationValueCalculatorInterface
{
Expand All @@ -35,7 +35,10 @@ public function calculate(
$ticker,
$datetime,
);
Assert::notNull($exchangeRate);

if (null === $exchangeRate) {
throw new ExchangeRateLogNotFoundException(sprintf('Exchange rate log for %s datetime not found.', $datetime->format('Y-m-d H:i:s')));
}

return $operation->getQuantity() * $exchangeRate->getRate();
}
Expand Down
10 changes: 10 additions & 0 deletions src/Report/Domain/Exception/ExchangeRateLogNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Panda\Report\Domain\Exception;

final class ExchangeRateLogNotFoundException extends \Exception
{
protected $message = 'Exchange rate log not found.';
}
10 changes: 10 additions & 0 deletions src/Trade/Domain/Exception/EmptyAdjustmentsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Panda\Trade\Domain\Exception;

final class EmptyAdjustmentsException extends \InvalidArgumentException
{
protected $message = 'Adjustments cannot be empty.';
}
6 changes: 4 additions & 2 deletions src/Trade/Domain/Factory/TransactionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

use Panda\AccountOHS\Domain\Model\Owner\OwnerInterface;
use Panda\AccountOHS\Domain\Provider\AuthorizedUserProviderInterface;
use Panda\Trade\Domain\Exception\EmptyAdjustmentsException;
use Panda\Trade\Domain\Model\Transaction\OperationInterface;
use Panda\Trade\Domain\Model\Transaction\Transaction;
use Panda\Trade\Domain\Model\Transaction\TransactionInterface;
use Panda\Trade\Domain\ValueObject\TransactionTypeEnum;
use Webmozart\Assert\Assert;

final class TransactionFactory implements TransactionFactoryInterface
{
Expand Down Expand Up @@ -93,7 +93,9 @@ public function createFee(
\DateTimeInterface $concludedAt,
?OwnerInterface $owner = null,
): TransactionInterface {
Assert::notEmpty($adjustments);
if ([] === $adjustments) {
throw new EmptyAdjustmentsException();
}

$transaction = new Transaction(
TransactionTypeEnum::FEE,
Expand Down
29 changes: 22 additions & 7 deletions tests/Architecture/LayersSeparationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

namespace Panda\Tests\Architecture;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Uid\Uuid;

final class LayersSeparationTest
{
Expand All @@ -26,19 +29,31 @@ public function test_domain_does_not_depend_on_other_layers(): Rule
->classes(...$this->applicationLayerSelectors, ...$this->infrastructureLayerSelectors);
}

public function test_domain_does_not_depend_on_doctrine(): Rule
public function test_domain_does_not_depend_on_vendor(): Rule
{
$this->findAllLayers();

return PHPat::rule()
->classes(...$this->domainLayerSelectors)
->shouldNotDependOn()
->classes(Selector::namespace('Doctrine'))
->canOnlyDependOn()
->classes(
Selector::namespace('Panda'),

// Allowed 3rd party classes
Selector::classname(Uuid::class),
Selector::classname(Collection::class),
Selector::classname(ArrayCollection::class),

// FIXME: requires too much effort to get rid of these dependencies for now
->excluding(
Selector::classname('Doctrine\Common\Collections\ArrayCollection'),
Selector::classname('Doctrine\Common\Collections\Collection'),
// PHP root namespace
Selector::classname(\BackedEnum::class),
Selector::classname(\Countable::class),
Selector::classname(\DateTimeImmutable::class),
Selector::classname(\DateTimeInterface::class),
Selector::classname(\Exception::class),
Selector::classname(\InvalidArgumentException::class),
Selector::classname(\Iterator::class),
Selector::classname(\IteratorAggregate::class),
Selector::classname(\Throwable::class),
);
}

Expand Down

0 comments on commit b0c92f8

Please sign in to comment.