Laravel で承認ワークフローをつくってみる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、私はある程度長く開発に携わっているので、過去にいろいろなシステムを担当させていただきました。

その中には、特に「社内システム」と呼ばれるクローズドな環境のシステムが多かったのですが、より業務の効率化につながるシステム構築は私に合っているかなという気がしています。

しかし、多数システム開発したことがあるといってもやったことのない開発もあったりします。

そして、そう考えると真っ先に思い浮かぶものがありました。

それが・・・・・・

承認ワークフロー

です。

つまり、「みんながオッケーしたら、承認」とか「2/3がオッケーなら可決」とかそういうシステムですね。

そこで❗

今回はLaravelを使ってシンプルな「承認、拒否、未回答が何人いるのか」がわかるシステムを作ってみることにしました。

ぜひ何かの参考になりましたら嬉しいです😄✨

嬉しいニュース✨

なんと、「レバテックフリーランス」さんが、このブログを「市場価値を高めたいエンジニアに読んでほしい技術ブログ特集」として紹介してくださいました!

ブログやってて良かった😄
ぜひ皆さんも、どんな紹介になってるか見に行ってみてくださいね。
↓↓↓

📝 市場価値を高めたいエンジニアに読んでほしい技術ブログ特集

※担当者様、丁寧にご対応いただきましてありがとうございました❗

 

開発環境: Laravel 10.x

前提として

ログイン機能がLaravelにインストールされていることが前提です。
もしまだの方は、Laravel Breezeなどを使ってサクッとインストールしておいてください。

また、usersテーブルに以下のようにテストユーザーが10人ほど入っている必要があります。

では、今回も楽しんでやっていきましょう❗

Enum をつくる

まずは全体に影響するので、Enum(列挙型)で「承認ステータス」をつくります。

必要なステータスは以下3つです。

  • 承認済み
  • 却下
  • 未承認

app/Enums/ApprovalStatus.php

<?php

namespace App\Enums;

enum ApprovalStatus: string
{
    case Pending = 'pending'; // 未承認
    case Approved = 'approved'; // 承認済み
    case Rejected = 'rejected'; // 却下

    public function getLabel(): string
    {
        return match ($this) {
            self::Pending => '未承認',
            self::Approved => '承認',
            self::Rejected => '却下',
        };
    }

    public function getTheme(): string
    {
        return match ($this) {
            self::Pending => 'bg-gray-500 text-yellow-800',
            self::Approved => 'bg-green-500 text-green-800',
            self::Rejected => 'bg-red-500 text-red-800',
        };
    }
}

この中では、先ほどの3つのステータスを登録し、さらにそれぞれの「日本語名」と「テーマカラー(TailwindCSS)」を取得できるようにしています。

DB まわりをつくる

次に、モデルやマイグレーションをつくっていきます。
以下のコマンドを実行してください。

php artisan make:model Approval -ms
php artisan make:model ApprovalDetail -m

すると、ファイルが全部で5つ作成されるのでそれぞれ中身を変更してください。

マイグレーション

まずはデータベースの設計図になる「マイグレーション」です。

