Laravel10(PHP)でバックエンドAPIを開発する方法まとめ【OpenAPI仕様書・管理画面カスタマイズ】


 

こんにちは。Tomoyuki(@tomoyuki65)です。

私は元々Railsから始めたところもあり、最初にLaravel(PHP)に触れた時はめちゃめちゃ違和感(特に「->」などw)があって、やっぱりRailsの方がいいな。。なんて思うこともありました。

ただ最近Laravelをよく使うようになり、実際に使い込んでみるとめちゃめちゃ使いやすいフレームワークだなとちょっと感動し、今では好きになりました。

よくRailsとLaravelは比較されることがありますが、世界的なシェアを考慮すると、現状Laravelやっとけば間違いなし!という状況ではあるので、どちらを始めようか迷っているような方がいるなら、ぜひLaravelからスタートしてみて欲しいところです。

また、Laravelの使い所としては、主にバックエンドAPIやそれに付随する管理画面で使うことになると思います。

※フロントエンドはReactやNext.jsでリッチな画面を作るのが主流であり、もし一つのフレームワークでフロントエンドとバックエンドの両方を開発するならRailsを使うのが最適。

ということで、この記事ではLaravelでバックエンドAPIを開発する方法についてまとめます。

 



Laravel10(PHP)でバックエンドAPIを開発する方法まとめ【OpenAPI仕様書・管理画面カスタマイズ】

今回使用するLaravelのバージョンなどについては、2023年11月時点で最新のLaravel10(PHP8.2)を使います。

まずは以下のコマンドを実行し、プロジェクトの作成を行います。

※Laravelのプロジェクト作成のコマンドについて、ディレクトリ名はapi、不要はものはインストールさせないためにmysqlだけを指定しています。尚、コマンドを実行するには事前にDockerも使えるようにしておく必要があるので、まだの方は別途ご準備下さい。

$ mkdir laravel10
$ cd laravel10
$ curl -s "https://laravel.build/api?with=mysql"| bash

 

最後にOSなどに設定しているパスワードを聞かれるので、入力して完了させて下さい。

 

パスワード入力後、以下のようにプロジェクトが作成されれば完了です。

 

タイムゾーンと言語の日本語化設定

次にLaravelのタイムゾーンと言語の日本語化の設定を行います。

「config/app.php」の設定について、「timezone」と「locale」を以下のように修正します。

・・・

/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/

// 'timezone' => 'UTC',
'timezone' => 'Asia/Tokyo',

/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/

// 'locale' => 'en',
'locale' => 'ja',

・・・

 

DB(MySQL)のコレクション設定の変更とテスト用DBの設定追加

次にDB(MySQL)のコレクション設定の変更とテスト用DBの設定を追加します。

まずは「config/database.php」のmysqlにある「collation」の設定を「utf8mb4_bin」に変更後、コピーして名前を「mysql_test」にしてテスト用DBの設定を追加します。

※コレクションの設定を「utf8mb4_bin」に変更することにより、文字の区別を厳密にすることができます。(例えばデフォルトのままだと「A」と「a」を区別せず同じ文字として認識するので注意が必要です)

・・・

    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'url' => env('DATABASE_URL'),
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
        ],

        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4', 
            // 'collation' => 'utf8mb4_unicode_ci',
            'collation' => 'utf8mb4_bin',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        // テスト用のDB設定
        'mysql_test' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_bin',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

・・・

 

次に「phpunit.xml」にテスト用DBのコネクション設定「<env name=”DB_CONNECTION” value=”mysql_test”/>」を追加します。

・・・

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="mysql_test"/>
        <env name="DB_DATABASE" value="testing"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>

・・・

 

次に.envをコピーし、テスト用の「.env.testing」を作成します。

APP_ENV、APP_KEY、LOG_CHANNEL、DB_CONNECTION、DB_HOST、DB_PORT、DB_DATABASE、DB_USERNAMEの値をそれぞれ以下のように修正します。

APP_NAME=Laravel
APP_ENV=testing
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=null
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql_test
DB_HOST=mysql_test
DB_PORT=3307
DB_DATABASE=testing
DB_USERNAME=testuser
DB_PASSWORD=password

 



Docker関連ファイルの作成

次はDocker関連ファイルを作成します。

Laravel10には最初からDocker関連ファイルが存在し、sailコマンドで使うことも可能ですが、バックエンドAPIについては実務を考慮し、後々本番環境へリリースすることを考えると、自前で用意したDockerfileを使いながら開発する方がおすすめです。

今回は一つのDockerfileだけで環境構築ができるようにするため、Dockerイメージ「php:8.2-apache」を使った方法についてご紹介します。

まずは既に存在している「docker-compose.yml」のファイル名を、「docker-compose.sail.yml」に修正しておきます。

$ mv docker-compose.yml docker-compose.sail.yml

 

次に以下のコマンドを実行し、Docker関連ファイルを作成します。

$ touch docker-compose.yml
$ mkdir docker
$ cd docker
$ mkdir local
$ cd local
$ mkdir php
$ mkdir mysql
$ cd mysql
$ touch my.cnf
$ cd ..
$ cd php
$ touch Dockerfile
$ touch 000-default.conf
$ touch php.ini
$ cd ../../..

 

version: '3.8'
services:
  app:
    build:
      context: .
      dockerfile: ./docker/local/php/Dockerfile
    container_name: app
    volumes:
      - .:/app
    ports:
      - '80:8080'
    depends_on:
      - mysql
      - mysql_test
mysql:
  image: mysql:8.0
  container_name: mysql
  environment:
    MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
    MYSQL_ROOT_HOST: "%"
    MYSQL_DATABASE: '${DB_DATABASE}'
    MYSQL_USER: '${DB_USERNAME}'
    MYSQL_PASSWORD: '${DB_PASSWORD}'
    MYSQL_ALLOW_EMPTY_PASSWORD: 1
    TZ: 'Asia/Tokyo'
  command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
  volumes:
    - mysql-local-data:/var/lib/mysql
    - ./docker/local/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
  ports:
    - 3306:3306
mysql_test:
  image: mysql:8.0
  container_name: mysql_test
  environment:
    MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
    MYSQL_ROOT_HOST: "%"
    MYSQL_DATABASE: 'testing'
    MYSQL_USER: 'testuser'
    MYSQL_PASSWORD: '${DB_PASSWORD}'
    MYSQL_TCP_PORT: 3307
    MYSQL_ALLOW_EMPTY_PASSWORD: 1
    TZ: 'Asia/Tokyo'
  command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
  volumes:
    - mysql-local-test-data:/var/lib/mysql
    - ./docker/local/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
  ports:
    - 3307:3306
  expose:
    - 3307
  volumes:
    mysql-local-data:
      driver: local
    mysql-local-test-data:
      driver: local

 

FROM php:8.2-apache

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_NO_INTERACTION 1
ENV COMPOSER_HOME /composer

RUN mkdir /app
WORKDIR /app

RUN apt-get update && apt-get install -y \
    libzip-dev \
    && docker-php-ext-install zip pdo_mysql

COPY --from=composer /usr/bin/composer /usr/bin/composer

COPY . /app
COPY ./docker/local/php/php.ini /usr/local/etc/php/php.ini
COPY ./docker/local/php/000-default.conf /etc/apache2/sites-available/000-default.conf

RUN composer install && \
php artisan cache:clear && \
php artisan config:clear

RUN chmod 777 -R storage && \
echo "Listen 8080" >> /etc/apache2/ports.conf && \
a2enmod rewrite

CMD ["apache2-foreground"]

 

<VirtualHost *:8080>
    ServerAdmin webmaster@localhost
    DocumentRoot /app/public/

<Directory /app/>
    AllowOverride All
    Require all granted
</Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

 

zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /var/log/php/php-error.log
default_charset = UTF-8

[Date]
date.timezone = Asia/Tokyo

[mysqlnd]
mysqlnd.collect_memory_statistics = on

[Assertion]
zend.assertions = 1

[mbstring]
mbstring.language = Japanese

※000-default.confやphp.iniは開発用の設定です。本番環境で使う際はカスタマイズして下さい。

 

各種ファイル作成後、以下のコマンドを実行してビルドします。

$ docker compose build --no-cache

 

次に以下のコマンドを実行し、コンテナを起動します。

$ docker compose up -d

 

ブラウザから「http://localhost」にアクセスし、以下のように表示されればOKです。

 

Sanctumのアンインストール

次に今回はデフォルトで入っているSanctum(認証系のシステム)は利用しないため、以下のコマンドを実行してアンインストールし、設定ファイル「config/sanctum.php」も削除します。

$ docker compose exec app composer remove laravel/sanctum

 

テスト用DBのアプリケーションキーの設定

次に以下のコマンドを実行し、テスト用DBのアプリケーションキー(.env.testingの「APP_KEY」の値)を設定します。

$ docker compose exec app php artisan key:generate --env=testing

 

既存のマイグレーションファイルの削除と新規作成

次に既に作成されているマイグレーションファイルを全て削除し、以下のコマンドを実行して新しいユーザーテーブル用のマイグレーションファイルを作成します。

$ docker compose exec app php artisan make:migration create_users_table

 

そして、新しく作成したマイグレーションファイルの中身は次のように修正します。

<?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('users', function (Blueprint $table) {
            $table->id();
            $table->string('uid')->unique();
            $table->string('name');
            $table->string('email');
            $table->datetimes();
            $table->softDeletesDatetime();
            $table->unique(['email', 'deleted_at'], 'unique_email_deleted_at');
        });
    }

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

※Laravelで日付項目を使う場合、datetime型を使いましょう。(デフォルトのtimestamp型は2038年までしか使えない問題があるので注意!)

 

