PHPStan(Larastan) level maxが通るようにLaravelでクリーンアーキテクチャを実装

# はじめに
実務では静的解析ツール(Larastan)を最高レベルに設定してしまうと警告が多発してしまうため控えておりましたが、
個人開発で最高レベルに設定しても問題ございませんように実装ができましたので参考になれば幸いです。

### Larastan導入に関する参考記事
https://qiita.com/Zucky2022/items/861dabe5e3c8e372b234

### Larastanの設定レベルに関する参考記事
https://qiita.com/shimabox/items/df03dde8bd6db4733231

### クリーンアーキテクチャとは
https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

<details>
<summary>今回の静的解析設定ファイル</summary>

```phpstan.neon
includes:
    - ./vendor/larastan/larastan/extension.neon

parameters:
    level: max
    paths:
        - app
        - bootstrap
        - database
        - routes
```
</details>

# 結論
- レイヤーを跨ぎデータを受け渡す際にはDTO(Data Transfer Object)を使用する
- 配列(Collectionを含む)を使用しない
- repositoryでDTOを生成する際にstdClassを使用する
    - <font color="Red">後ほど詳細に説明いたします</font>

# 前提
- Laravel version: 11.14.0

### その他意識したこと
- Frameworksに依存しないようにDomainフォルダ内でLaravelの固有のクラス(collection, carbon, etc.)を使用しない

# フォルダ構成

:::note warn
routesやresourcesや今回の記事に不要な処理などは除いております
:::

```
.
└── app/
    ├── Domain/
    │   ├── Project/
    │   │   ├── ValueObject/
    │   │   │   ├── Email.php
    │   │   │   └── Name.php
    │   │   ├── ProjectDto.php
    │   │   ├── ProjectEntity.php
    │   │   ├── ProjectFactory.php
    │   │   └── ProjectRepositoryInterface.php
    │   └── ...
    ├── Http/
    │   └── Controllers/
    │       └── ProjectController.php
    ├── Infrastructure/
    │   └── Repositories/
    │       └── ProjectRepository.php
    └── UseCases/
        └── Project/
            └── ProjectFindAction.php
```

# 各ファイル内容

<details>
<summary>ProjectController.php</summary>

:::note info
コントローラでDTOからEntityを生成
:::

```ProjectController.php
<?php

namespace App\Http\Controllers;

use App\Domain\Project\ProjectFactory;
use App\UseCases\Project\ProjectFindAction;
use Inertia\Response;

class ProjectController extends Controller
{
    /**
     * プロジェクト一覧画面
     *
     * @param \App\UseCases\Project\ProjectFindAction $projectFindAction
     * @return \Inertia\Response
     */
    public function index(ProjectFindAction $projectFindAction): Response
    {
        $projectDtoArr   = $projectFindAction->findAll();
        $projectEntities = array_map(function ($dto) {
            return ProjectFactory::create($dto);
        }, $projectDtoArr);

        // ...その他画面表示などの処理
    }
}
```
</details>

<details>
<summary>ProjectFindAction.php</summary>

:::note info
DTOの一次元配列であることをPHPDocで明示的に宣言
:::
```ProjectFindAction.php
<?php

namespace App\UseCases\Project;

use App\Domain\Project\ProjectDto;
use App\Domain\Project\ProjectRepositoryInterface;

/**
 * Use cases for project acquisition
 */
final class ProjectFindAction
{
    private ProjectRepositoryInterface $repository;

    public function __construct(ProjectRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    /**
     * プロジェクトを全て取得
     *
     * @return ProjectDto[]
     */
    public function findAll(): array
    {
        return $this->repository->findAll();
    }
```
</details>

<details>
<summary>ProjectRepositoryInterface.php</summary>

```ProjectRepositoryInterface.php
<?php

namespace App\Domain\Project;

/**
 * Interface with external(DB)
 */
interface ProjectRepositoryInterface
{
    public const TABLE_NAME = 'projects';

    /**
     * 全てのプロジェクトを取得
     *
     * @return ProjectDto[]
     */
    public function findAll(): array;
```
</details>

<details>
<summary>ProjectRepository.php</summary>

:::note info
DTOの一次元配列であることを関数を対象とするもの(interfaceで宣言済)とは別に返り値を対象にPHPDocで明示的に宣言
:::

```ProjectRepository.php
<?php

namespace App\Infrastructure\Repositories;

use App\Domain\Project\ProjectDto;
use App\Domain\Project\ProjectRepositoryInterface;
use Illuminate\Support\Facades\DB;
use stdClass;

/**
 * Project DB repository
 */
final class ProjectRepository implements ProjectRepositoryInterface
{
    public function findAll(): array
    {
        /** @var ProjectDto[] */
        return DB::table(ProjectRepositoryInterface::TABLE_NAME)
            ->whereNull('deleted_at')
            ->get()
            ->map(function ($value) {
                /** @var stdClass $value */
                return new ProjectDto(
                    id: $value->id,
                    departmentId: $value->department_id,
                    name: $value->name,
                    summary: $value->summary,
                );
            })
            ->toArray();
    }
```

