CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

90

Quality

90%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

This version of the tile failed moderation
Moderation pipeline encountered an internal error
Overview
Quality
Evals
Security
Files

php-clean-architecture.mdplugins/developer-kit-php/skills/clean-architecture/references/

PHP Clean Architecture Patterns

Value Objects Deep Dive

Money Value Object

<?php
// src/Domain/ValueObject/Money.php

namespace App\Domain\ValueObject;

use InvalidArgumentException;

final readonly class Money
{
    public function __construct(
        private int $cents,
        private string $currency
    ) {
        if ($cents < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }

        if (!in_array($currency, ['EUR', 'USD', 'GBP'], true)) {
            throw new InvalidArgumentException('Unsupported currency');
        }
    }

    public static function fromEuros(float $amount): self
    {
        return new self((int) round($amount * 100), 'EUR');
    }

    public function cents(): int
    {
        return $this->cents;
    }

    public function asFloat(): float
    {
        return $this->cents / 100;
    }

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

    public function add(self $other): self
    {
        $this->assertSameCurrency($other);

        return new self($this->cents + $other->cents, $this->currency);
    }

    public function subtract(self $other): self
    {
        $this->assertSameCurrency($other);

        return new self($this->cents - $other->cents, $this->currency);
    }

    public function multiply(float $factor): self
    {
        return new self((int) round($this->cents * $factor), $this->currency);
    }

    public function equals(self $other): bool
    {
        return $this->cents === $other->cents
            && $this->currency === $other->currency;
    }

    public function isGreaterThan(self $other): bool
    {
        $this->assertSameCurrency($other);

        return $this->cents > $other->cents;
    }

    private function assertSameCurrency(self $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
    }
}

UUID Value Object

<?php
// src/Domain/ValueObject/UserId.php

namespace App\Domain\ValueObject;

use InvalidArgumentException;
use Symfony\Component\Uid\Uuid;