マイグレーションの実行

以下のコマンドを実行し、マイグレーションを実行します。

$ docker compose exec app php artisan migrate
$ docker compose exec app php artisan migrate --database=mysql_test --env=testing

※テスト用DBにマイグレーションを実行する場合はオプションを付けます。

 



Userモデルの修正

次は既に作成済みのUserモデルを以下のように修正します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
    use HasFactory, Notifiable, SoftDeletes;

    // 日付フォーマットをY-m-d H:i:sに変更
    protected function serializeDate(\DateTimeInterface $date)
    {
        return $date->format('Y-m-d H:i:s');
    }

    protected $guarded = ['created_at', 'updated_at'];
}

※ユーザーテーブルについて、実務では論理削除するのが基本になると思うので、その場合は「SoftDeletes」を使います。

 

リポジトリパターンでAPIを作成する

次にユーザーに関するAPIを作りますが、その後のテストの作成や運用なども考慮し、リポジトリパターンという手法で作ります。

※業務ロジックはコントローラーに直接書かない方がいいのと、外部ライブラリを使用した部分のテストコードを書く際にモック化するのが容易になります。

 

リポジトリとサービスの作成

まずは以下のコマンドを実行し、各種ディレクトリやファイルを作成します。

$ mkdir -p app/Repositories/User
$ mkdir -p app/Services/User
$ touch app/Repositories/User/UserRepository.php
$ touch app/Repositories/User/UserRepositoryInterface.php
$ touch app/Services/UserService.php

 

次に作成したファイルの中身は以下のように記述します。

<?php

namespace App\Repositories\User;

use App\Models\User;

interface UserRepositoryInterface
{
    public function createUser(string $uid, string $name, string $email);
    public function saveUser(User $user);
    public function getAllUserWithTrashed();
    public function getUserFromUid(string $uid);
    public function deleteUser(User $user);
}

 

<?php

namespace App\Repositories\User;

use App\Models\User;

class UserRepository implements UserRepositoryInterface
{
    public function createUser(
        string $uid,
        string $name,
        string $email
    )
    {
        $user = new User();
        $user->uid = $uid;
        $user->name = $name;
        $user->email = $email;

        return $user;
    }

    public function saveUser(User $user)
    {
        return $user->save();
    }

    public function getAllUserWithTrashed()
    {
        // 論理削除データも取得
        return User::withTrashed()->get();
    }

    public function getUserFromUid(string $uid)
    {
        return User::where('uid', $uid)->first();
    }

    public function deleteUser(User $user)
    {
        return $user->delete();
    }
}

 

<?php

namespace App\Services;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Repositories\User\UserRepositoryInterface as UserRepository;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Exceptions\HttpResponseException;
use \Symfony\Component\HttpFoundation\Response;

class UserService extends Controller
{
    public function __construct(UserRepository $userRepo)
    {
        $this->userRepositry = $userRepo;
    }

    public function createUser(Request $request)
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepositry->createUser(
                        $request->uid,
                        $request->name,
                        $request->email
                    );

            $this->userRepositry->saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/createUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK'], Response::HTTP_CREATED);
    }

    public function getUsers(Request $request)
    {
        try {

            $users = $this->userRepositry->getAllUserWithTrashed();

        } catch (\Exception $e) {
            Log::error("UserService/getUsersでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return $this->jsonResponse($users);
    }

    public function getUser(Request $request, string $uid)
    {
        try {

            $user = $this->userRepositry->getUserFromUid($uid);

        } catch (\Exception $e) {
            Log::error("UserService/getUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return $this->jsonResponse($user);
    }

    public function updateUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepositry->getUserFromUid($uid);

            if (!is_null($request->name)) {
                $user->name = $request->name;
            }

            if (!is_null($request->email)) {
                $user->email = $request->email;
            }

            $this->userRepositry->saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/updateUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK']);
    }

    public function destroyUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepositry->getUserFromUid($uid);

            $this->userRepositry->deleteUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/destroyUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK']);
    }
}

 

次にjson出力時の日本語の文字化け対応をするため、共通処理として「Controller.php」に関数「jsonResponse」を追加して使えるようにしておきます。

※上記の「$this->jsonResponse($users)」の部分などで使っています。

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, ValidatesRequests;

    public function jsonResponse($data, $code = 200)
    {
        return response()->json(
                   $data,
                   $code,
                   ['Content-Type' => 'application/json;charset=UTF-8', 'Charset' => 'utf-8'],
                   JSON_UNESCAPED_UNICODE
               );
    }
}

 

次に作成したサービスとリポジトリを使えるようにするため、「AppServiceProvider.php」の「register()」の部分に登録します。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
    * Register any application services.
    */
    public function register(): void
    {
        $this->app->bind('App\Services\UserService');
        $this->app->bind('App\Repositories\User\UserRepositoryInterface', 'App\Repositories\User\UserRepository');
    }

    /**
    * Bootstrap any application services.
    */
    public function boot(): void
    {
        //
    }
}

 

ユーザーコントローラーの作成

次に以下のコマンドを実行し、ユーザーコントローラーを作成します。

$ docker compose exec app php artisan make:controller Api/UserController

 

次にユーザーコントローラーの中身は次のように記述します。

※リポジトリパターンでは業務ロジックはサービスに寄せるため、コントローラーはスッキリした記述になります。

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\UserService;

class UserController extends Controller
{
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function create(Request $request)
    {
        return $this->userService->createUser($request);
    }

    public function users(Request $request)
    {
        return $this->userService->getUsers($request);
    }

    public function user(Request $request, string $uid)
    {
        return $this->userService->getUser($request, $uid);
    }

    public function update(Request $request, string $uid)
    {
        return $this->userService->updateUser($request, $uid);
    }

    public function delete(Request $request, string $uid)
    {
        return $this->userService->destroyUser($request, $uid);
    }
}

 

ルーティングの追加

次にユーザーコントローラーのルーティングを追加するため、「routes/api.php」を以下のように修正します。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\UserController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// 今回は不要なのでコメントアウト
// Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
// return $request->user();
// });

Route::prefix('v1')->group(function() {
    Route::post('/user', [UserController::class, 'create']);
    Route::get('/users', [UserController::class, 'users']);
    Route::get('/user/{uid}', [UserController::class, 'user']);
    Route::put('/user/{uid}', [UserController::class, 'update']);
    Route::delete('/user/{uid}', [UserController::class, 'delete']);
});

 

PostmanでAPIの動作確認

次にAPIの動作を確認しますが、APIの動作確認にはPostmanを使うのがおすすめです。

ここではPostmanの詳細は割愛しますが、もしまだ使ったことがない方は以下の関連記事にある「17.5 Postmanの登録方法」の部分で解説しているので、ぜひ参考に準備をしてみて下さい。

関連記事👇

SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS(React)・Vercel・Rails7(APIモード)・AWS ECS(Fargate)】

2022年11月22日

 

ではまずユーザー作成のAPIの動作確認をします。

Postmanでリクエストを作成し、名前は「Create User」、メソッドは「POST」、URLは「http://localhost/api/v1/user」を入力します。

 

次にJSONデータをPOSTするため、タブ「Headers」をクリックし、Keyに「Content-Type」、Valueに「application/json」を設定します。

 

次にタブ「Body」をクリックし、「raw」を選択してPOSTするJSONデータを以下のように記述します。

 

次に画面右上の「Send」をクリックするとリクエストが実行され、ステータス「201」、レスポンスに設定したメッセージが出力されていればOKです。

 

次に作成したユーザーを確認するため、新しくリクエストを作成し、名前は「Get Users」、メソッドは「GET」、URLは「http://localhost/api/v1/users」を入力します。

そして画面右上の「Send」をクリックし、ステータス「200」、レスポンスに上記で登録したユーザーのデータが出力されればOKです。

 

次にuidを指定して対象ユーザーを取得するAPIを確認するため、新しくリクエストを作成し、名前は「Get User From Uid」、メソッドは「GET」、URLは「http://localhost/api/v1/user/A0001」を入力します。

そして画面右上の「Send」をクリックし、ステータス「200」、レスポンスに上記で登録したユーザーのデータが出力されればOKです。

 

次にユーザー情報を更新するAPIを確認するため、新しくリクエストを作成し、名前は「Update User」、メソッドは「PUT」、URLは「http://localhost/api/v1/user/A0001」、Headersに「Content-Type」と「application/json」、bodyでrawを選択し、更新する項目についてJSONデータを以下のように記述します。

 

そして画面右上の「Send」をクリックし、ステータス「200」、レスポンスに設定したメッセージが出力されていればOKです。

 

さらにもう一度リクエスト「Get User From Uid」実行し、nameが更新されていればOKです。

 

次にユーザーを削除するAPIを確認するため、新しくリクエストを作成し、名前は「Delete User」、メソッドは「DELETE」、URLは「http://localhost/api/v1/user/A0001」を入力します。

そして画面右上の「Send」をクリックし、ステータス「200」、レスポンスに設定したメッセージが出力されていればOKです。

 

さらにもう一度リクエスト「Get User From Uid」実行し、削除したユーザーが取得できなければOKです。

 

最後に論理削除データも取得できるようにしたAPIのリクエスト「Get Users」をもう一度実行し、論理削除したユーザーが取得でき、かつ項目「deleted_at」に削除日時が設定されていればOKです。

 



リクエストにバリデーションを付ける

次はユーザー作成や更新処理のAPIなど、POSTされるリクエストボディに対して、バリデーションチェックを付けます。

以下のコマンドを実行し、まずはバリデーションの共通処理用のトレイトを作成します。

$ mkdir app/Http/Requests
$ touch app/Http/Requests/ValidationFailedTrait.php

 

