ChatGPT活用ガイド!PHPでDDDスタイルのコードを自動生成

こんにちは。GMOインターネットグループ株式会社の長谷川です。

プロジェクト統括チームに所属しており、普段はプロジェクトマネジメント業務を担当し、プロジェクトの進捗管理や課題管理を行っています。元々はプログラマーを行っておりましたが、最近はプログラムに触れる機会も多くあり、DDDってなんだろう、と思う機会も多くあります。

今回は、コーディングを行わなくなった元エンジニアでも簡単にツールを作成できる手段としてChatGPTを利用する方法をご紹介します。

はじめに

記事の対象者

本記事は、PHPやLaravelフレームワーク、DDD(ドメイン駆動設計)に少し知識がある方であれば理解できる内容となっております。
また、過去にプログラミングを行っていただけれども、最近あまり触れていないなどの方も今後のコーディング手段としてご利用できる内容となっております。

注意点

本記事の情報は、著者の個人的な見解に基づくものであり、正確性などを保証するものではありません。

また実際とは異なるものを作成しております。他の方法で効率が良い方法もありますが、一例としてご理解ください。

今回の実施内容

処理内容としては、約200万行のCSVファイルを読み込み、データベースに保存する処理です。
正直、CSVインポート機能を利用した方が早いです。

個人的な見解としては、1回の命令文で回答を求めるのではなく、複数回の命令を繰り返し、少しずつ情報をインプットさせることで、より良い結果が得られるのではないかと思います。
今回は、そのようなケースについて実施した内容をご紹介します。

実行環境

ChatGPT 4o
AlmaLinux 9
PHP 8.3
Laravel Framework 10
MySQL 8.0

1回の命令文で結果を求めた場合

プロンプト内容

#命令文
以下の条件に従ってPHPのコードを記述してください。

#条件
プロジェクト名:ImportIpAddress
OS:AlmaLinux 9
言語:PHP 8.3
DB:Mysql 8.0
フレームワーク:Laravel Framework 10
実行形式:コマンドラインアプリケーション
設計手法:DDD
その他:
コマンドクラスはphp artisan make:commandで作成する
取得したデータはEntityクラスで取り扱う

#仕様
・CSVファイルを読み込む(約200万件)
 データ保存場所:storage/app/private
 ファイル名:ip_address.csv
 ファイル形式:CSV
 カラム情報:"ACCOUNTID","CREATEDDATE","ROLE","DOMAINNEWSFLG","ORGANIZATIONML","DOMAINID","DOMAINNAMEML","REGISTRYCREATEDDATE","EXPIRATIONDATE","HOSTNAME","RECORDTYPE","RECVALUE","RECORDSTATUS","REFLECTSTATUS"
・RECORDTYPEが20以外の行はスキップする。
・DBに保存する
 テーブル名:tbl_ip_addresses
 カラム名:
  id int(11) primary key
  domain_id int(11) not null
  domain_name string(255) not null
  ip_address string(15) not null
既にDOMAINID、RECVALUEが重複したデータがDBに存在する場合には書き込みを実施しない

ChatGPT出力結果

以下は、条件に基づいてコマンドラインアプリケーションのPHPコードを記述したものです。Laravelのコマンドクラスを使い、DDD設計手法に従って、CSVファイルを読み込み、条件に合致するデータをデータベースに保存するコードです。


まず、コマンドクラスを作成します:

php artisan make:command ImportIpAddresses

生成されたコマンドクラス ImportIpAddresses.php を以下のように編集します。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Domain\Services\IpAddressService;

class ImportIpAddresses extends Command
{
    protected $signature = 'import:ip-addresses';
    protected $description = 'Import IP addresses from CSV file';

    private $ipAddressService;

    public function __construct(IpAddressService $ipAddressService)
    {
        parent::__construct();
        $this->ipAddressService = $ipAddressService;
    }

    public function handle()
    {
        $filePath = storage_path('app/private/ip_address.csv');
        $this->ipAddressService->importFromCsv($filePath);
    }
}

次に、サービスクラス IpAddressService.php を作成します。

<?php

namespace App\Domain\Services;

use App\Domain\Entities\IpAddress;
use App\Domain\Repositories\IpAddressRepository;
use League\Csv\Reader;

class IpAddressService
{
    private $ipAddressRepository;

    public function __construct(IpAddressRepository $ipAddressRepository)
    {
        $this->ipAddressRepository = $ipAddressRepository;
    }