final readonly class UserId
{
    private string $value;

    public function __construct(string $value)
    {
        if (!Uuid::isValid($value)) {
            throw new InvalidArgumentException('Invalid UUID format');
        }

        $this->value = $value;
    }

    public static function generate(): self
    {
        return new self(Uuid::v4()->toRfc4122());
    }

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

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

Aggregate Pattern

<?php
// src/Domain/Entity/Order.php

namespace App\Domain\Entity;

use App\Domain\ValueObject\Money;
use App\Domain\ValueObject\OrderId;
use App\Domain\Event\OrderSubmittedEvent;
use InvalidArgumentException;

class Order
{
    private array $items = [];
    private string $status = 'pending';
    private array $domainEvents = [];

    public function __construct(
        private OrderId $id,
        private UserId $userId
    ) {
    }

    public function addItem(string $productId, int $quantity, Money $price): void
    {
        if ($this->status !== 'pending') {
            throw new InvalidArgumentException('Cannot modify submitted order');
        }

        if ($quantity <= 0) {
            throw new InvalidArgumentException('Quantity must be positive');
        }

        $this->items[] = new OrderItem($productId, $quantity, $price);
    }

    public function submit(): void
    {
        if (empty($this->items)) {
            throw new InvalidArgumentException('Cannot submit empty order');
        }

        if ($this->status !== 'pending') {
            throw new InvalidArgumentException('Order already submitted');
        }

        $this->status = 'submitted';
        $this->recordEvent(new OrderSubmittedEvent($this->id->value()));
    }

    public function total(): Money
    {
        $total = Money::fromEuros(0);

        foreach ($this->items as $item) {
            $total = $total->add($item->total());
        }

        return $total;
    }

    public function id(): OrderId
    {
        return $this->id;
    }

    public function domainEvents(): array
    {
        return $this->domainEvents;
    }

    public function clearDomainEvents(): void
    {
        $this->domainEvents = [];
    }

    private function recordEvent(object $event): void
    {
        $this->domainEvents[] = $event;
    }
}

OrderItem (Part of Aggregate)

<?php
// src/Domain/Entity/OrderItem.php

namespace App\Domain\Entity;

use App\Domain\ValueObject\Money;

final readonly class OrderItem
{
    public function __construct(
        private string $productId,
        private int $quantity,
        private Money $unitPrice
    ) {
    }

    public function total(): Money
    {
        return $this->unitPrice->multiply($this->quantity);
    }
}

Domain Services

When logic does not belong to a single entity:

<?php
// src/Domain/Service/PricingService.php

namespace App\Domain\Service;

use App\Domain\Entity\Order;
use App\Domain\ValueObject\Money;

interface PricingServiceInterface
{
    public function calculateDiscount(Order $order): Money;
}

Domain Events

<?php
// src/Domain/Event/OrderSubmittedEvent.php

namespace App\Domain\Event;

use DateTimeImmutable;

final readonly class OrderSubmittedEvent
{
    public function __construct(
        public string $orderId,
        public DateTimeImmutable $occurredAt = new DateTimeImmutable()
    ) {
    }
}

Specification Pattern

<?php
// src/Domain/Specification/SpecificationInterface.php

namespace App\Domain\Specification;

interface SpecificationInterface
{
    public function isSatisfiedBy(object $candidate): bool;
}
<?php
// src/Domain/Specification/ActiveUserSpecification.php

namespace App\Domain\Specification;

use App\Domain\Entity\User;

class ActiveUserSpecification implements SpecificationInterface
{
    public function isSatisfiedBy(object $candidate): bool
    {
        if (!$candidate instanceof User) {
            return false;
        }

        return $candidate->isActive();
    }
}

Enum for Status (PHP 8.1+)

<?php
// src/Domain/Enum/OrderStatus.php

namespace App\Domain\Enum;

enum OrderStatus: string
{
    case PENDING = 'pending';
    case SUBMITTED = 'submitted';
    case PAID = 'paid';
    case SHIPPED = 'shipped';
    case CANCELLED = 'cancelled';

    public function canTransitionTo(self $newStatus): bool
    {
        return match ($this) {
            self::PENDING => in_array($newStatus, [self::SUBMITTED, self::CANCELLED], true),
            self::SUBMITTED => in_array($newStatus, [self::PAID, self::CANCELLED], true),
            self::PAID => in_array($newStatus, [self::SHIPPED, self::CANCELLED], true),
            self::SHIPPED => false,
            self::CANCELLED => false,
        };
    }
}

Testing Value Objects

<?php
// tests/Domain/ValueObject/EmailTest.php

namespace App\Tests\Domain\ValueObject;

use App\Domain\ValueObject\Email;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

class EmailTest extends TestCase
{
    public function testCanCreateValidEmail(): void
    {
        $email = new Email('test@example.com');

        $this->assertEquals('test@example.com', $email->value());
    }

    public function testThrowsExceptionForInvalidEmail(): void
    {
        $this->expectException(InvalidArgumentException::class);

        new Email('invalid-email');
    }

    public function testEmailsAreComparable(): void
    {
        $email1 = new Email('test@example.com');
        $email2 = new Email('test@example.com');
        $email3 = new Email('other@example.com');

        $this->assertTrue($email1->equals($email2));
        $this->assertFalse($email1->equals($email3));
    }
}

Testing Use Cases (No Framework)

<?php
// tests/Application/Handler/CreateUserHandlerTest.php

namespace App\Tests\Application\Handler;

use App\Application\Command\CreateUserCommand;
use App\Application\Handler\CreateUserHandler;
use App\Tests\Infrastructure\Repository\InMemoryUserRepository;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

class CreateUserHandlerTest extends TestCase
{
    private InMemoryUserRepository $repository;
    private CreateUserHandler $handler;

    protected function setUp(): void
    {
        $this->repository = new InMemoryUserRepository();
        $this->handler = new CreateUserHandler($this->repository);
    }

    public function testCanCreateUser(): void
    {
        $command = new CreateUserCommand(
            id: '550e8400-e29b-41d4-a716-446655440000',
            email: 'test@example.com',
            name: 'John Doe'
        );

        ($this->handler)($command);

        $user = $this->repository->findById(new UserId($command->id));
        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user->name());
    }

    public function testCannotCreateDuplicateUser(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('User with this email already exists');

        $command = new CreateUserCommand(
            id: '550e8400-e29b-41d4-a716-446655440000',
            email: 'test@example.com',
            name: 'John Doe'
        );

        ($this->handler)($command);
        ($this->handler)($command);
    }
}

plugins

CHANGELOG.md

context7.json

CONTRIBUTING.md

README_CN.md

README_ES.md

README_IT.md

README.md

tessl.json

tile.json