PHP code example of lava83 / laravel-ddd

1. Go to this page and download the library: Download lava83/laravel-ddd library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

lava83 / laravel-ddd example snippets




declare(strict_types=1);

namespace App\Domain\TrelloManagement\Entities;

use Illuminate\Support\Collection;
use Lava83\DddFoundation\Domain\Entities\Entity;
use App\Domain\TrelloManagement\ValueObjects\Identity\MemberId;

class Member extends Entity
{
    public function __construct(
        private MemberId $trelloId,
        protected string $fullName,
        protected string $username,
    ) {
        parent::__construct();
    }

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

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

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

    public function update(string $fullName, string $username): void
    {
        $this->updateEntity([
            'fullName' => $fullName,
            'username' => $username,
        ]);
    }

    protected function applyChanges(Collection $changes): void
    {
        $this->applyChangesByPropertyMap([
            'fullName' => fn($value) => $this->fullName = $value,
            'username' => fn($value) => $this->username = $value,
        ], $changes);
    }
}



declare(strict_types=1);

namespace App\Domain\TrelloManagement\Entities;

use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Lava83\DddFoundation\Domain\Entities\Aggregate;
use Lava83\DddFoundation\Domain\ValueObjects\Communication\Link;
use App\Domain\TrelloManagement\Events\BoardCreated;
use App\Domain\TrelloManagement\Events\BoardUpdated;
use App\Domain\TrelloManagement\ValueObjects\Identity\BoardId;

class Board extends Aggregate
{
    public function __construct(
        protected BoardId $trelloId,
        protected string $name,
        protected string $description,
        protected bool $isClosed,
        protected Link $link,
        protected Link $shortUrl,
        protected bool $isSubscribed,
        protected ?CarbonImmutable $closedAt,
        protected ?CarbonImmutable $lastActivityAt,
        protected ?CarbonImmutable $lastView,
        protected Collection $lists,
        protected ?Webhook $webhook,
    ) {
        parent::__construct();
    }

    public static function create(
        BoardId $trelloId,
        string $name,
        string $description,
        bool $isClosed,
        Link $link,
        Link $shortUrl,
        bool $isSubscribed,
        ?CarbonImmutable $closedAt = null,
        ?CarbonImmutable $lastActivityAt = null,
        ?CarbonImmutable $lastView = null,
    ): self {
        $board = new self(
            trelloId: $trelloId,
            name: $name,
            description: $description,
            isClosed: $isClosed,
            link: $link,
            shortUrl: $shortUrl,
            isSubscribed: $isSubscribed,
            closedAt: $closedAt,
            lastActivityAt: $lastActivityAt,
            lastView: $lastView,
            lists: new Collection,
            webhook: null,
        );

        $board->recordEvent(
            new BoardCreated(
                $board->id(),
                collect([
                    'trelloId' => $board->trelloId(),
                    'name' => $board->name(),
                    'description' => $board->description(),
                    // ... other data
                ])
            )
        );

        return $board;
    }

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

    public function update(
        string $name,
        string $description,
        bool $isClosed,
        Link $link,
        Link $shortUrl,
        bool $isSubscribed,
        ?CarbonImmutable $closedAt,
        ?CarbonImmutable $lastActivityAt,
        ?CarbonImmutable $lastView
    ): void {
        $this->updateAggregateRoot(
            [
                'name' => $name,
                'description' => $description,
                'isClosed' => $isClosed,
                'link' => $link,
                'shortUrl' => $shortUrl,
                'isSubscribed' => $isSubscribed,
                'closedAt' => $closedAt,
                'lastActivityAt' => $lastActivityAt,
                'lastView' => $lastView,
            ],
            BoardUpdated::class,
        );
    }

    protected function applyChanges(Collection $changes): void
    {
        $this->applyChangesByPropertyMap([
            'name' => fn($value) => $this->name = $value,
            'description' => fn($value) => $this->description = $value,
            'isClosed' => fn($value) => $this->isClosed = $value,
            'link' => fn($value) => $this->link = $value,
            'shortUrl' => fn($value) => $this->shortUrl = $value,
            'isSubscribed' => fn($value) => $this->isSubscribed = $value,
            'closedAt' => fn($value) => $this->closedAt = $value,
            'lastActivityAt' => fn($value) => $this->lastActivityAt = $value,
            'lastView' => fn($value) => $this->lastView = $value,
        ], $changes);
    }
}



declare(strict_types=1);

