業務で経験したLaravel

# はじめに
**皆様 こんにちは・初めまして 2023年7月入社 Webエンジニアの小川と申します。**

執筆時点でLaravel + Next.js で運営されているサイトの案件に携わっております。

※自身はバックエンド側が主担当なのでどちらかというとLaravelメイン

Laravelについては、入社以前から学習していましたが、（当然といえば当然ですが）業務で触ったことにより様々な新しい学びや発展がありましたので、それをテーマにこの記事を執筆することにしました。

PHP, LaravelはさくっとWebアプリが動かせて便利ですが、業務での経験によりより実践的なテクニックが身についたかなと思います。（自身では実践的だと思っても既にご存じの方もいるかもしれませんが、この記事は自身のために書くことにしたのでそこはご容赦ください！:raised_hands:）

そして何より
**PHPやLaravelの魅力を皆様にしっていただきたいです!!**


## 前提
 - 紙面の関係上、環境構築等は省略します。
 - 本記事の題材は下記の仕様のサイトとします。
   - 読んだ本の投稿(POST処理)と閲覧(GET処理)ができる。
   - リソースとして本のデータとそれに紐づく著者のデータがある。
 - ここから本編です。Webアプリの各要素ごとに章だてていきます。

 [ルーティング](#ルーティング)
 [内部処理](#内部処理)
 [DB操作](#データベース操作)
 [デプロイ](#デプロイ)


### ルーティング
Laravelのルーティングは柔軟で非常に便利。
その分だけ、パスの記載が煩雑になったりしないように体系的にするのが大事!

 - ↓に適当に書いてもルーティングは組めるが、
 
**before**
```php:web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;
use App\Http\Controllers\AuthorController;

// 本一覧
Route::get('books', [BookController::class, 'index']);
// 本の登録
Route::post('Book/create', [BookController::class, 'store']);
// 本の詳細
Route::get('book/{id}', [BookController::class, 'show']);
// 本の更新
Route::put('book_update/{id}', [BookController::class, 'update']);
// 本の削除
Route::delete('book/{id}/destory', [BookController::class, 'destroy']);


// 著者一覧
Route::get('authors', [AuthorController::class, 'index']);
// 著者登録
Route::post('author/create', [AuthorController::class, 'store']);

```

 - 下記のような感じでgropu化し、すっきり！
 - gropu化し、プレフィックスも統一
 - 基本的なcrud処理なら`リソースコントローラ`の７つのメソッドから取捨選択するのもgood
   - index：一覧
   - create：登録ページ
   - store：登録
   - show：詳細表示
   - edit：編集ページ
   - update：更新
   - destroy：削除

**after**
```php:web.php
// 本の一覧、登録、詳細、更新、削除
Route::group(['prefix' => 'book'], function (): void {
    Route::get('/', [BookController::class, 'index']);
    Route::post('/create', [BookController::class, 'store']);
    Route::get('show/{id}', [BookController::class, 'show']);
    Route::put('update/{id}', [BookController::class, 'update']);
    Route::delete('delete/{id}', [BookController::class, 'destroy']);
});

// 著者一覧、登録
Route::resource('author', AuthorController::class)
    ->only(['index', 'store']);

```

### 内部処理
ユースケース等の内部ロジックについて

#### phpの型付け
phpは静的型付けの言語のため、私も当初はヌルポや型違いによるエラーをよくして起こしていました。
下記のコードもいくつかバグがありますが、実装段階ではすぐに気づくことができません。

なので、
 - メソッドの戻り値の型・引数の型を明示する
 - nullの可能性のあるものはそれを示唆する
 - PHPDocを記載する
 ことでIDEから警告を受け取りバグの早期発見！


**before**
```php:BookUsecae.php
<?php

namespace App\UseCase;

use App\Models\Book;

class BookUseCase
{
    public function __construct() {}

    public function getPrice(array $ids)
    {
        $price = 0;

        if (count($ids) === 0) {
            // 空の配列の場合はnullを渡す;
            // ！！配列としてforeachで回すのでNG！！
            $price = $this->calculateSum(null);
        } else {
            $price = $this->calculateSum($ids);
        }
        // ！！return文書き忘れ！！
    }

    private function calculateSum($ids)
    {
        if (is_null($ids)) return 0;

        $sum = 0;

        foreach ($ids as $id) {

            $price = $this->getBookPrice($id);

            // ！！引数の順序誤り！！
            $taxedPrice = $this->getTaxedPrice(function ($price) {
                return $price * 1.1;
            }, $price);

            $sum += $taxedPrice;
        }
        return $sum;
    }

    private function getBookPrice($id)
    {
        $book = Book::where('id', $id)->get();

        if ($book) {
            return $book->first()->price;
        } else {
            // 対象の本のレコード泣ければブランク返す
            // ！！このメソッドの戻り値は数値として扱わるので空文字はNG！！
            return "";
        }
    }

    private function getTaxedPrice($price, $getTaxedPriceExpression)
    {
        return $getTaxedPriceExpression($price);
    }
}

```

**after**
 - 仮引数に型を指定
 - メソッドの戻り値を指定
 - PHPDocを記載


![codewarn1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/55863d19-4ab1-9758-704f-d43db41f4f06.png)
![codewarn2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/7d0f91a2-2ca6-10ab-7cdd-445a85b6ec10.png)
![codewarn3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/0b82d173-d739-bb8a-43a2-92fa07b031c1.png)
![codewarn4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/ffb0eec3-ff97-8627-e8f6-f29f68011d94.png)

### データベース操作
LaravelのDBを扱うライブラリは多く分けて、`DBクラス`と`Eloquent`の２種類あるが、


自身は生のSQLに近いDBクラスが分かりやすいと最初思っていたが、Eloquentも慣れるとテーブルのリレーションの取得が便利:clap:
ここでは特に双方のjoinされたテーブルに対するクエリの表現について書きます。

下記のようなリレーションのテーブルに対して、結合してデータを取得する場合、

![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/e004ee4a-57b3-c205-7acb-26d7fe08619a.png)
![sql.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/15ae47ab-4600-76b7-114b-dfc5ad6a83be.png)



 - DBクラスの場合
 下記のようなコードになる。だいたいもとのSQLからイメージはわく形。

```php:BookRepository.php
<?php

namespace App\Repository;

use Illuminate\Support\Facades\DB;

class BookRepository
{
    /**
     * 本と著者名を紐づけて取得
     * @return \Illuminate\Support\Collection
     */
    public function getBooksAndAuthor()
    {
        $data = DB::table('books')
            ->join('authors', 'books.author_id', '=', 'authors.id')
            ->select(
                'books.id',
                'books.title',
                DB::raw("concat(authors.family_name, ' ', authors.first_name)"),
                'a.birth_place'
            )
            ->get();

        return $data;
    }
}
```

  - Eloquentの場合
    - まずはモデルの作成から
    - エンティティは自分で定義するわけではない
    - リレーションは
      - `hasMany`（1:N で紐づく）
      - `belongsTo`  （N:1 で紐づく）
      で定義

今回は
`本を基準に著者を紐づけて取得する`として、本のモデルを下記の通り作成

```php:Book.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    use HasFactory;

    public function author()
    {
        // 暗黙的に
        // booksテーブルのauthor_idカラム
        // と
        // authorsテーブルのidカラムで紐づけ
        return $this->belongsTo(Author::class);
    }
}

```

データの取得は下記の通り
```php:BookRepository.php
    /**
     * 本と著者名を紐づけて取得
     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
     */
    public function getBooksAndAuthor()
    {
        // withでリレーション先のテーブルのカラムを含めて取得
        // authorはBookモデルで「BelongsTo」を定義したメソッド名
        $data = Book::with(['author:id,family_name,first_name,birth_place'])
            ->get();

        return $data;
    }

```

**テーブル名や結合キーを書いていないのでコードがすっきりしていますね**
これは下記のルールを守っているからです。

  -  テーブル名はリソース名の先頭小文字の複数形とする
   →マイグレーションやテーブル設計の段階で気をつけるとgood👍

 - モデル名（クラス・ファイル名は）リソース名の先頭大文字の単数形とする
 - モデルクラスでリレーションを定義するメソッドはリソース名の先頭小文字の単数形
 - リレーションに用いるキーは「<リレーション先のリソース名の先頭小文字の単数形>_id」の形式

また上記のルールを守ることで体系的な命名をすることができます。


### デプロイ
#### nginxでの公開
ローカルでデバッグするだけなら
`php artisan serve`
でLaravel内蔵Webサーバを使えますが、製品版は**nginx**等のミドルウェアを使うのが一般的
業務でもnginxを調整して`クライアントからのリクエスト～Laravelで処理～レスポンス返却 `の流れを理解できたのでここを書きます。

 - nginxを導入する。（ここでは既に導入されているてい）
 - `/etc/nginx/conf.d`に`default.conf`を作成または編集
```nginx:www.conf
server {
    listen 80;
    server_name localhost;
# Laravelアプリの外部に直接公開される階層
    root /var/www/app/BookApp/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.blade.php index.php;

    charset utf-8;
# Laravelアプリのエントリーポイントであるindex.phpにリダイレクト
# 静的ファイルが存在すれば（直接CSSや画像ファイル参照する場合）はファイルを直接返却
    location / {
        try_files $uri /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

#    error_page 404 /index.blade.php;

    location ~ \.php$ {
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
# php-fpmの実体ファイルを設定
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;

    }
}
```

##### ポイント
 * Laravelアプリといえど、`index.php`がエントリーポイントとしてあるので、それを指定
 * 直接CSSや画像等の静的ファイルを指定した場合はそのファイルの内容が返される

  `/var/www/app/BookApp/public`をドキュメントルートしているのでそこから検索。
    ※ここの設定が間違っているとアプリ内で画像やCSSが効かずのっぺらぼうなサイトになったり、、
 * `php-fpm`はphpの速いやつ。`.sock`のソケットファイルでローカル内で通信される。ここら辺ももう少し深堀りたかった、、


**nginxで公開**
 - ポート8080,8000ではなくポート80としてサーバのメインのサイトとして公開される
 （※`php artisan serve `でも明示的にポート80を指定して起動可能）

![80.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513246/27d304e8-7f10-7550-3b74-cfd3162790cd.png)




# 最後に
ここまで読んでいただきありがとうございます！！
Laravelも素のphpも比較的に簡単にWebページを作れるのでお勧めです。
特に
 - Webアプリの学習をこれから始めたい
 - javaに疲れた（オブジェクト指向の考えは大体そのままに型付けを緩くコードを書ける）
 - JavaScriptよりももう少しサーバサイドの指向が強い技術を学びたい
etc
という方にお勧めです。

今回の記事は経験をしたことを文章にするだけ、と思っていましたがいざこの記事を書くときに色々と調べなおしたりしたこともあったので
```
他者に発信することは自分の知識を裏打ちするために有効
```
だなと思いました。
こういったアウトプットの機会を設けていただいてありがたく思います。

ちなみに私は12月から新現場です。
2024もお世話になりました！2025年も頑張りましょう🎍