次に作成した「ValidationFailedTrait.php」の中身は以下のように記述します。

<?php

namespace App\Http\Requests;

use App\Http\Controllers\Controller;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

trait ValidationFailedTrait
{
    /**
    * エラーメッセージを出力
    *
    * @return viod
    */
    protected function failedValidation(Validator $validator)
    {
        $response['errors'] = $validator->errors()->toArray();

        $controller = new Controller();

        throw new HttpResponseException(
            $controller->jsonResponse($response, 422)
        );
    }

    /**
    * 共通エラーメッセージ
    */
    public function messages()
    {
        return [
            'required' => ':attributeは必須項目です。',
            'string' => ':attributeは文字列で入力して下さい。',
            'email' => ':attributeは有効なメールアドレス形式で入力して下さい。',
            'max' => [
                'string' => ':attributeは:max文字以内で入力して下さい。',
            ],
            'min' => [
                'string' => ':attributeは:min文字以上で入力して下さい。',
            ],
        ];
    }

    /**
    * 共通エラー文言
    */
    public function attributes()
    {
        return [
            'uid' => 'uid',
            'name' => '名前',
            'email' => 'メールアドレス',
            'password' => 'パスワード',
        ];
    }
}

 

次に以下のコマンドを実行し、フォームリクエストを作成します。

$ docker compose exec app php artisan make:request User/CreateUserRequest
$ docker compose exec app php artisan make:request User/UpdateUserRequest

 

次に作成したファイルの中身はそれぞれ以下のように記述します。

<?php

namespace App\Http\Requests\User;

use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\ValidationFailedTrait;
class CreateUserRequest extends FormRequest
{
    use ValidationFailedTrait;
    /**
    * Determine if the user is authorized to make this request.
    */
    public function authorize(): bool
    {
        // 認可処理は無効にする
        return true;
    }

    /**
    * Get the validation rules that apply to the request.
    *
    * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
    */
    public function rules(): array
    {
        $validate = [];

        $validate += [
            'uid' => [
                'required',
                'string'
            ]
        ];

        $validate += [
            'name' => [
                'required',
                'string',
                'max:20'
            ]
        ];

        $validate += [
            'email' => [
                'required',
                'email'
            ]
        ];

        return $validate;
    }
}

 

<?php

namespace App\Http\Requests\User;

use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\ValidationFailedTrait;
class CreateUserRequest extends FormRequest
{
    use ValidationFailedTrait;
    /**
    * Determine if the user is authorized to make this request.
    */
    public function authorize(): bool
    {
        // 認可処理は無効にする
        return true;
    }

    /**
    * Get the validation rules that apply to the request.
    *
    * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
    */
    public function rules(): array
    {
        $validate = [];

        $validate += [
            'name' => [
                'string',
                'max:20'
            ]
        ];

        $validate += [
            'email' => [
                'email'
            ]
        ];

        return $validate;
    }
}

 

次にユーザーコントローラーの「create」と「update」のメソッドの型「Request」部分を作成したフォームリクエストに変更します。

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\UserService;
use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\UpdateUserRequest;

class UserController extends Controller
{

・・・

    public function create(CreateUserRequest $request)
    {
        return $this->userService->createUser($request);
    }

・・・

    public function update(UpdateUserRequest $request, string $uid)
    {
        return $this->userService->updateUser($request, $uid);
    }

・・・
}

 

これでバリデーションの追加が完了したのでPostmanでAPIを確認します。

例えばユーザー作成のAPIでリクエストボディを以下のようにして実行すると、バリデーションチェックに引っ掛かり、ステータス「422」で対応するエラーメッセージが出力されればOKです。

 



Firebase AuthenticationでAPIに認証機能を付ける

次に今回は認証基盤をGoogleの「Firebase Authentication」とし、APIに認証機能を付与します。

ここではFirebase Authenticationの詳細は割愛しますが、アカウントの作成などがまだの方は以下の関連記事にある「Firebase Authenticationによるログイン関連機能の実装方法」の部分で解説しているので、ぜひ参考に準備をしてみて下さい。

関連記事👇

SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS(React)・Vercel・Rails7(APIモード)・AWS ECS(Fargate)】

2022年11月22日

 

ではまずFirebaseプロジェクトの設定画面から接続情報を取得するため、「サービスアカウント>新しい秘密鍵を生成」をクリックします。

 

そして注意事項についてのポップアップが表示されるので確認し、「キーを生成」をクリックします。

 

これでFirebaseプロジェクトへの接続情報が記載されたjsonファイルをダウンロードできます。

 

次に環境変数設定用のファイルである「.env」や「.env.testing」に以下の項目でFirebaseプロジェクトの接続情報を設定します。

FIREBASE_TYPE=
FIREBASE_PROJECT_ID=
FIREBASE_PRIVATE_KEY_ID=
FIREBASE_PRIVATE_KEY=
FIREBASE_CLIENT_EMAIL=
FIREBASE_CLIENT_ID=
FIREBASE_AUTH_URI=
FIREBASE_TOKEN_URI=
FIREBASE_AUTH_PROVIDER_X509_CERT_URL=
FIREBASE_CLIENT_X509_CERT_URL=
FIREBASE_UNIVERSE_DOMAIN=

 

次に以下のコマンドを実行し、Firebaseを使えるようにするためのライブラリをインストールします。

$ docker compose exec app composer require kreait/firebase-php
$ docker compose exec app composer require kreait/laravel-firebase

 

次に以下のコマンドを実行し、コンフィグ用のファイルを出します。

$ docker compose exec app php artisan vendor:publish --provider="Kreait\Laravel\Firebase\ServiceProvider" --tag=config

 

次にコンフィグ用のファイル「config/firebase.php」にあるcredentialsの設定部分を以下のように修正します。

・・・

// 'credentials' => env('FIREBASE_CREDENTIALS', env('GOOGLE_APPLICATION_CREDENTIALS')),
'credentials' => [
    'type' => env('FIREBASE_TYPE', ''),
    'project_id' => env('FIREBASE_PROJECT_ID', ''),
    'private_key_id' => env('FIREBASE_PRIVATE_KEY_ID', ''),
    'private_key' => env('FIREBASE_PRIVATE_KEY', ''),
    'client_email' => env('FIREBASE_CLIENT_EMAIL', ''),
    'client_id' => env('FIREBASE_CLIENT_ID', ''),
    'auth_uri' => env('FIREBASE_AUTH_URI', ''),
    'token_uri' => env('FIREBASE_TOKEN_URI', ''),
    'auth_provider_x509_cert_url' => env('FIREBASE_AUTH_PROVIDER_X509_CERT_URL', ''),
    'client_x509_cert_url' => env('FIREBASE_CLIENT_X509_CERT_URL', ''),
    'universe_domain' => env('FIREBASE_UNIVERSE_DOMAIN', ''),
],

・・・

 

次にFirebaseの認証機能を付けるため、「Providers/AuthServiceProvider.php」を以下のように修正します。

<?php

namespace App\Providers;

// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
// Firebaseの認証機能用
use Illuminate\Support\Facades\Auth;
use Kreait\Firebase\Contract\Auth as FirebaseAuth;
use \Symfony\Component\HttpFoundation\Response;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    /**
    * The model to policy mappings for the application.
    *
    * @var array<class-string, class-string>
    */
    protected $policies = [
        //
    ];

    /**
    * Register any authentication / authorization services.
    */
    public function boot(): void
    {
        // Firebaseによる認証
        Auth::viaRequest('firebase', function (Request $request) {
            $idToken = $request->header('Authorization');
            if (empty($idToken)) {
                throw new HttpResponseException(response()->json(['message' => 'Bad Request'], Response::HTTP_BAD_REQUEST));
            }

            $idToken = str_replace('Bearer ', '', $idToken);
            $firebaseAuth = app(FirebaseAuth::class);
            try {
                $verifiedIdToken = $firebaseAuth->verifyIdToken($idToken);
            } catch (\Exception $e) {
                throw new HttpResponseException(response()->json(['message' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED));
            }

            $uid = $verifiedIdToken->claims()->get('sub');
            $email = $verifiedIdToken->claims()->get('email');

            // DBからユーザー情報取得(論理削除データも含む)
            $user = User::withTrashed()->where('uid', $uid)->first();

            if (!empty($user->deleted_at)) {
                throw new HttpResponseException(response()->json(['message' => 'Bad Request'], Response::HTTP_BAD_REQUEST));
            }

            if (empty($user)) {
                $user = new User();
                $user->uid = $uid;
                $user->email = $email;
            }

            return $user;
        });
    }
}

 

次にミドルウェアでFirebaseの認証を利用できるようにするため、「config/auth.php」にあるguardsの設定を以下のように修正します。

・・・

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    // Firebase用の設定を追加
    'api' => [
        'driver' => 'firebase',
    ],
],

・・・

 

次にルーティングの設定で、ユーザーの取得、更新、削除のAPIについてFirebaseの認証機能を有効にするため、「routes/api.php」を以下のように修正します。

・・・

Route::middleware('auth:api')->group(function () {
    Route::prefix('v1')->group(function() {
        Route::get('/user/{uid}', [UserController::class, 'user']);
        Route::put('/user/{uid}', [UserController::class, 'update']);
        Route::delete('/user/{uid}', [UserController::class, 'delete']);
    });
});

Route::prefix('v1')->group(function() {
    Route::post('/user', [UserController::class, 'create']);
    Route::get('/users', [UserController::class, 'users']);
});

・・・

 

これでFirebase認証をかけたAPIについては、リクエストヘッダーにFirebaseのidTokenを付与して認証が必要になります。

例えばユーザー取得のAPIを再度実行し、ステータス「400」で対応するエラーメッセージが出力されればOKです。

 

