every Tech Blog

株式会社エブリーのTech Blogです。

初めてLaravelを導入したバックエンド開発の構成紹介

こんにちは。RH開発部RHRAグループの池です。

2024年6月にエブリーは5つの小売アプリの運営について事業譲渡を受け、『 retail HUB 』へ移管しました。

引き継いだシステムのバックエンドはLaravelを用いて構築されていましたが、Laravelは弊社では初めて扱う技術スタックでした。そのため、チーム全体でLaravelの知見を深めながら、運用保守および開発を進めています。

このような状況の中、新規サーバーを構築する機会があり、Laravelの知見をチームで蓄積することも目的の一つとして、新規サーバーの開発においてLaravelを採用しました。

本記事では、弊社が初めてLaravelを導入した新規サーバーの構成についてご紹介させていただきます。

システム概要

まず最初に前提ですが、新規サーバーの開発にあたり、以下の条件を考慮して設計開発を進めています。

スピード重視の開発

  • リリース優先でまずは必要最小限の機能を実装
  • 開発効率を重視した技術選定
  • 段階的な改善を許容する設計

チームの技術背景

  • Laravelはチームが初めて扱う技術スタック
  • チーム全体で学習しながらの開発

将来を見据えた設計

  • マルチテナント対応を考慮
  • 段階的な機能拡張が可能な構造

このような方針をもとに、最初から作り込んだ設計を目指すのではなく、スピードを優先しつつも実用的な設計を考慮しながら挑戦と学びのある開発アプローチをバランスをとって選択しています。

全体像

今回開発している新規サーバーでは、モバイル向けAPIと管理画面向けAPIの2つのAPIを提供しており、これらAPIは共通のデータベースを使用しています。 これらを効率的に管理すべくモノレポ構成で開発を行っています。 構成の簡易図は以下の通りです。

技術スタック

こちらは紹介までになりますが、Laravel関連で採用している技術スタックは以下の通りです。

ほとんどが弊社として初めて扱うものであり、チーム全体で学習・議論しながら取り組んでいます。

  • PHP 8.3(8.4へアップグレード予定)
  • Laravel 11
  • Laravel Octane & Swoole
  • Laravel Sanctum
  • Pest
  • Larastan
  • Laravel Pint

ディレクトリ構成

リポジトリ全体のディレクトリ構造は、以下の通りです。

.
├── .github/            # GitHub Actionsの設定(パイプラインの共通化)
├── dashboard-api/      # 管理画面向け API プロジェクト
│   ├── Dockerfile      # 管理画面向け Dockerイメージ定義
│   ├── app             # 管理画面向け API 固有コード
│   │   ├── Exceptions
│   │   ├── Helpers
│   │   ├── Http
│   │   ├── Providers
│   │   ├── Repositories
│   │   │   ├── Interfaces  # リポジトリのインターフェース
│   │   └── Services
│   │       ├── Interfaces  # サービスのインターフェース
│   ├ ...
│   ├── compose.yaml    # ローカル開発環境の設定
│   ├── composer.json   # 共通パッケージをimport
│   ├── ecspresso       # 管理画面向け デプロイ設定
│   ├── tests           # 管理画面向け API 固有のテストコード
│   ├ ...
│
├── mobile-api/         # モバイル向け API プロジェクト
│   ├── Dockerfile      # モバイル向け Dockerイメージ定義
│   ├── app             # モバイル向け API 固有コード
│   │   ├── Exceptions
│   │   ├── Helpers
│   │   ├── Http
│   │   ├── Providers
│   │   ├── Repositories
│   │   │   ├── Interfaces  # リポジトリのインターフェース
│   │   └── Services
│   │       ├── Interfaces  # サービスのインターフェース
│   ├ ...
│   ├── compose.yaml    # ローカル開発環境の設定
│   ├── composer.json   # 共通パッケージをimport
│   ├── ecspresso       # モバイル向け デプロイ設定
│   ├── tests           # モバイル向け API 固有のテストコード
│   ├ ...
│
├── packages
│   └── common/         # 共通パッケージ(各 API プロジェクトで再利用)
│       ├── composer.json
│       └── src
│           ├── Models
│           ├── Providers
│           ├── Services
│           ├── Repositories
│           ├── databases       # データベース関連は全て共通化
│           │   ├── factories
│           │   ├── migrations
│           │   └── seeders
│           └── tests
│
├── phpstan.neon      # PHPStanの共通設定
├── pint.json         # Laravel Pintの共通設定  

プロジェクト共通コードはpackages/common/に配置された共通パッケージで管理します。データベースモデルやマイグレーション、ビジネスロジックなど、両APIで共有する機能を集約します。各APIプロジェクトからはComposerを通してこの共通パッケージをインポートして共通コードを利用する形となります。

また、管理画面向けAPI(dashboard-api/)とモバイルアプリ向けAPI(mobile-api/)配下では、それぞれのプロジェクトに応じた固有のロジック、テストコードや設定ファイル、デプロイ構成などを個別に管理しています。

このように、共通機能と個別機能を分離しながら開発を行なっています。加えて、CI/CD 用のワークフローも共通で管理します。

アーキテクチャ設計

私たちのシステムは、SaaSとしてマルチテナントでの運用を想定しており、テナントごとに異なるビジネスロジックやデータアクセスを柔軟に切り替えられるようなアーキテクチャを検討しました。

あまり特別なことはしてないですが、レイヤードアーキテクチャ+DIP(依存性の逆転) の形を取りつつ、マルチテナント対応のために ServiceInterface と RepositoryInterface を導入しています。

モノレポ構成

