Lumen × dockerの環境構築とLumenチュートリアル


この度Lumenを使用することになりまして、Lumenをキャッチアップしてみます。

Lumenの記事は古いものが多かったので、モダンなバージョンを使って
キャッチアップの様子を記事化してみました🙌

## 対象読者
- Laravel使ったことあるけど、Lumenってなんぞやと知りたい同じ境遇の方
- Lumen&dockerの環境で開発してみたい方
- Laravel使って一年の私にありがたいアドバイスをくださる神様

## 目的
Lumenを触ってみる目的で以下のことを実践します
- dockerコンテナ内にlumenのインストール
- lumenでAPIの作成
  - migrationやらcontroller作ったりする

以下のようなgetリクエストを送信して、記事の情報をjson形式でレスポンスするAPIを作成してみます
```
// 記事一覧
http://localhost:8080/articles

// 記事詳細
http://localhost:8080/articles/{id}
```

## まずLumenとは
~Laravelと同じようにアプデされて、同じように開発できて、得意領域が異なる〜

- Laravelのマイクロフレームワーク版
   - Laravelの開発チームが手掛ける
   - Laravelと近い感覚で開発が可能（コマンドや記述等）
   - Lumenは簡易アプリケーションやAPIサーバーに適する
- 速度やメモリ消費の観点でLaravelよりLumenが優れる
   - 必要最低限のファイルなのでインストールも激早
- 必要に応じてフルスタックフレームワークであるLaravelへのアップデートも可能（らしい）

詳しくは公式サイト: https://lumen.laravel.com/docs/10.x/installation

## 使用技術
- intel Mac（OS: Sonoma 14.1）
- lumen-framework 10 （お試しだし最新バージョン使っていきます）
- PHP 8.1
- MySQL 8

## ファイル構成

一応以下のようなファイル構成になっています。
src下にLumenが入る想定なので、最初は空でOK
```bash
lumen-react-typescript-docker/ % tree -L 2  
.
├── Makefile （なくてOK）
├── docker
│   ├── mysql
│   └── php
├── docker-compose.yml
└── src
    ├── lumenのファイルたち
```

## docker用意

Laravel x MySQLが動くコンテナがあれば再現可能です。
調べたらいっぱい出てくると思うので、~~割愛しま~~お好きな環境で試してくださいw

一応`docker-compose.yml`のappコンテナだけ掲載しておきます。
簡易的にappコンテナ内でビルドインウェブサーバーを起動しています。（もちろんwebサーバーを用意してもOK）
```yml
version: "3.9"
services:
  app:
    build: ./docker/php
    volumes:
      - ./docker/php/php.ini:/usr/local/php/php.ini
      - ./src:/var/www/html
    ports:
      - "8000:8000"
    command: php -S 0.0.0.0:8000 -t public
  db:
```

`docker compose up -d`後、以下のことが確認できれば大丈夫です
```
// アプリケーションサーバー
$ docker compose exec app composer -v
$ docker compose exec app php -v

// DB （開発中はMySQLにログインしっぱなしが楽）
$ docker compose exec db bash
// mysql login
# bash-4.4# mysql -uroot -p
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| laravel_db         |
| mysql              |
~~~
```

## Lumenインストール
早速コンテナ内でインストールしちゃいます
```
// 最新版インストール
docker compose exec app composer create-project --prefer-dist laravel/lumen .

// バージョン指定
docker-compose exec app composer create-project --prefer-dist laravel/lumen=10.x .
```