また、Firebase認証を試したい場合は、Firebase画面からユーザーを追加(メールアドレスとパスワードを入力して作成)し、メールアドレス、パスワード、uidをメモします。

 

次に上記で作成したユーザー作成APIを使用し、メモしたメールアドレスとuidで新規ユーザーを作成します。

 

次にFirebaseのAPIを使い、Firebaseに作成したユーザーのidTokenを取得します。

Postmanでリクエストを作成し、名前は「Get Firebase idToken」、メソッドは「POST」、URLは「https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=プロジェクト設定画面から確認できるウェブAPIキー」を入力後、タブ「Headers」をクリックしてKeyに「Content-Type」、Valueに「application/json」を設定します。

 

そしてbodyでrawを選択し、JSONデータにFirebaseユーザーのemail、password、およびreturnSecureTokenにtrueを設定してsendボタンをクリック後、取得できたidTokenの値をメモします。

 

次にユーザー取得APIでURLは「http://localhost/api/v1/user/Firebaseユーザーのuid」を入力し、タブ「Authorization」でtypeに「Bearer Token」を選択後、Tokenの値に先ほど取得したFirebaseユーザーのidTokenの値を設定します。

 

SendボタンをクリックしてAPIを実行し、以下のように先ほど登録したユーザー情報を取得できればOKです。

 



Policyで更新や削除APIに認可機能を付ける

次に更新や削除APIには認可機能を付与し、自分のデータについて更新や削除ができるのは自分だけになるようにします。

今回はLaravelのPolicyで認可機能を付けるため、以下のコマンドを実行してファイルを作成します。

$ docker compose exec app php artisan make:policy UserPolicy --model=User

 

次に作成したファイル「Policies/UserPolicy.php」のupdateとdeleteの部分を以下のように修正します。

・・・

    /**
    * Determine whether the user can update the model.
    */
    public function update(User $user, User $model): bool
    {
        return $user->id === $model->id;
    }

    /**
    * Determine whether the user can delete the model.
    */
    public function delete(User $user, User $model): bool
    {
        return $user->id === $model->id;
    }

・・・

 

次に「Providers/AuthServiceProvider.php」に作成したPolicyを追加します。

<?php

namespace App\Providers;

// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
// Firebaseの認証機能用
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\Exceptions\HttpResponseException;
use \Symfony\Component\HttpFoundation\Response;
use Kreait\Firebase\Contract\Auth as FirebaseAuth;
use App\Models\User;
// ポリシー追加
use App\Policies\UserPolicy;

class AuthServiceProvider extends ServiceProvider
{
    /**
    * The model to policy mappings for the application.
    *
    * @var array<class-string, class-string>
    */
    protected $policies = [
        User::class => UserPolicy::class,
    ];

・・・

 

次に「Services/UserService.php」の「updateUser」と「destroyUser」に認可処理を追加します。

・・・
    
    public function updateUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepositry->getUserFromUid($uid);

            // 認可処理
            if ($request->user()->cannot('update', $user)) {
                return response()->json(['message' => 'Bad Request'], Response::HTTP_BAD_REQUEST);
            }

            if (!is_null($request->name)) {
                $user->name = $request->name;
            }

            if (!is_null($request->email)) {
                $user->email = $request->email;
            }

            $this->userRepositry->saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/updateUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK']);
    }

    public function destroyUser(Request $request, string $uid)
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepositry->getUserFromUid($uid);

            // 認可処理
            if ($request->user()->cannot('delete', $user)) {
                return response()->json(['message' => 'Bad Request'], Response::HTTP_BAD_REQUEST);
            }

            $this->userRepositry->deleteUser($user);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
            Log::error("UserService/destroyUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK']);
    }

・・・

 

これで更新と削除のAPIに認可機能が付いたため、URLのuidがidTokenを検証して取得するuidと一致している場合にのみ更新可能です。

 

 



サインアップ機能のAPIを作成する

ここまででFirebaseとの連携ができるようになったので、合わせて簡単なサインアップ機能のAPIを作成してみます。

まずは以下のコマンドを実行し、リポジトリとサービス用のファイルを作成します。

$ cd app/Repositories
$ mkdir Auth
$ cd Auth
$ touch AuthRepositoryInterface.php
$ touch AuthRepository.php
$ cd ../../Services
$ touch AuthService.php
$ cd ../..

 

次に作成したファイルの中身を次のように記述します。

<?php

namespace App\Repositories\Auth;

interface AuthRepositoryInterface
{
    public function createFirebaseUser(
        string $email,
        string $password
    );
}

 

<?php

namespace App\Repositories\Auth;

use Kreait\Firebase\Contract\Auth as FirebaseAuth;

class AuthRepository implements AuthRepositoryInterface
{
    public function __construct(FirebaseAuth $firebaseAuth)
    {
        $this->firebaseAuth = $firebaseAuth;
    }

    public function createFirebaseUser(
        string $email,
        string $password
    )
    {
        $userProperties = [
            'email' => $email,
            'password' => $password,
        ];
        $createdUser = $this->firebaseAuth->createUser($userProperties);

        return $createdUser;
    }

    public function deleteFirebaseUser(string $uid)
    {
        $this->firebaseAuth->deleteUser($uid);
    }
}

 

<?php

namespace App\Services;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Repositories\Auth\AuthRepositoryInterface as AuthRepository;
use App\Repositories\User\UserRepositoryInterface as UserRepository;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Exceptions\HttpResponseException;
use \Symfony\Component\HttpFoundation\Response;

class AuthService extends Controller
{
    public function __construct(
        AuthRepository $authRepo,
        UserRepository $userRepo
    )
    {
        $this->authRepositry = $authRepo;
        $this->userRepositry = $userRepo;
    }

    public function registerUser(Request $request)
    {
        try {
            DB::beginTransaction();

            // Firebaseにユーザー作成
            $authUser = $this->authRepositry
                             ->createFirebaseUser(
                                 $request->email,
                                 $request->password
                             );

            // DBにユーザー作成
            $user = $this->userRepositry->createUser(
                             $authUser->uid,
                             $request->name,
                             $authUser->email
                         );
            $this->userRepositry->saveUser($user);

            DB::commit();
        } catch (\Exception $e) {
            // ロールバック
            DB::rollback();
            // Firebaseユーザーが作成済みの場合は削除処理
            if (!empty($authUser)) {
                $this->authRepositry->deleteFirebaseUser($authUser->uid);
            }
            Log::error("AuthService/registerUserでエラー");
            throw new HttpResponseException(response()->json(['message' => 'Internal Server Error'], Response::HTTP_INTERNAL_SERVER_ERROR));
        }

        return response()->json(['message' => 'OK'], Response::HTTP_CREATED);
    }
}

 

次に作成したサービスとリポジトリを使えるようにするため、「AppServiceProvider.php」の「register()」の部分に追加します。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
    * Register any application services.
    */
    public function register(): void
    {
        $this->app->bind('App\Services\UserService');
        $this->app->bind('App\Repositories\User\UserRepositoryInterface', 'App\Repositories\User\UserRepository');
        $this->app->bind('App\Services\AuthService');
        $this->app->bind('App\Repositories\Auth\AuthRepositoryInterface', 'App\Repositories\Auth\AuthRepository');
    }

    /**
    * Bootstrap any application services.
    */
    public function boot(): void
    {
        //
    }
}

 

次に以下のコマンドを実行し、リクエスト用のバリデーションを作成します。

$ cd app/Http/Requests
$ mkdir Auth
$ cd Auth
$ touch SignupRequest.php
$ cd ../..

 

次に作成したファイルの中身を次のように記述します。

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\ValidationFailedTrait;

class SignupRequest extends FormRequest
{
    use ValidationFailedTrait;

    /**
    * Determine if the user is authorized to make this request.
    */
    public function authorize(): bool
    {
        // 認可処理は無効にする
        return true;
    }

    /**
    * Get the validation rules that apply to the request.
    *
    * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
    */
    public function rules(): array
    {
        $validate = [];

        $validate += [
            'name' => [
                'required',
                'string',
                'max:20'
            ]
        ];

        $validate += [
            'email' => [
                'required',
                'email'
            ]
        ];

        $validate += [
            'password' => [
                'required',
                'string',
                'min:6'
            ]
        ];

        return $validate;
    }
}

 

次に以下のコマンドを実行し、Authコントローラーを作成します。

$ docker compose exec app php artisan make:controller Api/AuthController

 

次に作成したファイルの中身を次のように記述します。

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\AuthService;
use App\Http\Requests\Auth\SignupRequest;

class AuthController extends Controller
{
    public function __construct(AuthService $authService)
    {
        $this->authService = $authService;
    }

    public function signup(SignupRequest $request)
    {
        return $this->authService->registerUser($request);
    }
}

 

次にサインアップAPIのルーティングを追加します。

・・・

Route::middleware('auth:api')->group(function () {
    Route::prefix('v1')->group(function() {
        Route::get('/user/{uid}', [UserController::class, 'user']);
        Route::put('/user/{uid}', [UserController::class, 'update']);
        Route::delete('/user/{uid}', [UserController::class, 'delete']);
    });
});

Route::prefix('v1')->group(function() {
    Route::post('/user', [UserController::class, 'create']);
    Route::get('/users', [UserController::class, 'users']);
    Route::post('/signup', [AuthController::class, 'signup']);
});

・・・

 

これでサインアップAPIの作成が完了したので、Postmanで試してみます。

まずリクエストを作成し、名前は「Signup」、メソッドは「POST」、URLは「http://localhost/api/v1/signup」を入力後、タブ「Headers」をクリックしてKeyに「Content-Type」、Valueに「application/json」を設定します。

 