### なぜstdClassで宣言しているかについて　
#### <font color="Red">こちらが今回の記事の重要なポイントです</font>
- そのままの状態では対象のプロパティが存在しないと警告が発生
    - `id: $value->id,`
- [stdClass - 公式ドキュメント](https://www.php.net/manual/ja/class.stdclass.php)
- DTOで宣言してしまうとカラム名のスネークケースとPHPで一般的なキャメルケースで齟齬が生まれてしまい警告が発生
- その他Modelクラスなどで宣言してしまうとテーブルを結合した際やカラム名に対してエイリアスを使用した際に警告が発生
- 以上の理由から動的プロパティは非推奨ですが警告の発生を抑えるため限られた範囲内でのみ使用を許可いたしましたが...**より良い方法があればご教示いただきたいです！**

</details>

<details>
<summary>ProjectFactory.php</summary>

:::note info
DTOからビジネスロジックをプロパティに持たせる(値オブジェクトを使用する)形でEntityを生成
:::

```ProjectFactory.php
<?php

namespace App\Domain\Project;

use App\Domain\Project\ValueObject\Name;
use App\Domain\Project\ValueObject\Summary;

final class ProjectFactory
{
    public static function create(ProjectDto $dto): ProjectEntity
    {
        return new ProjectEntity(
            $dto->id,
            $dto->departmentId,
            new Name($dto->name),
            new Summary($dto->summary),
        );
    }
}
```
</details>

<details>
<summary>Name.php</summary>

```Name.php
<?php

namespace App\Domain\Project\ValueObject;

use App\Domain\ValueObjectInterface\StringValue;
use InvalidArgumentException;

/**
 * プロジェクト名VO
 */
final class Name implements StringValue
{
    public const MAX_LEN = 100;

    public function __construct(private string $value)
    {
        $this->validate($value);
        $this->value = $value;
    }

    public function validate(string $value): void
    {
        if (mb_strlen($value) > self::MAX_LEN) {
            throw new InvalidArgumentException(
                sprintf(
                    '%s must be %d or less characters, got %d.',
                    __CLASS__,
                    self::MAX_LEN,
                    mb_strlen($value),
                ),
            );
        }
    }

    public function value(): string
    {
        return $this->value;
    }
}
```
:::note info
※StringValue interfaceではvalidateメソッドの実装の強制とvalueメソッドでstring型を返すことを強制しております。
:::
Summary.phpに関しましては、Name.phpと実装がほぼ同じであるため記述を省略しております。
</details>

<details>
<summary>ProjectEntity.php</summary>

:::note info
今後プロパティに変更が生じさせないためイミュータブル(不変)になるように定義
:::
```ProjectEntity.php
<?php

namespace App\Domain\Project;

use App\Domain\Project\ValueObject\Name;
use App\Domain\Project\ValueObject\Summary;

/**
 * プロジェクトentity
 */
final class ProjectEntity
{
    private ?int $id;
    private int $departmentId;
    private Name $name;
    private Summary $summary;

    public function __construct(
        private ?int $id,
        private int $departmentId,
        private Name $name,
        private Summary $summary,
    ) {
        $this->id           = $id;
        $this->departmentId = $departmentId;
        $this->name         = $name;
        $this->summary      = $summary;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

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

    public function getName(): Name
    {
        return $this->name;
    }

    public function getSummary(): Summary
    {
        return $this->summary;
    }

    /**
     * Property to array
     *
     * @return array<string, int|null|string>
     */
    public function toArray(): array
    {
        return [
            'id'           => $this->id,
            'departmentId' => $this->departmentId,
            'name'         => $this->name->value(),
            'summary'      => $this->summary->value(),
        ];
    }
}
```

:::note info
もしプロパティを連想配列で返す場合はPHPDocでデータ型を細かく指定
:::

</details>

# 余談
- こちらのサイトですがフォルダ構成を表現する際にとても助かりました:bow:

https://tree.nathanfriend.io/

- Laravelのライブラリの[Carbon](https://readouble.com/laravel/10.x/ja/helpers.html#dates)ですがFrameworksに依存させるさせないの前に継承元のDateTimeクラスが非推奨になったため、そもそも使用を避けておりましたが、しっかりDateTimeImmutableクラスを継承したCarbonImmutableが存在することを最近知りました

https://qiita.com/kbys-fumi/items/b923cdfb09c8f5c35fce

- 今回作成したドメインですが今後オーバーヘッドが増えそうなので、DBに連動した**CoreDomain**、アプリケーションの操作に連動した**Domain**など区別して定義しても良いかと感じました。