namespace App\Infrastructure\Repositories\Trello;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Lava83\DddFoundation\Infrastructure\Repositories\Repository;
use App\Domain\TrelloManagement\Contracts\Board\BoardRepositoryInterface;
use App\Domain\TrelloManagement\Entities\Board;
use App\Domain\TrelloManagement\ValueObjects\Identity\BoardId;
use App\Infrastructure\Mappers\Trello\BoardMapper;
use App\Infrastructure\Models\Trello\Board\BoardModel;

class EloquentBoardRepository extends Repository implements BoardRepositoryInterface
{
    protected string $aggregateClass = Board::class;

    public function exists(BoardId $boardId): bool
    {
        return BoardModel::where('trello_id', $boardId->toString())->exists();
    }

    public function find(BoardId $boardId): ?Board
    {
        $model = BoardModel::with(['boardLists', 'webhook'])
            ->find($boardId->toString());

        return $model ? BoardMapper::toEntity($model, true) : null;
    }

    public function findOrFail(BoardId $boardId): Board
    {
        return BoardMapper::toEntity(
            BoardModel::with(['boardLists', 'webhook'])
                ->findOrFail($boardId->toString()),
            true
        );
    }

    public function findAll(): Collection
    {
        return BoardModel::with(['boardLists', 'webhook'])
            ->latest()
            ->get()
            ->map(fn(BoardModel $model) => BoardMapper::toEntity($model, true));
    }

    public function save(Board $board): void
    {
        DB::transaction(fn() => $this->saveEntity($board));
    }

    public function delete(Board $board): void
    {
        // Implementation
    }
}



declare(strict_types=1);

namespace App\Infrastructure\Mappers\Trello;

use Lava83\DddFoundation\Domain\Entities\Entity;
use Lava83\DddFoundation\Infrastructure\Contracts\EntityMapper;
use Lava83\DddFoundation\Infrastructure\Models\Model;
use App\Domain\TrelloManagement\Entities\Member;
use App\Domain\TrelloManagement\ValueObjects\Identity\MemberId;
use App\Infrastructure\Models\Trello\Member\MemberModel;

class MemberMapper implements EntityMapper
{
    /**
     * @param MemberModel $model
     */
    public static function toEntity(Model $model, bool $deep = false): Entity
    {
        $member = new Member(
            trelloId: MemberId::fromString($model->trello_id),
            fullName: $model->full_name,
            username: $model->username,
        );

        $member->hydrate($model);

        return $member;
    }

    /**
     * @param Member $entity
     */
    public static function toModel(Entity $entity): MemberModel
    {
        $data = [
            'trello_id' => $entity->trelloId(),
            'full_name' => $entity->fullName(),
            'username' => $entity->username(),
            'created_at' => $entity->createdAt(),
            'updated_at' => $entity->updatedAt(),
            'version' => $entity->version(),
        ];

        $member = MemberModel::findOr(
            $entity->id(), 
            ['*'], 
            fn() => app(MemberModel::class)
        );
        $member->fill($data);

        return $member;
    }
}



declare(strict_types=1);

namespace App\Infrastructure\Mappers;

use Lava83\DddFoundation\Domain\Entities\Aggregate;
use Lava83\DddFoundation\Infrastructure\Contracts\EntityMapper;
use Lava83\DddFoundation\Infrastructure\Contracts\EntityMapperResolver as EntityMapperResolverContract;
use App\Domain\TrelloManagement\Entities\Board;
use App\Domain\TrelloManagement\Entities\Member;
use App\Infrastructure\Mappers\Trello\BoardMapper;
use App\Infrastructure\Mappers\Trello\MemberMapper;

class EntityMapperResolver implements EntityMapperResolverContract
{
    /**
     * @param class-string<Aggregate> $entityClass
     */
    public function resolve(string $entityClass): EntityMapper
    {
        return match ($entityClass) {
            Board::class => app(BoardMapper::class),
            Member::class => app(MemberMapper::class),
            default => throw new NoMapperFoundForEntity($entityClass),
        };
    }
}

$this->app->singleton(
    EntityMapperResolverContract::class,
    EntityMapperResolver::class,
);



declare(strict_types=1);

namespace App\Domain\TrelloManagement\Events;

use Lava83\DddFoundation\Domain\Events\DomainEvent;

class BoardCreated extends DomainEvent
{
    public function eventName(): string
    {
        return 'trello.board.created';
    }
}



declare(strict_types=1);