次に「Body>raw」をクリックしてPOSTするデータを記述後、SendボタンをクリックしてAPIを実行し、ステータス「201」で正常終了すればOKです。

 

そしてユーザー取得APIを実行し、POSTしたデータのユーザーが作成されていればOKです。

 

最後にFirebaseを確認し、POSTしたデータのメールアドレスが登録されていればOKです。

 



Pestとテストコードの追加

次はテストコードの追加を行うため、以下のコマンドを実行し、Pestのインストールと初期化を行います。

$ docker compose exec app composer require pestphp/pest-plugin-laravel --dev
$ docker compose exec app ./vendor/bin/pest --init

 

初期化の際に聞かれる質問については「no」を入力して進めればOKです。

 

これで設定用ファイル「tests/Pest.php」が作成されるので、「Illuminate\Foundation\Testing\RefreshDatabase::class,」部分のコメントアウトを外し、テスト実行後にDBをリフレッシュさせる設定を有効にしておきます。

<?php

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/

uses(
    Tests\TestCase::class,
    Illuminate\Foundation\Testing\RefreshDatabase::class,
)->in('Feature');

・・・

 

次に以下のコマンドを実行し、テストを実行してみます。

$ docker compose exec app php artisan test

 

テスト実行後、既にあるサンプルファイルのテストが正常終了すればOKです。

 

次に以下のコマンドを実行し、ユーザーAPI用のテストファイルを作成します。

$ docker compose exec app php artisan pest:test UserTest

 

次に作成したファイルの中身を次のように記述します。

<?php

use Illuminate\Support\Str;
use App\Models\User;

test('ユーザーが新規作成され、ステータス201で正常終了すること', function () {
    // API実行
    $path = "/api/v1/user";
    $uid = Str::random(10);
    $name = "testUser";
    $email = "test@example.com";
    $body = [
        'uid' => $uid,
        'name' => $name,
        'email' => $email
    ];
    $response = $this->post($path, $body);

    // 検証
    expect($response->status())->toBe(201);
    $this->assertDatabaseHas(User::class, [
        'uid' => $uid,
        'name' => $name,
        'email' => $email,
    ]);
});

test('対象ユーザーをjson形式で取得し、ステータス200で正常終了すること', function () {
    // 事前にユーザーデータを作成
    $uid = Str::random(10);
    $name = "testUser";
    $email = "test@example.com";
    $this->user = User::create([
        'uid' => $uid,
        'name' => $name,
        'email' => $email
    ]);

    // API実行
    $path = "/api/v1/user/{$uid}";
    $response = $this->actingAs($this->user, 'api')->get($path);

    // 検証
    $res_json = json_encode(User::where('uid', $uid)->first());
    expect($response->status())->toBe(200);
    expect($response->content())->toBeJson();
    expect($response->content())->toBe($res_json);
});

test('未認証の場合は対象ユーザーを取得できず、ステータス400を返すこと', function () {
    // 事前にユーザーデータを作成
    $uid = Str::random(10);
    $name = "testUser";
    $email = "test@example.com";
    $this->user = User::create([
        'uid' => $uid,
        'name' => $name,
        'email' => $email
    ]);

    // API実行
    $path = "/api/v1/user/{$uid}";
    $response = $this->get($path);

    // 検証
    $res_json = json_encode([ "message" => "Bad Request" ]);
    expect($response->status())->toBe(400);
    expect($response->content())->toBe($res_json);
});

※Pestを利用した方がテストコードを簡潔に書けるほか、PHPUnitのアサーションもそのまま使えるのがメリットです。また認証が必要なAPIについては、「actingAs($this->user, ‘api’)」の部分で認証処理をpassさせています。そのほか今回は使いませんでしたが、事前にユーザーを作成している部分はFactoryを使うと簡潔にできます。

 

テストを実行し、以下のように全てのテストがpassすればOKです。

 

次に以下のコマンドを実行し、サインアップAPI用のテストファイルを作成します。

$ docker compose exec app php artisan pest:test AuthTest

 

次に作成したファイルの中身を次のように記述します。

<?php

use Illuminate\Support\Str;
use App\Repositories\Auth\AuthRepositoryInterface as AuthRepository;
use App\Repositories\User\UserRepositoryInterface as UserRepository;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use App\Models\User;

test('サインアップ処理が正常終了し、DBにユーザーが新規作成されること', function () {
    // Firebaseのユーザー作成処理をモック化
    $uid = Str::random(10);
    $name = "testUser";
    $email = "test@example.com";
    $password = Str::random(10);
    $properties = [
        'uid' => $uid,
        'email' => $email
    ];
    $returnObj = (object) $properties;
    $mockAuthRepo = Mockery::mock(AuthRepository::class)->makePartial();
    $mockAuthRepo->shouldReceive('createFirebaseUser')
                 ->once()
                 ->andReturn($returnObj);
    $this->app->instance(AuthRepository::class, $mockAuthRepo);

    // API実行
    $path = "/api/v1/signup";
    $body = [
        'name' => $name,
        'email' => $email,
        'password' => $password
    ];
    $response = $this->post($path, $body);

    // 検証
    expect($response->status())->toBe(201);
    $this->assertDatabaseHas(User::class, [
        'uid' => $uid,
        'name' => $name,
        'email' => $email,
    ]);
});

test('ユーザー保存処理でエラーの場合、ロールバックされてユーザーが作成されないこと', function () {
    // Firebaseのユーザー作成、削除処理をモック化
    $uid = Str::random(10);
    $name = "testUser";
    $email = "test@example.com";
    $password = Str::random(10);
    $properties = [
        'uid' => $uid,
        'email' => $email
    ];
    $returnObj = (object) $properties;
    $mockAuthRepo = Mockery::mock(AuthRepository::class)->makePartial();
    $mockAuthRepo->shouldReceive('createFirebaseUser')
                 ->once()
                 ->andReturn($returnObj);
    $mockAuthRepo->shouldReceive('deleteFirebaseUser')
                 ->once()
                 ->andReturn();
    $this->app->instance(AuthRepository::class, $mockAuthRepo);

    // ユーザー保存処理をモック化
    $message = 'Internal Server Error';
    $code = 500;
    $mockUserRepo = Mockery::mock(UserRepository::class)->makePartial();
    $mockUserRepo->shouldReceive('saveUser')
                 ->andThrow(new AccessDeniedHttpException($message, null, $code));
    $this->app->instance(UserRepository::class, $mockUserRepo);

    // API実行
    $path = "/api/v1/signup";
    $body = [
        'name' => $name,
        'email' => $email,
        'password' => $password
    ];
    $response = $this->post($path, $body);

    // 検証
    $res_json = json_encode([ "message" => $message ]);
    expect($response->status())->toBe(500);
    expect($response->content())->toBe($res_json);
    $this->assertDatabaseMissing(User::class, [
        'uid' => $uid,
        'name' => $name,
        'email' => $email,
    ]);
});

※外部サービス等を利用している処理はそのままだと不本意に実行されてしまうため、モック化することで実行されるのを回避しています。リポジトリパターンでAPIを作成しておけば、このようにモック化しやすくなるのでおすすめです。

 

テストを実行し、以下のように全てのテストがpassすればOKです。

 



OpenAPIでAPI仕様書の作成

次に以下のコマンドを実行し、API仕様書を作成するためのOpenAPIに関するライブラリをインストールします。

$ docker compose exec app composer require vyuldashev/laravel-openapi

 

次に「config/app.php」のprovidersに「Vyuldashev\LaravelOpenApi\OpenApiServiceProvider::class,」を追記します。