database/migrations/****_**_**_******_create_approvals_table.php

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('approvals', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->comment('説明');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('approvals');
    }
};

database/migrations/****_**_**_******_create_approval_details_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Enums\ApprovalStatus;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('approval_details', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('approval_id')->comment('承認ID');
            $table->unsignedBigInteger('user_id')->comment('ユーザーID');
            $table->string('approval_status')
                ->default(ApprovalStatus::Pending)
                ->comment('承認ステータス'); // App\Enums\ApprovalStatus
            $table->timestamps();

            $table->foreign('approval_id')
                ->references('id')
                ->on('approvals')
                ->onDelete('cascade');
            $table->foreign('user_id')
                ->references('id')
                ->on('users');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('approval_details');
    }
};

なお、これら2つのテーブルは「1:多」でリレーションシップを組むことになります。

モデル

次にモデルです。
Approval.phpに以下5つのAccessorをつくってデータにアクセスしやすくしておきましょう❗

  • approved_count: 承認された数
  • rejected_count: 拒否された数
  • pending_count: 未回答の数
  • has_responded: (ログインしているユーザーが)すでに回答したかどうか
  • has_authority: (そもそも)回答する権利があるのかどうか

また、ApprovalDetail.phpとのリレーションシップも追加します。

app/Models/Approval.php

<?php

namespace App\Models;

use App\Enums\ApprovalStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Approval extends Model
{
    use HasFactory;

    protected $appends = [
        'approved_count',
        'rejected_count',
        'pending_count',
        'has_responded',
        'has_authority',
    ];

    // Relationship
    public function details()
    {
        return $this->hasMany(ApprovalDetail::class, 'approval_id', 'id');
    }

    // Accessor
    public function getApprovedCountAttribute() // 承認された件数
    {
        return $this->details->filter(function($detail){

            return $detail->approval_status === ApprovalStatus::Approved->value;

        })->count();
    }

    public function getRejectedCountAttribute() // 却下された件数
    {
        return $this->details->filter(function($detail){

            return $detail->approval_status === ApprovalStatus::Rejected->value;

        })->count();
    }

    public function getPendingCountAttribute() // 未承認の件数
    {
        return $this->details->filter(function($detail){

            return $detail->approval_status === ApprovalStatus::Pending->value;

        })->count();
    }

    public function getHasRespondedAttribute() // ユーザーが既に返答しているかどうか
    {
        $user = auth()->user();
        $count = $this->details
            ->where('user_id', $user->id)
            ->where('approval_status', '<>', ApprovalStatus::Pending->value)
            ->count();

        return $count > 0;
    }

    public function getHasAuthorityAttribute()
    {
        $user = auth()->user();
        $count = $this->details
            ->where('user_id', $user->id)
            ->count();

        return $count > 0;
    }
}

Seeder(テストデータ)

続いてデータベースに保存するテストデータをつくります。

database/seeders/ApprovalSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Approval;
use App\Models\ApprovalDetail;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class ApprovalSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $users = User::get();

        for ($i = 0; $i < 10; $i++) {

            $approval = new Approval([
                'title' => '承認依頼 ' . $i,
                'description' => '承認依頼 ' . $i . ' の説明',
            ]);
            $approval->save();

            $adding_users = $users
                ->shuffle()
                ->take(5);

            foreach ($adding_users as $index => $all_user) {

                $approval_detail = new ApprovalDetail([
                    'approval_id' => $approval->id,
                    'user_id' => $all_user->id,
                ]);
                $approval_detail->save();

            }


        }
    }
}

そして、DatabaseSeederに登録します。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // 省略

        $this->call([
            ApprovalSeeder::class, // 👈 これ
        ]);
    }
}

では、この状態でDBを初期化してみましょう。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

すると、実際のテーブルはこうなりました。

コントローラーをつくる

続いて、コントローラーです。
以下のコマンドを実行してください。

php artisan make:controller ApprovalController

すると、ファイルが作成されるので中身を次のようにします。

app/Http/Controllers/ApprovalController.php

<?php

namespace App\Http\Controllers;

use App\Enums\ApprovalStatus;
use App\Models\Approval;
use Illuminate\Http\Request;

class ApprovalController extends Controller
{
    public function index(Request $request)
    {
        $user = $request->user();
        $approvals = Approval::query()
            ->whereHas('details', function($query) use($user){ // 自分に承認依頼があるものだけを取得

                $query->where('user_id', $user->id);

            })
            ->paginate(15);

        return view('approval.index', [
            'approvals' => $approvals,
        ]);
    }

    public function edit(Approval $approval)
    {
        if(! $approval->has_authority) { // 承認権限がない場合

            abort(403);

        }

        $approval->load('details');
        $approval_statuses = [
            ApprovalStatus::Approved,
            ApprovalStatus::Rejected,
        ];

        return view('approval.edit', [
            'approval' => $approval,
            'approval_statuses' => $approval_statuses,
        ]);
    }

    public function update(Approval $approval, Request $request)
    {
        // バリデーションは省略しています

        if(! $approval->has_authority) { // 承認権限がない場合

            abort(403);

        }

        $approval_detail = $approval->details
            ->where('user_id', $request->user()->id)
            ->first();
        $approval_detail->approval_status = $request->status;
        $approval_detail->save();

        return to_route('approval.index');
    }
}

この中でやっていることは以下のとおりです。

  • index: (自分に関連する)承認議題の一覧を表示
  • edit: 「承認」or「拒否」を選択するフォーム
  • update: 選択された「承認」or「拒否」を実行する

ビュー(一覧ページ)をつくる

では、先ほどのコントローラーの中で使ったビューをつくります。
以下のコマンドを実行してください。

php artisan make:view approval.index

resources/views/approval/index.blade.php

<html>
<head>
    <title>承認議題の一覧</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    <div class="p-5">
        <h1 class="text-3xl mb-5">承認議題の一覧</h1>
        <ul>
            @foreach ($approvals as $approval)
                <li>
                    {{ $approval->title }}

                        承認: {{ $approval->approved_count }}
                        /
                        拒否: {{ $approval->rejected_count }}
                        /
                        未回答: {{ $approval->pending_count }}

                    @if($approval->has_responded === false)
                    <a class="text-blue-500 underline" href="{{ route('approval.edit', $approval->id) }}">
                        回答する
                    </a>
                    @endif
                </li>
            @endforeach
        </ul>
    </div>
</body>
</html>

ビュー(承認ページ)をつくる

同じくコントローラー内にセットしたもう1つのビューです。

php artisan make:view approval.edit

すると、ファイルが作成されるので中身を以下のようにします。

resources/views/approval/edit.blade.php

<html>
<head>
    <title>承認議題の一覧</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="p-5">
    <h1 class="text-3xl mb-5">{{ $approval->title }}</h1>
    <div class="mb-5 bg-gray-200 p-3 rounded">
        {{ $approval->description }}
    </div>
    <form method="POST" action="{{ route('approval.update', $approval) }}">
        @csrf
        @method('PUT')
        @foreach($approval_statuses as $approval_status)
            <button name="status" value="{{ $approval_status->value }}" class="{{ $approval_status->getTheme() }} text-white font-bold py-2 px-4 rounded">
                {{ $approval_status->getLabel() }}
            </button>
        @endforeach
    </form>
</div>
</body>
</html>

なお、このビューでは実際に「承認」or「拒否」を決めることができます。

ルートをつくる

では、最後にルートです。

routes/web.php

use App\Http\Controllers\ApprovalController;

// 省略

Route::prefix('approval')->middleware('auth')->controller(ApprovalController::class)->group(function(){

    Route::get('/', 'index')->name('approval.index');
    Route::get('edit/{approval}', 'edit')->name('approval.edit');
    Route::put('/{approval}', 'update')->name('approval.update');

});

なお、今回はログインが前提の機能ですので、middleware('auth')でログインチェックをしていることに注目してください。

これで作業は完了です。
お疲れ様でした😄✨

テストしてみる

では、実際にテストしてみましょう❗

まずブラウザで(太郎さんでログインし)「https://******/approval」へアクセスします。