namespace App\Application\Listeners;

use Illuminate\Events\Dispatcher;
use App\Domain\TrelloManagement\Events\BoardCreated;
use App\Domain\TrelloManagement\Events\BoardUpdated;

class BoardEventSubscriber
{
    public function handleBoardCreated(BoardCreated $event): void
    {
        // Handle board creation
        // e.g., send notifications, update read models, etc.
    }

    public function handleBoardUpdated(BoardUpdated $event): void
    {
        // Handle board updates
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            BoardCreated::class => 'handleBoardCreated',
            BoardUpdated::class => 'handleBoardUpdated',
        ];
    }
}

protected $subscribe = [
    BoardEventSubscriber::class,
];



declare(strict_types=1);

namespace App\Application\Services\Trello\Board;

use Illuminate\Support\Collection;
use Lava83\DddFoundation\Domain\ValueObjects\Communication\Link;
use App\Domain\TrelloManagement\Contracts\Board\BoardRepositoryInterface;
use App\Domain\TrelloManagement\Contracts\Board\BoardServiceInterface;
use App\Domain\TrelloManagement\Entities\Board;
use App\Domain\TrelloManagement\ValueObjects\Identity\BoardId;

class BoardApplicationService implements BoardServiceInterface
{
    public function __construct(
        private BoardRepositoryInterface $boardRepository,
    ) {}

    public function listBoards(): Collection
    {
        return $this->boardRepository->findAll();
    }

    public function board(string $boardId): Board
    {
        return $this->boardRepository->findOrFail(
            BoardId::fromString($boardId)
        );
    }

    public function createBoard(array $data): Board
    {
        $board = Board::create(
            trelloId: BoardId::fromString($data['trello_id']),
            name: $data['name'],
            description: $data['description'],
            isClosed: $data['is_closed'],
            link: Link::fromString($data['link']),
            shortUrl: Link::fromString($data['short_url']),
            isSubscribed: $data['is_subscribed'],
            closedAt: $data['closed_at'] ?? null,
            lastActivityAt: $data['last_activity_at'] ?? null,
            lastView: $data['last_view'] ?? null,
        );

        $this->boardRepository->save($board);

        return $board;
    }

    public function updateBoard(string $boardId, array $data): Board
    {
        $board = $this->boardRepository->findOrFail(
            BoardId::fromString($boardId)
        );

        $board->update(
            name: $data['name'],
            description: $data['description'],
            isClosed: $data['is_closed'],
            link: Link::fromString($data['link']),
            shortUrl: Link::fromString($data['short_url']),
            isSubscribed: $data['is_subscribed'],
            closedAt: $data['closed_at'] ?? null,
            lastActivityAt: $data['last_activity_at'] ?? null,
            lastView: $data['last_view'] ?? null,
        );

        $this->boardRepository->save($board);

        return $board;
    }
}



declare(strict_types=1);

namespace App\Domain\Shared\ValueObjects;

use Carbon\CarbonImmutable;
use Lava83\DddFoundation\Domain\ValueObjects\ValueObject;

class DateRange extends ValueObject
{
    private function __construct(
        private CarbonImmutable $startDate,
        private CarbonImmutable $endDate,
    ) {
        $this->validate();
    }

    public static function create(
        CarbonImmutable $startDate, 
        CarbonImmutable $endDate
    ): self {
        return new self($startDate, $endDate);
    }

    public function startDate(): CarbonImmutable
    {
        return $this->startDate;
    }

    public function endDate(): CarbonImmutable
    {
        return $this->endDate;
    }

    public function contains(CarbonImmutable $date): bool
    {
        return $date->greaterThanOrEqualTo($this->startDate)
            && $date->lessThanOrEqualTo($this->endDate);
    }

    public function overlaps(DateRange $other): bool
    {
        return $this->startDate->lessThanOrEqualTo($other->endDate)
            && $other->startDate->lessThanOrEqualTo($this->endDate);
    }

    public function durationInDays(): int
    {
        return $this->startDate->diffInDays($this->endDate);
    }

    protected function validate(): void
    {
        if ($this->startDate->greaterThan($this->endDate)) {
            throw new \InvalidArgumentException(
                'Start date must be before or equal to end date'
            );
        }
    }

    public function toString(): string
    {
        return sprintf(
            '%s to %s',
            $this->startDate->format('Y-m-d'),
            $this->endDate->format('Y-m-d')
        );
    }