私たちのチームでは、主に以下の理由でモノレポの構成を採用しました。

  • 各APIプロジェクトが同じドメインで共通化できる要素が多い
    • データベースマイグレーション、モデル定義
    • ビジネスロジック、ユーティリティ
    • linter設定
    • CI/CDパイプライン
  • 全員が複数プロジェクトを横断して開発する小規模なチーム体制との親和性あり
    • 各プロジェクト横断的な変更がしやすい、影響範囲を把握しやすい など

続いて、Laravelにおけるモノレポ設定方法とInterfaceのDIについて実例を紹介します。

モノレポ設定方法とDIの実例紹介

Laravel プロジェクトにおいて共通パッケージを利用する際の主な方法は、Composer のrepositoriesを用いる方法です。具体的には、各 API プロジェクトの composer.json に共通パッケージのリポジトリ定義を追加し、依存関係として設定します。

1. Composer のリポジトリ定義

例として、mobile-api/composer.json の一部は以下のようになります。

{
    "require": {
        "sample/common": "dev-main"
    },
    "repositories": [
        {
            "type": "path",
            "url": "../packages/common"
        }
    ]
}

同様の設定を dashboard-api/composer.json にも記載することで、両サービスで共通パッケージを最新コードとして取り込むことが可能となります。

2. オートロード設定

共通パッケージ内のクラスは PSR-4 に従った名前空間の設定を行うことで、Laravel のオートローダーにより自動的に読み込まれます。これにより、サービス内で自然な形で共通機能が利用できる状態となります。

{
    "name": "sample/common",
    "autoload": {
        "psr-4": {
            "Sample\\Common\\": "src/"
        }
    },
    ...
    "extra": {
        "laravel": {
            "providers": [
                "Sample\\Common\\Providers\\CommonServiceProvider"
            ]
        }
    }
}

また、extra.laravel.providersにサービスプロバイダーが指定することで、共通パッケージの初期化やサービス登録が自動的に行われます。 指定したCommonServiceProviderではloadMigrationsFromメソッドを呼び出し、共通パッケージ内のマイグレーションファイルを読み込むようにします。 そうすることで、mobile-apidashboard-apiのプロジェクトからマイグレーションを実行する際に、共通パッケージ内のマイグレーションファイルも読み込まれるようになります。

<?php
    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../databases/migrations');
    }
?>

3. 共通パッケージの利用例

例えば、共通パッケージ内に用意されたShopモデルを、dashboard-apiプロジェクトで利用する場合、以下のように記述します。

<?php

use DashboardApi\Repositories\Interfaces\ShopRepositoryInterface;
use Sample\Common\Models\Shop;

class ShopRepository implements ShopRepositoryInterface
{
    private Shop $shop;

    public function __construct(Shop $shop)
    {
        $this->shop = $shop;
    }

    public function findShopById(int $id): Shop
    {
        return $this->shop->find($id);
    }
?>

4. InterfaceのDI

インターフェースと実装の紐付けは、Laravel のサービスコンテナを活用して行っています。これにより、テナントごとに異なる実装を柔軟に切り替えることが可能です。

<?php

namespace DashboardApi\Providers;

use Illuminate\Support\ServiceProvider;
use Sample\Common\Services\Interfaces\ArticleServiceInterface;
use Sample\Common\Repositories\Interfaces\ArticleRepositoryInterface;
use DashboardApi\Services\ShopService;
use DashboardApi\Repositories\ShopRepository;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Service層のDI設定
        $this->app->bind(ShopServiceInterface::class, ShopService::class);
        
        // Repository層のDI設定
        $this->app->bind(ShopRepositoryInterface::class, ShopRepository::class);
    }
}
?>

将来的にはテナントごとにDIを切り替えることで、テナントごとに異なるビジネスロジックを持たせるような想定をしています。

<?php

public function register(): void
{
    $this->app->bind(ShopServiceInterface::class, function ($app) {
        // テナントに応じて実装を切り替え
        return match ($tenant) {
            'tenant_a' => new TenantAShopService(
                $app->make(ShopRepositoryInterface::class)
            ),
            'tenant_b' => new TenantBShopService(
                $app->make(ShopRepositoryInterface::class)
            ),
            default => new ShopService(
                $app->make(ShopRepositoryInterface::class)
            ),
        };
    });
}
?>

現状の課題と向き合い方

現在、開発を始めて間もない段階ですが、いくつかの課題が見えてきています。

当初は管理画面APIとモバイルアプリAPIで多くのビジネスロジックを共通化できると考えていましたが、実際には想定より共通化できる範囲が限定的でした。 現時点では主にデータベースのモデル定義とマイグレーションの共通化に留まっており、より効果的なロジックの共通化方法を模索しています。

細かいですが、開発環境については、現在各APIプロジェクトで個別に環境を立ち上げる必要があり、一つのdocker composeで統合的に管理できる環境の整備を検討しています。

チーム開発での課題については、スピード重視の開発という前提において、挑戦と学習、および品質との丁度良いバランスについて日々議論しています。

例えば、

  • 少人数チームで初期開発フェーズにおいてテストコードの適切な粒度
  • クラス設計の責務分担
  • 必要十分なドキュメント整備の範囲
  • フローを固めすぎない開発プロセス

など、少人数チームならではの密なコミュニケーションを取りながら方針を固めています。

まとめ

今回は私たちが初めて取り組むLaravelでのバックエンド開発について、開発方針を踏まえた構成のアプローチをご紹介させていただきました。

最後に、本記事が同じようにLaravelでの開発を検討されている方々の参考になれば幸いです。

また、私たちは常に新しい技術的チャレンジに取り組める仲間を募集しています。私自身も新しい技術にチームで試行錯誤しながら取り組める環境で、日々成長を感じています。少しでも面白そうと感じていただけた方はぜひお気軽にお声がけください!

参考記事