インストール超早。。
以下のようになっていたら成功です
[http://localhost:8000/](http://localhost:8000/)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1063001/0ac0058f-1d28-604c-c277-2dc0e4618c06.png" width="500">

デフォルトではバージョンを返すんですね〜、Bladeなどももちろん使用可能です。
src/routes/web.php
```php
$router->get('/', function () use ($router) {
    return $router->app->version();
});
```

## テスト
デフォルトのテストを動かしてみましょう。`artisan test`コマンドないんだ。。
```
% docker compose exec app vendor/bin/phpunit

PHPUnit 10.4.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.25
Configuration: /var/www/html/phpunit.xml

.                                                                   1 / 1 (100%)

Time: 00:00.261, Memory: 12.00 MB

OK (1 test, 1 assertion)
```
動きましたね👏


## artisanコマンドの追加
デフォルトでは使用可能なartisanコマンドの数が少ないです。（`artisan list`で確認できます）
artisanコマンド使いたい人は追加しましょう。（[参考サイト](https://stackoverflow.com/questions/34339517/lumen-laravel-eloquent-php-artisan-makemodel-not-defined)）


```
% docker-compose exec app composer require flipbox/lumen-generator
~~色々追加される~~
```
次に`bootstrap/app.php`ファイル内に以下の記述を追加します
```php
// $app->register(App\Providers\EventServiceProvider::class);
// 以下を追記する
$app->register(Flipbox\LumenGenerator\LumenGeneratorServiceProvider::class);
```
再度コマンドで確認してみると使用可能なartisanコマンドが増えていることが確認できます。
```
% docker-compose exec app php artisan list
~~~
 make
  make:cast            Create a new custom Eloquent cast class
  make:channel         Create a new channel class
  make:command         Create a new Artisan command
  make:controller      Create a new controller class
  make:event           Create a new event class
  make:exception       Create a new custom exception class
  make:factory         Create a new model factory
  make:job             Create a new job class
  make:listener        Create a new event listener class
  make:mail            Create a new email class
  make:middleware      Create a new middleware class
  make:migration       Create a new migration file
  make:model           Create a new Eloquent model class
```

## モデルとマイグレーションファイルを作成する

今回はArticleという名前のモデルやテーブルを作ってみます。

```
docker-compose exec app php artisan make:model Article -m
```

Laravelと同じ感じで開発できますね
マイグレーションファイルは以下のようにしてみました。
```php
use Illuminate\Support\Facades\Date; // 追加する
~~~
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->string('title', 50);
            $table->text('body');
            $table->integer('status');
            $table->dateTime('created_at')
                ->nullable()
                ->default(Date::now());
            $table->dateTime('updated_at')
                ->nullable()
                ->default(Date::now());
        });
    }
```
let's migrate
```
docker-compose exec app php artisan migrate
```
DBを確認してみるとテーブルが定義できていることが確認できました🙌
```
mysql> show tables;
+----------------------+
| Tables_in_laravel_db |
+----------------------+
| articles             |
| migrations           |
+----------------------+
2 rows in set (0.01 sec)

mysql> SHOW FULL COLUMNS FROM articles;
+------------+-----------------+--------------------+------+-----+---------------------+----------------+---------------------------------+---------+
| Field      | Type            | Collation          | Null | Key | Default             | Extra          | Privileges                      | Comment |
+------------+-----------------+--------------------+------+-----+---------------------+----------------+---------------------------------+---------+
| id         | bigint unsigned | NULL               | NO   | PRI | NULL                | auto_increment | select,insert,update,references |         |
| title      | varchar(50)     | utf8mb4_unicode_ci | NO   |     | NULL                |                | select,insert,update,references |         |
| body       | text            | utf8mb4_unicode_ci | NO   |     | NULL                |                | select,insert,update,references |         |
| status     | int             | NULL               | NO   |     | NULL                |                | select,insert,update,references |         |
| created_at | datetime        | NULL               | YES  |     | 2023-11-16 07:19:11 |                | select,insert,update,references |         |
| updated_at | datetime        | NULL               | YES  |     | 2023-11-16 07:19:11 |                | select,insert,update,references |         |
+------------+-----------------+--------------------+------+-----+---------------------+----------------+---------------------------------+---------+
```

### モデルの編集

生成されたArticleモデルを以下のように修正しました

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory; // 追加
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory; // 追加

    protected $fillable = [
        'title', // 追加↓
        'body',
        'status',
    ];
}
```
HasFactoryは不要だと思うのですが、seederの生成でエラーが出たので追記しています。
Modelとfactoryをartisanコマンドで作成したら違うのかな？


## Configファイルの導入
LumenにはLaravelのようにConfigファイルがデフォルトでありません。
テストデータを日本語で生成したいので、Configファイルを作成していきます。

`src`フォルダ下に`app.php`を作成し、一旦以下のような内容にしておきます。
src/config/app.php
```php
return [
    'faker_locale' => 'ja_JP',
];
```

次に以下の記述を`bootstrap/app.php`に追記します
src/bootstrap/app.php
```php
/** 設定ファイルの読み込み */
$app->configure('app');
```

一応確認
```
% docker compose exec app php artisan tinker 
Psy Shell v0.11.22 (PHP 8.1.25 — cli) by Justin Hileman
> config('app.faker_locale')
= "ja_JP"
```
反映されていますね👏

※ Laravelでは`timezone`の設定は`config/app.php`に記述しますが、Lumenでは`.env`を利用するみたいです

.env
```
APP_TIMEZONE=Asia/Tokyo // 修正
```

## テストデータの作成

### Factoryの作成

テストデータを複数生成したいので、Factoryを作成します。
```
docker compose exec app php artisan make:Factory ArticleFactory
```

以下のようにFactoryを定義しました
```php
use App\Models\Article; // 追記
use Carbon\Carbon; // 追記
use Illuminate\Database\Eloquent\Factories\Factory;

class ArticleFactory extends Factory
{
    protected $model = Article::class; // 追記

    public function definition(): array
    {
        return [ // 追記↓
            'title' => $this->faker->word,
            'body' => $this->faker->realText(10),
            'status' => $this->faker->numberBetween(0, 1),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ];
    }
}
```

### Seederの作成

Factoryの定義を元にテストデータを作成する記述をSeederにて行います。
以下のコマンドでSeederを生成します。
```
docker compose exec app php artisan make:seeder ArticleTableSeeder
```
Seeder自体は以下のようにしました
```php
use App\Models\Article; // 追記
use Illuminate\Database\Seeder;

class ArticleTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Article::factory(10)->create(); // 追記
    }
}
```
### DatabaseSeederに定義

Seederを使うためにdatabase/seeds/DatabaseSeeder.phpに、作成したシーダークラスを定義します。
```php
/**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call('UsersTableSeeder');
        $this->call(ArticleTableSeeder::class); // 追加
    }
```

### ダミーデータの生成
以下のコマンドで生成します。
```
 % docker compose exec app php artisan db:seed

   INFO  Seeding database.  

  Database\Seeders\ArticleTableSeeder .............................................................. RUNNING  
  Database\Seeders\ArticleTableSeeder ....................................................... 199.37 ms DONE 
```

DBにデータが入っていることも確認しておきます。
```
mysql> select * from articles;
+----+--------------+--------------------------------+--------+---------------------+---------------------+
| id | title        | body                           | status | created_at          | updated_at          |
+----+--------------+--------------------------------+--------+---------------------+---------------------+
|  1 | perspiciatis | 光りんこうに思いま。           |      0 | 2023-11-22 00:19:52 | 2023-11-22 00:19:52 |
|  2 | inventore    | ぼくもうしてっぽが。           |      1 | 2023-11-22 00:19:52 | 2023-11-22 00:19:52 |
|  3 | sapiente     | あかり注文ちゅうは。           |      1 | 2023-11-22 00:19:52 | 2023-11-22 00:19:52 |
```

日本語に対応しているFakerは日本語に、now()として登録した時間が日本時間なのも確認できました◎

## コントローラーの作成
今回使用しないものもありますが、リソースコントローラーを以下のコマンドで作成しましょい
```
% docker compose exec app php artisan make:controller ArticleController --resource
Controller created successfully.
```

## ルーティングの追加
src/routes/web.phpに以下の記述を追加します。
```php
// index
$router->get('articles', 'ArticleController@index');
// show
$router->get('articles/{id}', 'ArticleController@show');
```
Laravelだと`Route::resource('articles', 'ArticleController');`と書けますが、Routeファサードがデフォルトで使えないっぽいので、一つずつ宣言します。

### ルーティングの確認
`artisan route:list`は使えないようですね。
以下のように`tinker`を用いて、`app('router')->getRoutes();`のコマンドを叩いてルーティングの確認を行います。
```
% docker-compose exec app php artisan tinker
Psy Shell v0.11.22 (PHP 8.1.25 — cli) by Justin Hileman
> app('router')->getRoutes();
= [
    "GET/" => [
      "method" => "GET",
      "uri" => "/",
      "action" => [
        Closure() {#20 …2},
      ],
    ],
    "GET/articles" => [
      "method" => "GET",
      "uri" => "/articles",
      "action" => [
        "uses" => "App\Http\Controllers\ArticleController@index",
      ],
    ],
    "GET/articles/{id}" => [
      "method" => "GET",
      "uri" => "/articles/{id}",
      "action" => [
        "uses" => "App\Http\Controllers\ArticleController@show",
      ],
    ],
  ]

> 
```

## Eloquentを使用可能にする
`bootstrap/app.php`にて，`$app->withEloquent();`のコメントアウトを外します。

## コントローラを編集する
以下のように記述しました。（`options: JSON_PRETTY_PRINT`とするとjsonが整形されて画面で見やすいです）

```php
use App\Models\Article; // 追加

class ArticleController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $articles = Article::all();
        return response()->json(data: $articles, options: JSON_PRETTY_PRINT);;
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $article = Article::find($id);
        return response()->json($article);
    }
}
```

## 画面でレスポンスを確認する
http://localhost:8000/articles
![スクリーンショット 2023-11-23 17.01.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1063001/11d46284-c318-7a6d-32f4-17fdfcca3937.png)

http://localhost:8000/articles/3
![スクリーンショット 2023-11-23 17.23.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1063001/573e058c-4fd4-e200-a28c-9b60b9b4ea81.png)

日本語がUnicodeエスケープされていますが、クライアントで受け取った際には解除されるっぽいので問題はない。。はず。。確認するか。。

## viewの作成

indexの方だけ確認してみます。
`src/resources/views/articles/index.blade.php`を作成します
```php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Article List</title>
</head>
<body>

<div>
    @foreach($articles as $article)
        <div>
            <h2>{{ $article['title'] }}</h2>
            <p>{!! $article['body'] !!}</p>
        </div>
    @endforeach
</div>

</body>
</html>
```
続いて`src/app/Http/Controllers/ArticleController.php`のindex()内を以下のようにします
```php
$articles = Article::all();
// return response()->json(data: $articles, options: JSON_PRETTY_PRINT);
return view('articles.index', compact('articles')); // 追加
```

クライアント側で受け取った際に、無事に解除されていますね！
http://localhost:8000/articles
![スクリーンショット 2023-11-23 17.44.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1063001/ffb3234d-32a9-cac6-26a4-114d5583f466.png)

## まとめ

- Laravelでよく使ってたコマンドまで使えないから、たくさん詰まりましたw
- 速度面で優れるのは間違いないから、Laravelとうまく使い分けることが大事そう
- LaravelもLumenももっと理解しようってモチベ上がった




## 参考
たくさん参考にさせて頂きました。m(_ _)m

https://qiita.com/cappyzawa/items/f20ccccea6881991dbcb

https://qiita.com/jefftech/items/f266adb6cc184197f9b0