    public function toArray(): array
    {
        return [
            'start_date' => $this->startDate->toDateString(),
            'end_date' => $this->endDate->toDateString(),
        ];
    }

    public function equals(mixed $other): bool
    {
        if (!$other instanceof self) {
            return false;
        }

        return $this->startDate->equalTo($other->startDate)
            && $this->endDate->equalTo($other->endDate);
    }
}

use Lava83\DddFoundation\Domain\ValueObjects\Identity\Uuid;
use Lava83\DddFoundation\Domain\ValueObjects\Identity\MongoObjectId;
use Lava83\DddFoundation\Domain\ValueObjects\Communication\Email;
use Lava83\DddFoundation\Domain\ValueObjects\Communication\Link;
use Lava83\DddFoundation\Domain\ValueObjects\Data\Json;

// UUID
$userId = Uuid::generate();
$userId = Uuid::fromString('550e8400-e29b-41d4-a716-446655440000');

// MongoDB ObjectId (compatible with Trello IDs)
$boardId = MongoObjectId::fromString('507f1f77bcf86cd799439011');

// Email
$email = Email::fromString('[email protected]');
echo $email->toString(); // [email protected]

// Link/URL
$url = Link::fromString('https://example.com');
echo $url->toString(); // https://example.com

// JSON Data
$json = Json::fromArray(['key' => 'value']);
$data = $json->toArray();

$board = Board::create(/* ... */);
$board->update(/* ... */); // Records BoardUpdated event

$boardRepository->save($board); 
// 1. Saves to database
// 2. Automatically dispatches all uncommitted events via Laravel's event system
// 3. Clears uncommitted events from the aggregate

// User A loads board
$boardA = $boardRepository->find($boardId);
$versionA = $boardA->version(); // version = 1

// User B loads same board
$boardB = $boardRepository->find($boardId);

// User B updates and saves
$boardB->update(/* ... */);
$boardRepository->save($boardB); // version now = 2

// User A tries to save
$boardA->update(/* ... */);
$boardRepository->save($boardA); 
// Throws ConcurrencyException because version mismatch



namespace App\Infrastructure\Models\Trello\Board;

use Lava83\DddFoundation\Infrastructure\Models\Model;

class BoardModel extends Model
{
    protected $table = 'trello_boards';
    protected $primaryKey = 'trello_id'; // Uses UUID/MongoObjectId
    
    protected $fillable = [
        'trello_id',
        'name',
        'description',
        // ...
    ];
}

public function save(Board $board): void
{
    DB::transaction(fn() => $this->saveEntity($board));
}



namespace App\Infrastructure\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\TrelloManagement\Contracts\Board\BoardRepositoryInterface;
use App\Infrastructure\Repositories\Trello\EloquentBoardRepository;

class RepositoriesServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            BoardRepositoryInterface::class, 
            EloquentBoardRepository::class
        );
        
        // Register other repositories...
    }
}



namespace App\Application\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\TrelloManagement\Contracts\Board\BoardServiceInterface;
use App\Application\Services\Trello\Board\BoardApplicationService;

class ApplicationServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            BoardServiceInterface::class,
            BoardApplicationService::class
        );
        
        // Register other services...
    }
}

// ✅ Good - Uses generic Collection from illuminate/support
use Illuminate\Support\Collection;

class Board extends Aggregate
{
    public function __construct(
        // ...
        protected Collection $lists,
    ) {
        parent::__construct();
    }
}

// ❌ Avoid - Don't use Eloquent in domain layer
use Illuminate\Database\Eloquent\Collection;

public function activeBoards(): Collection
{
    return collect($this->boards)->filter(
        fn(Board $board) => !$board->isClosed()
    );
}

// ✅ Good - Clear intent
$board = Board::create($id, $name, $description, ...);

// ❌ Avoid - Using new directly
$board = new Board($id, $name, $description, ...);

public function update(/* params */): void
{
    $this->updateAggregateRoot(
        [/* changes */],
        BoardUpdated::class, // Always provide event class
    );
}

public function save(Board $board): void
{
    DB::transaction(fn() => $this->saveEntity($board));
}

public function resolve(string $entityClass): EntityMapper
{
    return match ($entityClass) {
        YourEntity::class => app(YourEntityMapper::class),
        // ...
    };
}

try {
    $repository->save($board);
} catch (ConcurrencyException $e) {
    // Reload entity and retry, or notify user
    $board = $repository->findOrFail($boardId);
    // Reapply changes...
}