すると、(自分に回答権がある)承認議題の一覧が表示されるので、一番上の「回答する」をクリックしてみます。

ページ移動するので「承認」をクリックしてみます。

すると・・・・・・

はい❗
承認が件追加になり、未回答がに減りました。

まずは成功です。😄

では、一旦ログアウトして、別のユーザー(次郎さん)でログインしてみます。

すると、以下のように「承認:1」で「未回答:4」のままですが、次郎さんはまだ回答していないのでリンクが表示されています。

では、回答ページへ移動して今度は「拒否」してみましょう。

どうなるでしょうか・・・・・・

はい❗
今度は「拒否」が1になり、「未回答」が3に変更になりました。

すべて成功です😄✨

企業様へのご提案

今回は「シンプルに承認すべきユーザーを誰にするか?」を機能として組み込みましたが、今回のコードを拡張すると、「承認グループ」をつくりグループごとに承認機能を実装できるようになります。

  1. 第1グループ: 太郎さん、次郎さん
  2. 第2グループ: 三郎さん、四郎さん、五郎さん
  3. 第3グループ: 六郎さん

つまり、グループ内の承認がすべて完了した時点で次のグループへお伺いをたてることができる、という流れになります。

もしこういった社内承認フローをシステム化したいとお考えでしたら、いつでもお気軽にご相談ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は「承認フロー」をつくってみました。

特に日本はコンセンサス社会なので、「俺に話がきてないぞ😤」なんてことになるとめんどうだったりするので、こういったシステムが体系化されているとよりスムーズに仕事を進めることにつながるかもしれません。

ぜひ皆さんも、仕事をスムーズにするシステムを考えてみてくださいね。

ではでは〜❗

「留学してたカナダの家賃価格が
同時の2倍になっているらしい…
また行くのが遠のくのか😫(しかも円安)」

このエントリーをはてなブックマークに追加       follow us in feedly