    public function importFromCsv($filePath)
    {
        $csv = Reader::createFromPath($filePath, 'r');
        $csv->setHeaderOffset(0);

        foreach ($csv as $record) {
            if ($record['RECORDTYPE'] != 20) {
                continue;
            }

            $ipAddress = new IpAddress(
                $record['DOMAINID'],
                $record['DOMAINNAMEML'],
                $record['RECVALUE']
            );

            if (!$this->ipAddressRepository->exists($ipAddress)) {
                $this->ipAddressRepository->save($ipAddress);
            }
        }
    }
}

エンティティクラス IpAddress.php を作成します。

<?php

namespace App\Domain\Entities;

class IpAddress
{
    private $domainId;
    private $domainName;
    private $ipAddress;

    public function __construct($domainId, $domainName, $ipAddress)
    {
        $this->domainId = $domainId;
        $this->domainName = $domainName;
        $this->ipAddress = $ipAddress;
    }

    public function getDomainId()
    {
        return $this->domainId;
    }

    public function getDomainName()
    {
        return $this->domainName;
    }

    public function getIpAddress()
    {
        return $this->ipAddress;
    }
}

リポジトリクラス IpAddressRepository.php を作成します。

<?php

namespace App\Domain\Repositories;

use App\Domain\Entities\IpAddress;
use Illuminate\Support\Facades\DB;

class IpAddressRepository
{
    public function exists(IpAddress $ipAddress)
    {
        return DB::table('tbl_ip_addresses')
            ->where('domain_id', $ipAddress->getDomainId())
            ->where('ip_address', $ipAddress->getIpAddress())
            ->exists();
    }

    public function save(IpAddress $ipAddress)
    {
        DB::table('tbl_ip_addresses')->insert([
            'domain_id' => $ipAddress->getDomainId(),
            'domain_name' => $ipAddress->getDomainName(),
            'ip_address' => $ipAddress->getIpAddress()
        ]);
    }
}

最後に、テーブルを作成するためのマイグレーションを実行します。

php artisan make:migration create_tbl_ip_addresses_table

