0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript Transformer + modeltyper】最強の PHP Type Generator をつくる!(Type-統合編)

0
Last updated at Posted at 2025-12-03

前二つの記事。

この二つ、Enum にも対応してるのですが、それぞれで生成してしまうと共有ができない...

Laravel Data は Model の型生成には対応していないんです。。。
Resource DTO を作成して、Model をラップする方針のようです。

DDD としては正解なんだけど、、、やだーーー
Repository もコスト分の納得できてないのに!
(でかい集計しか逃がさない)

Typescript Transformer には自作で PHP のクラスから TS の型を生成するプラグインを作る機能が搭載されています。
それなら、組み込んでしまえ!

作り方

てことで TypeScript Transformer にて modeltyper を実行する ModelTransformer を自作しました。
本当は Model から型を生成するスクリプトを書こうと思ったんですけど、考慮事項が多く、 modeltyper が優秀すぎるので、頼ったほうが良いと判断しました。

見つけた Model クラスファイルに対して、modeltyper を実行して、文字列としてゴリゴリに加工していきます。
もし特殊な事例があっても、このスクリプトに処理を挟み込めばよいのでお手軽ですね。

詳細な説明は、コード内のコメントに記載しました。

app/Utils/ModelTransformer.php
<?php

namespace App\Utils;

use FumeApp\ModelTyper\Actions\Generator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use ReflectionClass;
use Spatie\TypeScriptTransformer\Structures\TransformedType;
use Spatie\TypeScriptTransformer\Transformers\Transformer;
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
use FumeApp\ModelTyper\Overrides\ModelInspector;

class ModelTransformer implements Transformer
{
    public function __construct(protected TypeScriptTransformerConfig $config)
    {
    }

    public function transform(ReflectionClass $class, string $name): ?TransformedType
    {
        if (! $class->isSubclassOf(Model::class)) {
            return null;
        }

        // (1) 利用 Class を集める
        $inspector = app(ModelInspector::class);
        $inspect = $inspector->inspect($class->getName());

        $modelMaps = collect()
            // cast で利用するキャストを取得
            ->merge(collect(data_get($inspect, 'attributes'))->pluck('cast'))
            // relation で利用するキャストを取得
            ->merge(collect(data_get($inspect, 'relations'))->pluck('related'))
            // 大文字で始まるものを抽出
            ->filter(fn ($attr) => Str::of($attr)->test('/^[A-Z]/'))
            // 一意にする
            ->unique()
            // 使いやすいように [ClassName => Namespace] に変換
            ->mapWithKeys(fn ($attr) => [
                Str::of($attr)->afterLast('\\')->toString() =>
                Str::of($attr)->replace('\\', '.')->toString()
            ]);

        // (2) Model to Type
        $modelTyper = app(Generator::class)(
            specificModel: $class->getName(),
        );

        // type を成型する
        $format = Str::of($modelTyper)
            // 装飾子削除
            ->replaceMatches('/export interface \w+ /s', '')
            // 下部 enum 削除
            ->replaceMatches('/.const.*/s', '')
            // 末尾改行削除
            ->replaceMatches('/(\n|\r\n|\r)$/s', '')
            // 大文字クラス置換
            ->replaceMatches('/ ([A-Z]\w*)/', fn ($match) =>
                ' '.$modelMaps->get(data_get($match, 1), data_get($match, 1),)
            );

        return TransformedType::create(
            $class,
            $name,
            $format,
        );
    }
}

これを TypeScript 生成処理に組み込みます。
併せて、必要な設定を調整します。

config/typescript-transformer.php
<?php

return [
    ...,

    'transformers' => [
+       App\Utils\ModelTransformer::class,
        Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer::class,
        Spatie\TypeScriptTransformer\Transformers\EnumTransformer::class,
        Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class,
        Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer::class,
    ],

    'writer' => Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter::class,

+   'transform_to_native_enums' => false,

+   'transform_null_to_optional' => true,
];

そして、対象の Model には #[Typescript] を忘れずに付与しましょう。

+ use Spatie\TypeScriptTransformer\Attributes\TypeScript;

+ #[TypeScript()]
  class Post extends Model
  { ...

これで実行すると...?

$ php artisan typescript:transform

一つのふぁいるとして、出てきます!

resources/types/generated.d.ts
declare namespace App.Data {
  export type PostData = {
    user_id: number;
    title: string;
    body?: string;
    tags: Array<App.Data.PostTagData>;
  };
  // ...
}

declare namespace App.Models {
  export type Post = {
    // columns
    id: number
    user_id: number
    title: string
    body: string
    created_at: string | null
    updated_at: string | null
    // relations
    user: App.Models.User
    tags: App.Models.PostTag[]
    // counts
    tags_count: number
    // exists
    user_exists: boolean
    tags_exists: boolean
  };
  // ...
}

おわりに

私のユースではこちらで十分な機能ですね。
もし namespace なんていらねぇ!ってことれあれば各自調整してくだしあ。

プラグインとして公開も考えていたのですが、処理が汚いので Qiita にとどめておきます。
巨人の肩に乗ろうキャンペーン実施中。

これで長かった型周りのお話はおしまいです!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?