・・・

    'providers' => ServiceProvider::defaultProviders()->merge([
        /*
        * Package Service Providers...
        */
        Vyuldashev\LaravelOpenApi\OpenApiServiceProvider::class,

・・・

 

次に以下のコマンドを実行し、コンフィグ用のファイルを出します。

$ docker compose exec app php artisan vendor:publish --provider="Vyuldashev\LaravelOpenApi\OpenApiServiceProvider" --tag="openapi-config"

 

次にコンフィグ用ファイル「config/openapi.php」を以下のように修正します。

<?php

$title = "API仕様書";
$description = "Laravel10のバックエンドAPIのAPI仕様書";

return [

    'collections' => [

        'default' => [

            'info' => [
                'title' => $title,
                'description' => $description,
                'version' => '1.0.0',
                'contact' => [],
            ],

            'servers' => [
                [
                    'url' => env('APP_URL'),
                    'description' => null,
                    'variables' => [],
                ],
            ],

            'tags' => [

                [
                    'name' => 'user',
                    'description' => 'ユーザー',
                ],

            ],

・・・

 

次に以下のコマンドを実行し、仕様書作成に必要な各種ファイルを作成します。

$ docker compose exec app php artisan openapi:make-requestbody User/CreateUser
$ docker compose exec app php artisan openapi:make-response Common/Created
$ docker compose exec app php artisan openapi:make-response Common/InternalServerError
$ docker compose exec app php artisan openapi:make-response User/User
$ docker compose exec app php artisan openapi:make-schema User
$ docker compose exec app php artisan openapi:make-security-scheme BearerToken

 

次に作成したファイルの中身を次のように記述します。(BearerTokenSecurityScheme.phpのみデフォルトのまま使う)

<?php

namespace App\OpenApi\RequestBodies\User;

use GoldSpecDigital\ObjectOrientedOAS\Objects\RequestBody;
use Vyuldashev\LaravelOpenApi\Factories\RequestBodyFactory;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;

class CreateUserRequestBody extends RequestBodyFactory
{
    public function build(): RequestBody
    {
        $response = Schema::object()
                        ->properties(
                            Schema::string('uid')
                                ->example('azby0tie9k')
                                ->description('Firebaseのuid'),
                            Schema::string('name')
                                ->example('オープンAPIユーザー')
                                ->description('名前'),
                            Schema::string('email')
                                ->example('openapi@example.com')
                                ->description('メールアドレス'),
                        )
                        ->required(
                            'uid',
                            'name',
                            'email',
                        );

        return RequestBody::create()
                   ->description('登録ユーザー情報')
                   ->content(
                       MediaType::json()
                           ->schema($response)
                   );
    }
}

 

<?php

namespace App\OpenApi\Responses\Common;

use GoldSpecDigital\ObjectOrientedOAS\Objects\Response;
use Vyuldashev\LaravelOpenApi\Factories\ResponseFactory;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;

class CreatedResponse extends ResponseFactory
{
    public function build(): Response
    {
        $response = Schema::object()
                        ->properties(
                            Schema::string('message')
                                ->example('OK')
                                ->description('created'),
                    );

        return Response::created()
                   ->description('正常終了')
                   ->content(
                       MediaType::json()
                           ->schema($response)
                   );
    }
}

 

<?php

namespace App\OpenApi\Responses\Common;

use GoldSpecDigital\ObjectOrientedOAS\Objects\Response;
use Vyuldashev\LaravelOpenApi\Factories\ResponseFactory;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;

class InternalServerErrorResponse extends ResponseFactory
{
    public function build(): Response
    {
        $response = Schema::object()
                        ->properties(
                            Schema::string('message')
                                ->example('Internal Server Error')
                                ->description('internal server error'),
                        );

        return Response::internalservererror()
                   ->description('サーバーエラー')
                   ->content(
                       MediaType::json()
                           ->schema($response)
                   );
    }
}

 

<?php

namespace App\OpenApi\Responses\User;

use GoldSpecDigital\ObjectOrientedOAS\Objects\Response;
use Vyuldashev\LaravelOpenApi\Factories\ResponseFactory;
use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;
use App\OpenApi\Schemas\UserSchema;

class UserResponse extends ResponseFactory
{
    public function build(): Response
    {
        return Response::ok()
                   ->description('ユーザー情報取得に成功')
                   ->content(
                       MediaType::json()
                           ->schema(UserSchema::ref())
                   );
    }
}

 

<?php

namespace App\OpenApi\Schemas;

use GoldSpecDigital\ObjectOrientedOAS\Contracts\SchemaContract;
use GoldSpecDigital\ObjectOrientedOAS\Objects\AllOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\AnyOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Not;
use GoldSpecDigital\ObjectOrientedOAS\Objects\OneOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
use Vyuldashev\LaravelOpenApi\Factories\SchemaFactory;
use Vyuldashev\LaravelOpenApi\Contracts\Reusable;

class UserSchema extends SchemaFactory implements Reusable
{
    /**
    * @return AllOf|OneOf|AnyOf|Not|Schema
    */
    public function build(): SchemaContract
    {
        return Schema::object('User')
                   ->properties(
                       Schema::integer('id')
                           ->example('1')
                           ->description('usersのid'),
                       Schema::string('uid')
                           ->example('azby0tie9k')
                           ->description('Firebaseのuid'),
                       Schema::string('name')
                           ->example('オープンAPIユーザー')
                           ->description('名前'),
                       Schema::string('email')
                           ->example('openapi@example.com')
                           ->description('メールアドレス'),
                       Schema::string('created_at')
                           ->format('date-time')
                           ->example('2023-12-26 23:51:57')
                           ->description('作成日時'),
                       Schema::string('updated_at')
                           ->format('date-time')
                           ->example('2023-12-26 23:51:57')
                           ->description('更新日時'),
                       Schema::string('deleted_at')
                           ->format('date-time')
                           ->example('2023-12-26 23:51:57')
                           ->description('削除日時'),
                       );
    }
}

※Schemaファイルについては、「use Vyuldashev\LaravelOpenApi\Contracts\Reusable;」を使い、classに「implements Reusable」を付与しないとエラーになる

 

次にユーザーコントローラーにOpenAPIの設定について以下のように記述します。
(各種タグ「#[OpenApi\PathItem]」や「#[OpenApi\Operation(tags: [‘user’])]」等を記述します。)
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\UserService;
use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\UpdateUserRequest;

// OpenAPI用
use Vyuldashev\LaravelOpenApi\Attributes as OpenApi;
use App\OpenApi\RequestBodies\User\CreateUserRequestBody;
use App\OpenApi\Responses\Common\CreatedResponse;
use App\OpenApi\Responses\User\UserResponse;
use App\OpenApi\Responses\Common\InternalServerErrorResponse;
use App\OpenApi\SecuritySchemes\BearerTokenSecurityScheme;

#[OpenApi\PathItem]
class UserController extends Controller
{
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    /**
    * ユーザー作成
    *
    * @param CreateUserRequest $request
    * @return CreatedResponse
    */
    #[OpenApi\Operation(tags: ['user'])]
    #[OpenApi\RequestBody(factory: CreateUserRequestBody::class)]
    #[OpenApi\Response(factory: CreatedResponse::class)]
    #[OpenApi\Response(factory: InternalServerErrorResponse::class)]
    public function create(CreateUserRequest $request)
    {
        return $this->userService->createUser($request);
    }

    public function users(Request $request)
    {
        return $this->userService->getUsers($request);
    }

    /**
    * ユーザー情報取得
    *
    * @param Request $request
    * @param string $uid
    * @return CreatedResponse
    */
    #[OpenApi\Operation(tags: ['user'], security: BearerTokenSecurityScheme::class)]
    #[OpenApi\Response(factory: UserResponse::class)]
    #[OpenApi\Response(factory: InternalServerErrorResponse::class)]
    public function user(Request $request, string $uid)
    {
        return $this->userService->getUser($request, $uid);
    }

    public function update(UpdateUserRequest $request, string $uid)
    {
        return $this->userService->updateUser($request, $uid);
    }

    public function delete(Request $request, string $uid)
    {
        return $this->userService->destroyUser($request, $uid);
    }
}

※OpenAPIのライブラリを使う際は、コメント部分も記述しないとエラーになるので注意して下さい。

 

次に以下のコマンドを実行し、OpenAPIのファイル「OpenApi/openapi.json」を出力します。

$ docker compose exec app php artisan openapi:generate > app/OpenApi/openapi.json

 

出力されたファイルの中身を確認したい場合は、テキストエディタにVSCodeを使っている場合は拡張機能「OpenAPI (Swagger) Editor」をインストールすると可能です。

 

拡張機能をインストール後、先ほど出力したファイル「OpenApi/openapi.json」をVSCodeでアクティブにし、Macの場合はショートカットキー「shift + option + p」を押すとプレビュー画面を開けるため、以下のように確認できればOKです。

 

次にGitHubで管理して確認できるようにするため、上記で作成したファイル「openapi.json」をマークダウン形式のファイルに変換させておきます。

ファイルの変換をするにはnodeをインストールしてnpmコマンドを使える必要がありますが、ここではインストール方法などは割愛します。(例えばvolta「curl https://get.volta.sh | bash」などを使って準備して下さい)

ではnpmコマンドの準備完了後、以下のコマンドを実行し、変換用のライブラリ「openapi-to-md」をインストールします。

$ npm i openapi-to-md

 

次に「package.json」に実行用のスクリプト「“openapi-to-md”: “openapi-to-md”」を追加します。

{
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "openapi-to-md": "openapi-to-md"
    },
    "devDependencies": {
        "axios": "^1.1.2",
        "laravel-vite-plugin": "^0.8.0",
        "vite": "^4.0.0"
    },
    "dependencies": {
        "openapi-to-md": "^1.0.24"
    }
}

 

次に以下のコマンドを実行し、openapi.jsonをopenapi.mdに変換します。

$ npm run openapi-to-md app/OpenApi/openapi.json app/OpenApi/openapi.md

 

これでAPI仕様書をGitHubで管理、確認が可能です。(GitHub上でopenapi.mdを確認すると以下のように表示されます。)

 



Laravel-Adminで管理画面を作成

次に管理画面を作成するため、ルートディレクトリ直下に戻り、以下のコマンドを実行してAPIの時と同様に新しいLaravelディレクトリ「admin」を作成します。

$ curl -s "https://laravel.build/admin?with=mysql"| bash

 

コマンド実行後、以下のように新しいディレクトリ「admin」と各種ファイルが作成されればOKです。

 

次に開発サーバーを起動させますが、管理画面についてはデフォルトのLaravel Sailを使います。

使うコマンドは「./vendor/bin/sail」ですが、sailだけで簡潔に書けるようにするため、以下のコマンドを実行してエイリアスを作成します。

$ echo 'alias sail="./vendor/bin/sail"' >> ~/.zshrc

 

コマンド実行後、ターミナルを再起動させ、以下のコマンドを実行して開発サーバーを起動させます。

$ cd admin
$ sail up -d

 

次にAPIの時と同様に、既存のマイグレーションファイルの削除、既存のUserモデルファイルの削除、タイムゾーン設定、DB(MySQL)のコレクション設定、Sanctumのアンインストールも行います。

 

次に以下のコマンドを実行し、管理画面用のライブラリ「encore/laravel-admin」をインストールします。

$ sail composer update
$ sail composer require encore/laravel-admin

 

次に以下のコマンドを実行し、関連ファイルを出します。

$ sail php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider"

 

次に以下のコマンドを実行し、laravel-adminをインストールします。

$ sail php artisan admin:install

 

次に以下のコマンドを実行し、Admin用のSeederファイルを作成します。

$ sail php artisan make:seeder AdminTablesSeeder

 

次に作成したファイルの中身は以下のように記述します。

<?php

namespace Database\Seeders;

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

// 追加
use Encore\Admin\Auth\Database\Administrator;
use Encore\Admin\Auth\Database\Role;
use Encore\Admin\Auth\Database\Permission;
use Encore\Admin\Auth\Database\Menu;
use Illuminate\Support\Facades\Hash;

class AdminTablesSeeder extends Seeder
{
    /**
    * Run the database seeds.
    */
    public function run(): void
    {
        // create a user.
        Administrator::truncate();
        Administrator::create([
            'username' => 'admin',
            'password' => Hash::make('admin'),
            'name' => 'Administrator',
        ]);

        // create a role.
        Role::truncate();
        Role::create([
            'name' => 'Administrator',
            'slug' => 'administrator',
        ]);

        // ロール追加
        Role::create([
            'name' => 'ユーザー管理者',
            'slug' => 'user.administrator',
        ]);

        // add role to user.
        Administrator::first()->roles()->save(Role::first());

        //create a permission
        Permission::truncate();
        Permission::insert([
            [
                'name' => 'All permission',
                'slug' => '*',
                'http_method' => '',
                'http_path' => '*',
            ],
            [
                'name' => 'Dashboard',
                'slug' => 'dashboard',
                'http_method' => 'GET',
                'http_path' => '/',
            ],
            [
                'name' => 'Login',
                'slug' => 'auth.login',
                'http_method' => '',
                'http_path' => "/auth/login\r\n/auth/logout",
            ],
            [
                'name' => 'User setting',
                'slug' => 'auth.setting',
                'http_method' => 'GET,PUT',
                'http_path' => '/auth/setting',
            ],
            [
                'name' => 'Auth management',
                'slug' => 'auth.management',
                'http_method' => '',
                'http_path' => "/auth/roles\r\n/auth/permissions\r\n/auth/menu\r\n/auth/logs",
            ],

            // パーミッション追加
            [
                'name' => 'ユーザー管理',
                'slug' => 'user.management',
                'http_method' => '',
                'http_path' => "/users*",
            ],
        ]);

        Role::first()->permissions()->save(Permission::first());

        // ロールにパーミンションを設定
        Role::where('slug', 'user.administrator')->first()->permissions()->save(Permission::where('slug', 'user.management')->first());
        Role::where('slug', 'user.administrator')->first()->permissions()->save(Permission::where('slug', 'dashboard')->first());
        Role::where('slug', 'user.administrator')->first()->permissions()->save(Permission::where('slug', 'auth.setting')->first());

        // add default menus.
        Menu::truncate();

        // 追加するメニューの修正
        Menu::insert([
            [
                'parent_id' => 0,
                'order' => 1,
                'title' => 'Dashboard',
                'icon' => 'fa-bar-chart',
                'uri' => '/',
            ],
            [
                'parent_id' => 0,
                'order' => 2,
                'title' => 'ユーザー管理',
                'icon' => 'fa-user',
                'uri' => '',
            ],
            [
                'parent_id' => 2,
                'order' => 3,
                'title' => 'ユーザー',
                'icon' => 'fa-users',
                'uri' => '/users',
            ],
            [
                'parent_id' => 0,
                'order' => 4,
                'title' => 'Admin',
                'icon' => 'fa-tasks',
                'uri' => '',
            ],
            [
                'parent_id' => 4,
                'order' => 5,
                'title' => 'Users',
                'icon' => 'fa-users',
                'uri' => 'auth/users',
            ],
            [
                'parent_id' => 4,
                'order' => 6,
                'title' => 'Roles',
                'icon' => 'fa-user',
                'uri' => 'auth/roles',
            ],
            [
                'parent_id' => 4,
                'order' => 7,
                'title' => 'Permission',
                'icon' => 'fa-ban',
                'uri' => 'auth/permissions',
            ],
            [
                'parent_id' => 4,
                'order' => 8,
                'title' => 'Menu',
                'icon' => 'fa-bars',
                'uri' => 'auth/menu',
            ],
            [
                'parent_id' => 4,
                'order' => 9,
                'title' => 'Operation log',
                'icon' => 'fa-history',
                'uri' => 'auth/logs',
            ],
        ]);

        // メニューに追加するロール設定を追加修正
        // add role to menu.
        Menu::where('title', 'Dashboard')->first()->roles()->save(Role::first());
        Menu::where('title', 'Admin')->first()->roles()->save(Role::first());
        Menu::where('title', 'ユーザー管理')->first()->roles()->save(Role::where('slug', 'user.administrator')->first());
    }
}

 

次に以下のコマンドを実行し、再マイグレーションとSeederの実行を行います。

$ sail php artisan migrate:refresh
$ sail php artisan db:seed --class=AdminTablesSeeder

 

次にブラウザで「http://localhost/admin/auth/login」にアクセスし、ログイン画面が表示されればOKです。

 

次にSeederファイルで作成したAdminユーザーのユーザーID「admin」、パスワード「admin」でログインし、以下のようなダッシュボード画面が表示されればOKです。

※もし本番環境にデプロイしてHTTPS接続する場合は、環境変数「ADMIN_HTTPS=true」を設定する必要があるので覚えておきましょう。

 

次にAPIで作成したUserのマイグレーションファイルとモデルファイルを、Adminディレクトリ内の対象の場所にそれぞれコピーして持ってきます。

その後、以下のコマンドを実行し、マイグレーションとコントローラーの作成を行います。

$ sail php artisan migrate
$ sail php artisan admin:make UserController --model=App\\Models\\User

 

次に作成したファイルの中身を以下のように修正します。

<?php

namespace App\Admin\Controllers;

use App\Models\User;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;

class UserController extends AdminController
{
    /**
    * Title for current resource.
    *
    * @var string
    */
    protected $title = 'ユーザー';

    /**
    * Make a grid builder.
    *
    * @return Grid
    */
    protected function grid()
    {
        $grid = new Grid(new User());

        // 論理削除データも表示
        $grid->model()->query()->withTrashed()->orderBy('id', 'desc');

        $grid->column('id', __('id'));
        $grid->column('uid', __('uid'));
        $grid->column('name', __('名前'));
        $grid->column('email', __('メールアドレス'));
        $grid->column('created_at', __('作成日時'));
        $grid->column('updated_at', __('更新日時'));
        $grid->column('deleted_at', __('削除日時'));

        // フィルタ設定
        $grid->filter(function($filter){
            $filter->equal('uid', 'uid');
            $filter->like('name', '名前');
            $filter->like('email', 'メールアドレス');
            $filter->between("created_at", '作成日')->datetime();
            $filter->between("updated_at", '更新日')->datetime();
            $filter->between("deleted_at", '削除日')->datetime();

            $filter->where(function ($query) {
                if ($this->input[0] == '1') {
                    $query->where('deleted_at', NULL);
                } elseif ($this->input[0] == '2') {
                    $query->where('deleted_at', '!=', NULL);
                }
            }, 'ステータス')->radio([
                '1' => 'アクティブ',
                '2' => '削除済'
            ]);

        });

        return $grid;
    }

    /**
    * Make a show builder.
    *
    * @param mixed $id
    * @return Show
    */
    protected function detail($id)
    {
        // 論理削除データも表示
        $show = new Show(User::withTrashed()->findOrFail($id));

        $show->field('id', __('id'));
        $show->field('uid', __('uid'));
        $show->field('name', __('名前'));
        $show->field('email', __('メールアドレス'));
        $show->field('created_at', __('作成日時'));
        $show->field('updated_at', __('更新日時'));
        $show->field('deleted_at', __('削除日時'));

        return $show;
    }

    /**
    * Make a form builder.
    *
    * @return Form
    */
    protected function form()
    {
        $form = new Form(new User());

        $form->text('uid', __('uid'));
        $form->text('name', __('名前'));
        $form->email('email', __('メールアドレス'));
        $form->datetime('deleted_at', __('削除日時'));

        return $form;
    }
}

 

次にAdmin側のルーティング設定に「$router->resource(‘/users’, UserController::class);」を追加します。

<?php

use Illuminate\Routing\Router;

Admin::routes();

Route::group([
    'prefix' => config('admin.route.prefix'),
    'namespace' => config('admin.route.namespace'),
    'middleware' => config('admin.route.middleware'),
    'as' => config('admin.route.prefix') . '.',
], function (Router $router) {

    $router->get('/', 'HomeController@index')->name('home');
    $router->resource('/users', UserController::class);
});

 

次に管理画面の左のメニュー「ユーザー管理>ユーザー」をクリックし、以下のようにユーザー画面が表示されればOKです。

※ユーザー管理メニューについては、Seederファイルで設定していますが、管理画面から手動でメニューの追加等も可能です。

 

カスタムフォームを使う場合

例えば管理画面からあるテーブルのデータを新規作成時、同時に別のテーブルのデータを一緒に作成したいというような場合は、カスタムフォームでオーバライドすれば可能です。

laravel-adminのフォームに関するファイルは「vendor/encore/laravel-admin/src/Form.php」にあるので、そこから関連のメソッドを参照してオーバーライドして下さい。

※コントローラーに以下のようなクラスでオーバーライドし、フォームの部分で使っている「Form」を「CustomForm」に置き換えれば使えます。

・・・

// カスタムフォーム
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Encore\Admin\Form\Builder;

class CustomForm extends Form
{
    // 新規作成処理後のリダイレクト処理のオーバーライド
    protected function redirectAfterStore()
    {
        // ここに行いたい処理を書く

        $resourcesPath = $this->resource(0);

        $key = $this->model->getKey();

        return $this->redirectAfterSaving($resourcesPath, $key);
    }

    // 更新処理後のオーバーライド
    public function update($id, $data = null)
    {
        // ここに行いたい処理を書く

        $data = ($data) ?: request()->all();

        $isEditable = $this->isEditable($data);

        if (($data = $this->handleColumnUpdates($id, $data)) instanceof Response) {
            return $data;
        }

        /* @var Model $this ->model */
        $builder = $this->model();

        if ($this->isSoftDeletes) {
            $builder = $builder->withTrashed();
        }

        $this->model = $builder->with($this->getRelations())->findOrFail($id);

        $this->setFieldOriginalValue();

        // Handle validation errors.
        if ($validationMessages = $this->validationMessages($data)) {
            if (!$isEditable) {
                return back()->withInput()->withErrors($validationMessages);
            }

            return response()->json(['errors' => Arr::dot($validationMessages->getMessages())], 422);
        }

        if (($response = $this->prepare($data)) instanceof Response) {
            return $response;
        }

        DB::transaction(function () {
            $updates = $this->prepareUpdate($this->updates);

            foreach ($updates as $column => $value) {
                /* @var Model $this ->model */
                $this->model->setAttribute($column, $value);
            }

            $this->model->save();

            $this->updateRelation($this->relations);
        });

        if (($result = $this->callSaved()) instanceof Response) {
            return $result;
        }

        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
            return $response;
        }

        return $this->redirectAfterUpdate($id);
    }

    // 更新処理後のリダイレクト処理のオーバーライド
    protected function redirectAfterUpdate($key)
    {
        // ここに行いたい処理を書く

        $resourcesPath = $this->resource(-1);

        return $this->redirectAfterSaving($resourcesPath, $key);
    }

    // 保存処理後のリダイレクト処理のオーバーライド
    protected function redirectAfterSaving($resourcesPath, $key)
    {
        // ここに行いたい処理を書く

        if (request('after-save') == 1) {
            // continue editing
            $url = rtrim($resourcesPath, '/')."/{$key}/edit";
        } elseif (request('after-save') == 2) {
            // continue creating
            $url = rtrim($resourcesPath, '/').'/create';
        } elseif (request('after-save') == 3) {
            // view resource
            $url = rtrim($resourcesPath, '/')."/{$key}";
        } else {
            $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
        }

        admin_toastr(trans('admin.save_succeeded'));

        return redirect($url);
    }

}

 



Laravel-Adminの管理画面をカスタマイズ

次はLaravel-Adminの管理画面を軽くカスタマイズしてみます。

まずは環境変数ファイル「.env」に画面表示切り替え用の環境変数「APP_DISPLAY_ENV=local」を追加し、コンフィグファイル「config/app.php」に「’display_env’ => env(‘APP_DISPLAY_ENV’, ‘production’),」を追加して環境変数の値を使えるようにします。

次にCSS関連のファイル「app/Admin/bootstrap.php」にカスタマイズ用のファイル読み込み設定と、ヘッダーの背景色のカスタマイズ設定を追加します。

・・・

Encore\Admin\Form::forget(['map', 'editor']);

// カスタマイズのため、「/resources/views/laravel-admin」に配置したファイルを読み取る設定
app('view')->prependNamespace('admin', resource_path('views/laravel-admin'));

// ヘッダーの背景色をカスタマイズ
use Encore\Admin\Admin;
$env = config('app.display_env');
    if ($env == 'local') {
        Admin::style('.skin-blue-light .main-header .logo {background-color: dimgray;}');
        Admin::style('.skin-blue-light .main-header .navbar {background-color: dimgray;}');
        Admin::style('.navbar-nav>.user-menu>.dropdown-menu>li.user-header {background-color: dimgray;}');
    } elseif ($env == 'development') {
        Admin::style('.skin-blue-light .main-header .logo {background-color: #009977;}');
        Admin::style('.skin-blue-light .main-header .navbar {background-color: #009977;}');
        Admin::style('.navbar-nav>.user-menu>.dropdown-menu>li.user-header {background-color: #009977;}');
    } elseif ($env == 'staging') {
        Admin::style('.skin-blue-light .main-header .logo {background-color: #B384FF;}');
        Admin::style('.skin-blue-light .main-header .navbar {background-color: #B384FF;}');
        Admin::style('.navbar-nav>.user-menu>.dropdown-menu>li.user-header {background-color: #B384FF;}');
    } elseif ($env == 'production') {
        Admin::style('.skin-blue-light .main-header .logo {background-color: #2C7CFF;}');
        Admin::style('.skin-blue-light .main-header .navbar {background-color: #2C7CFF;}');
        Admin::style('.navbar-nav>.user-menu>.dropdown-menu>li.user-header {background-color: #2C7CFF;}');
    }

 

次にダッシュボードページのコントローラーファイル「app/Admin/Controllers/HomeController.php」を以下のように修正します。

<?php

namespace App\Admin\Controllers;

use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\Dashboard;
use Encore\Admin\Layout\Column;
use Encore\Admin\Layout\Content;
use Encore\Admin\Layout\Row;
use Encore\Admin\Facades\Admin;

class HomeController extends Controller
{
    public function index(Content $content)
    {
        if (Admin::user()->isAdministrator()) {
            $res = $content
                       ->title('ダッシュボード')
                       ->row(Dashboard::title())
                       ->row(Dashboard::environment());
        } else {
            // Administrator以外はenvironmentを非表示
            $res = $content
                       ->title('ダッシュボード')
                       ->row(Dashboard::title());
        }

        return $res;
    }
}

 

次に以下のコマンドを実行し、ファイル格納用のディレクトリを作成します。

$ cd resources/views
$ mkdir laravel-admin
$ cd laravel-admin
$ mkdir partials
$ mkdir dashboard

 

次にディレクトリ「vendor/encore/laravel-admin/resources/views」内にlaravel-admin用の関連ファイルがあるので、そこからファイル「login.blade.php」、「partials/header.blade.php」、「dashboard/title.blade.php」をコピーし、作成したディレクトリ内にそれぞれ配置します。

そして、コピーして配置した各種ファイルを以下のようにそれぞれ修正します。

・・・

<div class="login-box">
  <div class="login-logo">
    <!-- タイトルロゴ修正 -->
    <!-- <a href="{{ admin_url('/') }}"><b>{{config('admin.name')}}</b></a> -->
    <a href="{{ admin_url('/') }}"><b>{{config('app.display_env')}}<br/>管理画面</b></a>
  </div>
  <!-- /.login-logo -->

・・・

 

<!-- Header Navbar -->
<nav class="navbar navbar-static-top" role="navigation">
    <!-- Sidebar toggle button-->
    <a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button">
        <span class="sr-only">Toggle navigation</span>
    </a>
    <ul class="nav navbar-nav hidden-sm visible-lg-block">
    {!! Admin::getNavbar()->render('left') !!}
    </ul>

    <!-- 中央にAPP_DISPLAY_ENVを表示 -->
    <div style="display: flex; justify-content: center; position:relative; top: 5px;">
        <div style="width: 100px; height: 0; font-size: x-large; font-weight: bold; color: white;">
            {!! config('app.display_env') !!}
        </div>
    </div>

・・・

 

<style>
    .title {
        font-size: 50px;
        color: #636b6f;
        font-family: 'Raleway', sans-serif;
        font-weight: 100;
        display: block;
        text-align: center;
        margin: 20px 0 10px 0px;
    }

    .links {
        text-align: center;
        margin-bottom: 20px;
    }

    .links > a {
        color: #636b6f;
        padding: 0 25px;
        font-size: 12px;
        font-weight: 600;
        letter-spacing: .1rem;
        text-decoration: none;
        text-transform: uppercase;
    }

    /* ダッシュボードメニュー用 */
    .main {
        width: 100%;
        margin-bottom: 20px;
    }

    .main .content {
        width: 80%;
        height: 90%;
        background-color: white;
    }

    .main .content h3 {
        margin-bottom: 30px;
    }

    .main .content ul {
        font-size: large;
    }

    .main .content li {
        margin-bottom: 20px;
    }

    .main .content .fa-angle-left:before {
        display: none;
    }

    .main .content li .treeview-menu {
        margin-top: 10px;
       margin-bottom: 20px;
    }

</style>

<div class="title">
    管理メニュー
</div>
<div class="main">
    <div class="content">
        <ul>
            @each('admin::partials.menu', Admin::menu(), 'item')
        </ul>
    </div>
</div>

 

上記の修正後、ログイン画面やダッシュボード画面を確認し、以下のように表示されれば簡単なカスタマイズは完了です。

※管理メニューの一覧は、画面左のメニューからコピーしたものなので、ユーザーの権限次第で表示・非表示の制御がされています。

 



最後に

今回はLaravelでバックエンドAPIを開発する方法についてまとめました。

今回ご紹介した内容は実際の実務でも役立つ内容になっているので、現在Web系エンジニアを目指して頑張っている方や、これから目指そうと思って学習しようと考えている方はぜひ参考にプログラミングをしてみて下さい!

 

各種SNSなど

各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします!

 

The following two tabs change content below.

Tomoyuki

SEを5年経験後、全くの未経験ながら思い切ってブロガーに転身し、月間13万PVを達成。その後コロナの影響も受け、以前から興味があったWeb系エンジニアへのキャリアチェンジを決意。現在はWeb系エンジニアとして働きながら、プロゲーマーとしても活躍できるように活動中。








シェアはこちらから


おすすめのゲーミングPCまとめ

近年、eスポーツという言葉が浸透し、ゲーミングPCの需要も増えましたが、「ゲーミングPCはどう選べばいいの?」という人も多いのではないでしょうか。そんなPC初心者でも迷わず選べるよう、おすすめのゲーミングPCをご紹介します!


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です