Open11

【開発ノート】画像素材ストッカー開発

ぬぬぬぬ

開発目的

web制作で使用する画像素材をチームで保管・共有するため

ぬぬぬぬ

メモ

Laravel Sailで作成してプロジェクトをgithub経由でcloneしたらsailコマンドが使えなかった

原因
必要なパッケージをインストールしていないから
そのためのcomposerもインストールしていない

解決方法
下記のコマンドでcomposerをインストールして各種パッケージをインストールする

docker run --rm \
    -u "$(id -u):$(id -g)" \
    -v $(pwd):/var/www/html \
    -w /var/www/html \
    composer install
ぬぬぬぬ

開発

各種テーブルとモデル作成、リレーションシップ設定

ぬぬぬぬ

Googleドライブへの接続

  1. google cloude consoleでGoogle Drive APIを使えるように設定
    参考:
    https://qiita.com/doran/items/15b2c59adb410ddeeb8a

  2. パッケージのインストール

# 必要なパッケージのインストール
composer require google/apiclient
composer require google/cloud-storage
  1. 認証ファイルの配置
mkdir -p config/google
mv service-account.json config/google/service-account.json
mv oauth-credentials.json config/google/oauth-credentials.json
  1. gitignoreの設定
/config/google/*.json
!/config/google/.gitkeep
  1. サービスクラスの実装

  2. カスタム例外クラスの作成

  3. サービスプロバイダーの登録

php artisan make:provider GoogleServiceProvider
  1. 設定ファイルの更新(config/services.php)
return [
    // 他の設定...
    
    'google' => [
        'auth_type' => env('GOOGLE_DRIVE_AUTH_TYPE', 'service-account'),
        'folder_id' => env('GOOGLE_DRIVE_FOLDER_ID'),
        'credentials_path' => env('GOOGLE_DRIVE_CREDENTIALS_PATH', base_path('config/google/service-account.json')),
    ],
];
  1. 環境変数の設定
GOOGLE_DRIVE_FOLDER_ID=your-folder-id
GOOGLE_DRIVE_AUTH_TYPE=service-account

GOOGLE_DRIVE_FOLDER_IDはgoogle driveで作成したフォルダにアクセルしURLを確認する
例:
https://drive.google.com/drive/folders/1aB2cD3eF4gH5iJ6kL7mN8...
この1aB2cD3eF4gH5iJ6kL7mN8...部分がGOOGLE_DRIVE_FOLDER_ID

  1. コントローラーで処理を実装

  2. viewとrouteの設定

ぬぬぬぬ

Google Cloud Consoleでの設定手順

a. プロジェクトの作成:

Google Cloud Console にアクセス
新しいプロジェクトを作成

b. APIの有効化:

左メニューから「APIとサービス」→「ライブラリ」
検索で "Google Drive API" を探す
「有効にする」をクリック

c. サービスアカウントの作成:

左メニューから「APIとサービス」→「認証情報」
「認証情報を作成」→「サービスアカウント」を選択
必要事項を入力:

サービスアカウント名(例:my-app-drive-access)
説明(任意)

「作成」をクリック

d. キーの作成:

作成したサービスアカウントをクリック
「キー」タブを選択
「鍵を追加」→「新しい鍵を作成」
JSONを選択
キーがダウンロードされます

e. 権限の設定:

Google Driveで使用するフォルダを開く
フォルダを右クリック→「共有」
サービスアカウントのメールアドレスを追加

メールアドレスは [サービスアカウント名]@[プロジェクトID].iam.gserviceaccount.com
※Google Cloud Console で確認できる
Google Cloud Console にアクセス
左メニューから「IAM と管理」→「サービスアカウント」を選択
サービスアカウントの一覧が表示され、そこにメールアドレスが表示されている

編集者権限を付与

ぬぬぬぬ

開発

google driveへのアップロードロジックをサービスクラスとして作成

initialize() メソッド

Google Clientを設定し、認証タイプに応じてサービスアカウントまたはOAuthを初期化

initializeServiceAccount() メソッド

サービスアカウントを使用してGoogle Clientを設定
認証ファイルが存在しない場合は例外をスロー。

initializeOAuth() メソッド

OAuthを使用してGoogle Clientを設定

uploadFile() メソッド

指定されたファイルをGoogle Driveにアップロード
アップロードが成功したらfile_id view_link nameを返却する

ぬぬぬぬ

開発

アップロードに成功したらファイル情報をDBに登録

  public function fileUpload($file)
  {
    try {
      $result = $this->driveService->uploadFile($file);

      Image::create([
        'major_category_id' => 1,
        'drive_file_id' => $result['file_id'],
        'drive_view_link' => $result['web_view_link'],
        'drive_download_link' => $result['web_content_link'],
        'title' => $result['name'],
        'user_name' => 'test',
        'mime_type' => $result['mimeType'],
        'file_size' => $result['size'],
        'discription' => $result['description'],
        'thumbnail_link' => $result['thumbnail_link']
      ]);

      return [
        'success' => true,
        'file_id' => $result['file_id'],
        'file_url' => $result['web_view_link'],
        'file_name' => $result['name'],
        'file_type' => $result['mimeType'],
        'file_size' => $result['size'],
        'created_time' => $result['created_time'],
        'modified_time' => $result['modified_time'],
        'description' => $result['description'],
        'web_content_link' => $result['web_content_link'],
        'thumbnail_link' => $result['thumbnail_link']
      ];
    } catch (GoogleDriveException $e) {
      return [
        'success' => false,
        'error' => 'アップロードに失敗しました: ' . $e->getMessage()
      ];
    }
  }
ぬぬぬぬ

試験実装

DBからファイル情報を取得してviewで表示

// namespace App\Http\Controllers
class HomeController extends Controller
{
    public function index()
    {
        $files = Image::with('majorCategory')->get()->map(function ($image) {
            $image->file_size = number_format($image->file_size / 1024, 1);
            $image->major_category = $image->majorCategory->name;
            return $image;
        })->sortByDesc('created_at');
        return view('index', compact('files'));
    }
}

// route/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');

メモ

以下のカラムを取得するときにアクセサで自動処理をしておく

  • mime_type → 拡張子の文字列だけ返却
  • file_size → KBの小数点第一位までを返却
// namespace App\Models;
class Image extends Model
{
    ...

    public function getMimeTypeAttribute($value)
    {
        return explode('/', $value)[1];
    }

    public function getFileSizeAttribute($value)
    {
        return number_format($value / 1024, 1);
    }
}
ぬぬぬぬ

開発

google認証の実装

  1. socialiteのインストール
composer require laravel/socialite
  1. 各ファイルの作成
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class AllowedEmail extends Model
{
    protected $fillable = ['email'];
    
    public static function isAllowed($email)
    {
        return static::where('email', $email)->exists();
    }
}

// config/services.php
return [
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_REDIRECT_URI'),
    ],
];

// routes/web.php
use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Auth\GoogleController;

Route::get('auth/google', [GoogleController::class, 'redirectToGoogle'])->name('auth.google');
Route::get('auth/google/callback', [GoogleController::class, 'handleGoogleCallback']);

// App/Http/Controllers/Auth/GoogleController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\AllowedEmail;
use Laravel\Socialite\Facades\Socialite;
use Exception;
use Illuminate\Support\Facades\Auth;

class GoogleController extends Controller
{
    public function redirectToGoogle()
    {
        return Socialite::driver('google')->redirect();
    }

    public function handleGoogleCallback()
    {
        try {
            $googleUser = Socialite::driver('google')->user();
            
            // メールアドレスが許可リストにあるか確認
            if (!AllowedEmail::isAllowed($googleUser->email)) {
                return redirect()->route('login')
                    ->with('error', 'このメールアドレスではログインできません。');
            }

            // ユーザーが存在するか確認し、なければ作成
            $user = User::where('google_id', $googleUser->id)->first();
            
            if (!$user) {
                $user = User::create([
                    'name' => $googleUser->name,
                    'email' => $googleUser->email,
                    'google_id' => $googleUser->id,
                    'password' => encrypt(rand(1,10000)), // ダミーパスワード
                ]);
            }

            Auth::login($user);
            return redirect()->intended('dashboard');

        } catch (Exception $e) {
            return redirect()->route('login')
                ->with('error', '認証中にエラーが発生しました。');
        }
    }
}

// database/migrations/xxxx_xx_xx_create_allowed_emails_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAllowedEmailsTable extends Migration
{
    public function up()
    {
        Schema::create('allowed_emails', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

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

// database/migrations/xxxx_xx_xx_add_google_id_to_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddGoogleIdToUsersTable extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('google_id')->nullable()->after('id');
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('google_id');
        });
    }
}
  1. middlewareの作成
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;

class CheckAuth
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (!Auth::check()) {
            // APIリクエストの場合は401レスポンス
            if ($request->expectsJson()) {
                return response()->json([
                    'message' => '認証が必要です。',
                ], 401);
            }

            // 現在のURLを保存してリダイレクト
            return redirect()->route('login')
                ->with('error', 'ログインが必要です。')
                ->with('redirect', $request->fullUrl());
        }

        // セッションの有効期限を延長
        if ($request->session()->has('auth')) {
            $request->session()->regenerate();
        }


        return $next($request);
    }
}

\bootstrap\app.phpにエイリアスを登録

<?php

use App\Http\Middleware\CheckAuth;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'auth' => CheckAuth::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();