生成されたマイグレーションファイルを以下のように編集します。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTblIpAddressesTable extends Migration
{
    public function up()
    {
        Schema::create('tbl_ip_addresses', function (Blueprint $table) {
            $table->id();
            $table->integer('domain_id');
            $table->string('domain_name', 255);
            $table->string('ip_address', 15);
            $table->timestamps();

            $table->unique(['domain_id', 'ip_address']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('tbl_ip_addresses');
    }
}

これで、コマンドを実行してCSVファイルを読み込み、データベースに保存することができます。

php artisan migrate
php artisan import:ip-addresses

結果に対するコメント(1回の命令文で結果を求めた場合)

それっぽく作成されてはいるのですが、”App\Domain\Repositories”にあるファイルがインターフェースではなく実装クラスとなっています。
本来は、”App\Domain\Repositories”はインターフェースクラスとなり、”App\Infrastructure\Repositories”に実装クラスがあるのが良いのではないかと思います。

複数回の命令文で結果を求めた場合

命令文(1回目)

プロンプト内容

#命令文
以下の条件でプログラムを作成する
適切なディレクトリ構造を提案してください

#条件
プロジェクト名:ImportIpAddress
OS:AlmaLinux 9
言語:PHP 8.3
DB:Mysql 8.0
フレームワーク:Laravel Framework 10
実行形式:コマンドラインアプリケーション
設計手法:DDD

ChatGPT出力結果

以下は、ImportIpAddress プロジェクトのためのディレクトリ構造です。

これはドメイン駆動設計 (DDD) の原則に基づいています。

ImportIpAddress/
├── app/
│   ├── Console/
│   │   └── Commands/
│   ├── Domain/
│   │   ├── Models/           # エンティティや値オブジェクトを格納
│   │   ├── Repositories/     # リポジトリインターフェースを格納
│   │   ├── Services/         # ドメインサービスを格納
│   │   └── Exceptions/       # ドメイン固有の例外を格納
│   ├── Infrastructure/
│   │   ├── Persistence/      # 実際のリポジトリの実装を格納
│   │   ├── CSV/              # CSVファイルの読み込みに関する処理を格納
│   │   ├── MySQL/            # MySQLへのデータ保存に関する処理を格納
│   │   └── Services/         # インフラサービスを格納
│   └── Application/
│       ├── Services/         # アプリケーションサービスを格納
│       └── DTOs/             # データ転送オブジェクトを格納
├── config/
├── database/
│   ├── migrations/
│   └── seeders/
├── routes/
│   └── console.php           # コマンドラインルートを定義
├── storage/
│   ├── app/
│   │   └── private/
│   │       └── ip_address.csv # CSVファイルの保存場所
│   └── logs/
├── tests/
│   ├── Unit/
│   └── Feature/
├── .env
├── composer.json
└── artisan

各ディレクトリの説明
・app/Console/Commands/: カスタムArtisanコマンドを配置します。
・app/Domain/: ドメイン層を表現し、ビジネスロジックを含むエンティティ、リポジトリインターフェース、サービスなどを含みます。
・app/Infrastructure/: インフラ層を表現し、データベースアクセス、外部サービスとの通信などを担当します。
・app/Application/: アプリケーション層を表現し、アプリケーションサービスやDTO(データ転送オブジェクト)を含みます。
・routes/console.php: コマンドラインルートを定義するためのファイルです。
・storage/app/private/: プライベートデータの保存場所です。

この構造を使用して、DDDの原則に従った堅牢で拡張性のあるアプリケーションを開発できます。

命令文(2回目)

プロンプト内容

#命令文
DDDの各ディレクトリのクラスの呼び出し可否のルールを教えてください。

ChtGPT出力結果

DDDの原則に基づいた各ディレクトリのクラスの呼び出し可否のルールについて説明します。以下のルールを参考にしてください。

ディレクトリと呼び出しルール

1.Domain Layer
・ Models: 他のドメインモデル、ドメインサービス、リポジトリから呼び出されます。インフラストラクチャ層やアプリケーション層から直接呼び出さない。
・ Repositories: ドメインサービスやアプリケーションサービスから呼び出されます。リポジトリインターフェースの実装はインフラストラクチャ層に置く。
・ Services: 他のドメインサービス、ドメインモデル、アプリケーションサービスから呼び出されます。インフラストラクチャ層から直接呼び出さない。
・ Exceptions: ドメイン層内のエラー処理に使用します。他の層からもキャッチすることは可能ですが、発生はドメイン層内のみ。
2.Application Layer
・ Services: アプリケーションサービスは、ドメイン層のモデルやリポジトリ、インフラストラクチャ層のサービスを呼び出します。ユーザーインターフェース層やコマンドライン層から呼び出される。
・ DTOs: アプリケーション層やインターフェース層でデータのやり取りに使用します。データの変換や転送に特化し、ビジネスロジックを含まない。
3.Infrastructure Layer
・ Persistence: リポジトリの実装が含まれ、ドメイン層のリポジトリインターフェースを実装します。アプリケーションサービスやドメインサービスから呼び出される。
・ CSV: CSVファイルの読み込みや書き込みの処理が含まれ、インフラストラクチャサービスやアプリケーションサービスから呼び出される。
・ MySQL: MySQLへのデータアクセス処理が含まれ、ドメイン層のリポジトリインターフェースの実装が置かれる。アプリケーションサービスやドメインサービスから呼び出される。
・ Services: 外部サービスやインフラストラクチャ固有の処理が含まれ、アプリケーション層やドメイン層から呼び出される。
4.User Interface Layer (CLI, Web, etc.)
・ Console Commands: コマンドラインインターフェース(CLI)のエントリーポイントであり、アプリケーションサービスを呼び出します。ドメイン層やインフラストラクチャ層を直接呼び出さない。

具体的な例

Domain Model (app/Domain/Models): ドメインモデルは他のモデルやサービスとやり取りできますが、インフラストラクチャ層のクラスを直接呼び出すことはありません。

<?php
namespace App\Domain\Models;

class IpAddress
{
    // IpAddressのプロパティとメソッド
}

Domain Service (app/Domain/Services): ドメインサービスはリポジトリインターフェースや他のドメインモデルを使用します。

<?php
namespace App\Domain\Services;

use App\Domain\Repositories\IpAddressRepositoryInterface;

class IpAddressChecker
{
    protected $ipAddressRepository;

    public function __construct(IpAddressRepositoryInterface $ipAddressRepository)
    {
        $this->ipAddressRepository = $ipAddressRepository;
    }

    // ドメインロジックを実装
}

Application Service (app/Application/Services): アプリケーションサービスはドメインサービスやリポジトリを使用します。

<?php
namespace App\Application\Services;

use App\Domain\Services\IpAddressChecker;

class IpAddressCheckService
{
    protected $ipAddressChecker;

    public function __construct(IpAddressChecker $ipAddressChecker)
    {
        $this->ipAddressChecker = $ipAddressChecker;
    }

    // アプリケーションロジックを実装
}

Infrastructure Repository Implementation (app/Infrastructure/Persistence): リポジトリの実装はインフラストラクチャ層に含まれ、ドメインリポジトリインターフェースを実装します。

<?php
namespace App\Infrastructure\Persistence;

use App\Domain\Repositories\IpAddressRepositoryInterface;

class MySqlIpAddressRepository implements IpAddressRepositoryInterface
{
    // MySQLへのデータアクセスロジックを実装
}

このように、DDDの原則に従った呼び出しルールを守ることで、クリーンで保守性の高いコードベースを維持できます。

命令文(3回目)

プロンプト内容

#命令文
以下の条件に従ってPHPのコードを記述してください。

#条件
プロジェクト名:ImportIpAddress
OS:AlmaLinux 9
言語:PHP 8.3
DB:Mysql 8.0
フレームワーク:Laravel Framework 10
実行形式:コマンドラインアプリケーション
設計手法:DDD
その他:
コマンドクラスはphp artisan make:commandで作成する
取得したデータはEntityクラスで取り扱う

#仕様
・CSVファイルを読み込む(約200万件)
 データ保存場所:storage/app/private
 ファイル名:ip_address.csv
 ファイル形式:CSV
 カラム情報:"ACCOUNTID","CREATEDDATE","ROLE","DOMAINNEWSFLG","ORGANIZATIONML","DOMAINID","DOMAINNAMEML","REGISTRYCREATEDDATE","EXPIRATIONDATE","HOSTNAME","RECORDTYPE","RECVALUE","RECORDSTATUS","REFLECTSTATUS"
・RECORDTYPEが20以外の行はスキップする。
・DBに保存する
 テーブル名:tbl_ip_addresses
 カラム名:
  id int(11) primary key
  domain_id int(11) not null
  domain_name string(255) not null
  ip_address string(15) not null
既にDOMAINID、RECVALUEが重複したデータがDBに存在する場合には書き込みを実施しない

ChatGPT出力結果

以下の条件に従って、ImportIpAddressプロジェクトのコードを記述します。DDD設計に基づき、コマンドクラス、エンティティクラス、リポジトリクラス、サービスクラスを定義し、CSVファイルを読み込み、データベースに保存するプロセスを実装します。

1.コマンドクラスの作成


まず、コマンドクラスを作成します。

php artisan make:command ImportIpAddresses

生成された app/Console/Commands/ImportIpAddresses.php を以下のように編集します。

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Application\Services\IpAddressImportService;

class ImportIpAddresses extends Command
{
    protected $signature = 'import:ipaddresses';
    protected $description = 'Import IP addresses from CSV file';

    protected $ipAddressImportService;

    public function __construct(IpAddressImportService $ipAddressImportService)
    {
        parent::__construct();
        $this->ipAddressImportService = $ipAddressImportService;
    }

    public function handle()
    {
        $filePath = storage_path('app/private/ip_address.csv');
        $this->ipAddressImportService->importFromCsv($filePath);
        $this->info('IP addresses imported successfully.');
    }
}

2.エンティティクラスの作成
次に、エンティティクラスを定義します。

<?php
namespace App\Domain\Models;

class IpAddress
{
    public $domainId;
    public $domainName;
    public $ipAddress;

    public function __construct(int $domainId, string $domainName, string $ipAddress)
    {
        $this->domainId = $domainId;
        $this->domainName = $domainName;
        $this->ipAddress = $ipAddress;
    }
}

3.リポジトリインターフェースと実装の作成

リポジトリインターフェースとその実装を作成します。

リポジトリインターフェース

<?php
namespace App\Domain\Repositories;

use App\Domain\Models\IpAddress;

interface IpAddressRepositoryInterface
{
    public function save(IpAddress $ipAddress): bool;
    public function exists(int $domainId, string $ipAddress): bool;
}

リポジトリ実装

<?php
namespace App\Infrastructure\Persistence;

use App\Domain\Models\IpAddress;
use App\Domain\Repositories\IpAddressRepositoryInterface;
use Illuminate\Support\Facades\DB;

class MySqlIpAddressRepository implements IpAddressRepositoryInterface
{
    public function save(IpAddress $ipAddress): bool
    {
        return DB::table('tbl_ip_addresses')->insert([
            'domain_id' => $ipAddress->domainId,
            'domain_name' => $ipAddress->domainName,
            'ip_address' => $ipAddress->ipAddress,
        ]);
    }

    public function exists(int $domainId, string $ipAddress): bool
    {
        return DB::table('tbl_ip_addresses')
            ->where('domain_id', $domainId)
            ->where('ip_address', $ipAddress)
            ->exists();
    }
}

4.サービスクラスの作成

サービスクラスを作成して、CSVファイルの読み込みとデータベース保存のロジックを実装します。

<?php
namespace App\Application\Services;

use App\Domain\Models\IpAddress;
use App\Domain\Repositories\IpAddressRepositoryInterface;

class IpAddressImportService
{
    protected $ipAddressRepository;

    public function __construct(IpAddressRepositoryInterface $ipAddressRepository)
    {
        $this->ipAddressRepository = $ipAddressRepository;
    }

    public function importFromCsv(string $filePath)
    {
        if (!file_exists($filePath) || !is_readable($filePath)) {
            throw new \Exception("CSV file is not readable.");
        }

        $header = null;
        if (($handle = fopen($filePath, 'r')) !== false) {
            while (($row = fgetcsv($handle, 1000, ',')) !== false) {
                if (!$header) {
                    $header = $row;
                } else {
                    $data = array_combine($header, $row);
                    if ($data['RECORDTYPE'] == 20) {
                        $ipAddress = new IpAddress($data['DOMAINID'], $data['DOMAINNAMEML'], $data['RECVALUE']);
                        if (!$this->ipAddressRepository->exists($ipAddress->domainId, $ipAddress->ipAddress)) {
                            $this->ipAddressRepository->save($ipAddress);
                        }
                    }
                }
            }
            fclose($handle);
        }
    }
}

5.サービスプロバイダーの登録
リポジトリインターフェースと実装をサービスコンテナに登録します。

<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\Repositories\IpAddressRepositoryInterface;
use App\Infrastructure\Persistence\MySqlIpAddressRepository;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(IpAddressRepositoryInterface::class, MySqlIpAddressRepository::class);
    }

    public function boot()
    {
        //
    }
}

6.マイグレーションの作成
データベースのテーブルを作成するマイグレーションを作成します。

php artisan make:migration create_tbl_ip_addresses_table

生成されたマイグレーションファイルを以下のように編集します。

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTblIpAddressesTable extends Migration
{
    public function up()
    {
        Schema::create('tbl_ip_addresses', function (Blueprint $table) {
            $table->id();
            $table->integer('domain_id')->unsigned();
            $table->string('domain_name', 255);
            $table->string('ip_address', 15);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tbl_ip_addresses');
    }
}

7.Artisanコマンドの実行
最後に、Artisanコマンドを実行して、CSVファイルのデータをデータベースにインポートします。

php artisan import:ipaddresses

この構成により、CSVファイルからIPアドレスを読み込み、条件に従ってMySQLデータベースに保存するDDD設計のコマンドラインアプリケーションが完成します。

結果に対するコメント(複数回の命令文で結果を求めた場合)

1回目の命令でChatGPTにディレクトリ構造とその役割を質問し、2回目の命令でディレクトリ構造における呼び出し可否や各レイヤーからどのクラスを呼び出せるかを質問しました。

この段階的なアプローチにより、App\Domain\RepositoriesとApp\Infrastructure\Persistenceの役割を認識した状態でコーディングが行われました。

「1回の命令文で結果を求める方法」では、DDDについての情報が不足していましたが、「複数回の命令文で結果を求める方法」では、あらかじめDDDについて学習させたうえで生成を行うことで、求めていた結果を得ることができました。

まとめ

つまり、段階的な情報提供がChatGPTを効果的に活用するためには重要です。
このアプローチにより、より精度の高い結果を得ることができ、生成AIの活用にも繋がります。
また、生成AIを活用することで通常作成に4時間程度かかるツールも30分程度の時間でできるようになったりします。
是非、皆さんもご利用してみてください。

次回も機会があれば、生成AIの活用方法について紹介していきますので、お楽しみにしてください。

ブログの著者欄

長谷川 零

GMOインターネットグループ株式会社

2015年1月にGMOインターネットグループ株式会社に入社。インフラチームに配属され、マネージャーを経て、現在、インフラ・運用本部プロジェクト統括チームに所属。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック