自分流Laravelディレクトリちら見せ

いつも通りTwitterでLaravelの話をしていたところ、

ということがあり、とりあえず自分流のLaravelを紹介します。
2014年にその時の自分の開発プロジェクトなどで利用していたものをまとめました。

blog.comnect.jp.net

新原さんのエントリも参考にしてみると良いでしょう。 www.1x1.jp

Laravelは開発者が自由に組み合わせて、自由に構築できるフレームワークという側面があり、
ある程度慣れてきた頃に、みなさん色々試行錯誤すると思います。

RoRっぽさを求めている方はそれの色を求め、
Javaや、エンタープライズ向けのものを求めている方はその色になっていくと思います。

あれから月日が流れ、折角ですので2016年版として自分流のLaravelを紹介します。
*2014年からだいぶ形を変えています。

「なぜそのディレクトリ構造にしているのか」は、アーキテクチャを交えて紹介していきます。

ディレクトリ構造

f:id:ytakezawa:20160505010853p:plain

appディレクトリ配下は画像のようになっています。
デフォルトのディレクトリ以外を紹介します。

Annotationディレクト

Laravel-Aspectパッケージを利用している為、プロジェクトで利用する追加アノテーションを設置しています。

Aspect

プロジェクトで利用するAspectの設置場所です。
Interceptorディレクトリは、Laravel-Aspectで提供しているもの以外の処理で、
横断で実行されるものが設置されます。

PointCutディレクトリは、上記で追加した横断処理の発生条件クラスを設置しています(ポイントカット)

Domain

所謂ドメインモデルのディレクトリです。
Entity、Repositoryがあり、ドメインサービスと、
ドメインを1パッケージと扱う様に内部にサービスプロバイダがあります。
アプリケーションの規模によっては、
この部分だけを本当に独立したコンポーネントとして別リポジトリにすることも多々有ります。

Http

特に大きく手を入れていませんが、
laravelcollectiveのAnnotationを利用しているため、routes.phpはありません。

Modules

Laravel-Aspectパッケージで利用する、各アスペクトを適用するクラスを登録するmoduleクラスがあります。

Providers

routes.phpを利用しない為、RouteServiceProviderクラス自体も削除しています

Services

アプリケーションサービスが設置されます。

Facade削除

自分流のなかで、特徴の一つでもあるのがFacadeの削除です。

Facade削除についてはLaravelリファレンス発売時のアドベントカレンダーでも記述しています。

ytake.hateblo.jp

config/app.phpのaliasesキーの配列をバッサリ削除(またはコメントアウト)しています。

    'aliases' => [
        // Facadeは利用しない為エイリアス削除
    ],

さらに、コンテナへのアクセサ登録処理もバッサリ外しています。

この処理はapp/Http/Kernel.phpと、app/Console/Kernel.phpにありますので、
次のようにbootstrappersプロパティを上書きしています。

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

/**
 * Class Kernel
 */
class Kernel extends HttpKernel
{
    /**
     * アプリケーションの初期段階で解決されるサービスを変更しています
     * Facadeの登録はこの時点で行われるため、初期実行で実行されないように外しています
     *
     * fluentdを利用することが多いため、パッケージを利用してロガーを拡張しています
     * @see Ytake\LaravelFluent\ConfigureLogging
     * @var string[]
     */
    protected $bootstrappers = [
        'Illuminate\Foundation\Bootstrap\DetectEnvironment',
        'Illuminate\Foundation\Bootstrap\LoadConfiguration',
        'Ytake\LaravelFluent\ConfigureLogging',
        'Illuminate\Foundation\Bootstrap\HandleExceptions',
        'Illuminate\Foundation\Bootstrap\RegisterProviders',
        'Illuminate\Foundation\Bootstrap\BootProviders',
    ];

本来であれば、ここにIlluminate\Foundation\Bootstrap\RegisterFacadesが記述されていますが、
丸ごと削除です。
ついでにfluentdを利用するので、config/app.phpのlogをfkuentdに変更したりする為に、
ログを拡張したパッケージに差し替えています。

.envファイルだけで辛い場合は、Illuminate\Foundation\Bootstrap\LoadConfigurationクラスも、
拡張したものに差し変える場合もあります。

ドメイン

EntityとRepositoryを利用していますので、簡単な例だと次の通りです。

User

<?php
declare (strict_types = 1);

namespace App\Domain\UserRegistration\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Class User
 *
 * @ORM\Entity
 * @ORM\Table(name="users")
 * @ORM\HasLifecycleCallbacks
 */
class User
{
    /**
     * @var int
     * @ORM\Column(name="user_id",type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $userId;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $name;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $email;

    /**
     * @var string
     * @ORM\Column(name="remember_token",type="string")
     */
    protected $rememberToken;

    /**
     * created Time/Date
     *
     * @var int
     * @ORM\Column(name="created_at",type="datetime")
     */
    protected $createdAt;

    /**
     * User constructor.
     *
     * @param string $name
     * @param string $email
     */
    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    /**
     * @return int
     */
    public function getId() : int
    {
        return $this->userId;
    }

    /**
     * @return string
     */
    public function getName() : string
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getEmail() : string
    {
        return $this->email;
    }

    /**
     * @return string
     */
    public function getRememberToken()
    {
        return $this->rememberToken;
    }

    /**
     * @return int
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * @param string $token
     */
    public function setToken(string $token)
    {
        $this->rememberToken = $token;
    }

    /**
     * @ORM\PrePersist
     */
    public function setCreatedAt()
    {
        $this->createdAt = new \DateTime("now");
    }
}

Repository

リポジトリはインターフェース経由で主にドメインサービスクラスから利用されますが、だいたい下記の様になっています。
Aspectパッケージの@Cachebaleを使って、データ取得時に自動で値がキャッシュされる様にしています。

<?php

namespace App\Domain\UserRegistration\Repository;

use Doctrine\ORM\EntityManagerInterface;
use App\Domain\UserRegistration\Entity\User;
use Ytake\LaravelAspect\Annotation\Cacheable;

/**
 * Class UserRepository
 */
class UserRepository implements UserRepositoryInterface
{
    /** @var EntityManagerInterface */
    protected $entityManager;

    /**
     * UserRepository constructor.
     *
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    /**
     * @Cacheable(cacheName="user:find",key={"#userId"})
     * @param int $userId
     *
     * @return mixed
     */
    public function find(int $userId)
    {
        return $this->entityManager->find(User::class, $userId);
    }

    /**
     * @param User $user
     *
     * @return int
     */
    public function add(User $user) : int
    {
        $this->entityManager->persist($user);
        $this->entityManager->flush();

        return $user->getId();
    }
}

ドメイン配下のサービスクラスは特になにもありません。
場合によってはDomain配下にサービスを置かずにapp/Services配下におくこともあります。
規模によって変わります。

アプリケーションサービス

主にドメインサービスなどのクライアントになりますが、
厳密にそこまで分けない場合もあります。
例としては次の通りです。

<?php
declare (strict_types = 1);

namespace App\Services;

use Illuminate\Contracts\Mail\Mailer;
use App\Domain\UserRegistration\Entity\User;
use App\Domain\UserRegistration\UserRegistrationInterface;
use Ytake\LaravelAspect\Annotation\Loggable;
use Ytake\LaravelAspect\Annotation\Transactional;

/**
 * Class UserRegister
 */
class UserRegister
{
    /** @var UserRegistrationInterface */
    protected $domain;

    /** @var Mailer  */
    protected $mailer;

    /**
     * UserRegister constructor.
     *
     * @param UserRegistrationInterface $domain
     * @param Mailer                    $mailer
     */
    public function __construct(UserRegistrationInterface $domain, Mailer $mailer)
    {
        $this->domain = $domain;
        $this->mailer = $mailer;
    }

    /**
     * @Loggable(skipResult=true)
     * @Transactional
     *
     * @param string $name
     * @param string $email
     * @return void
     */
    public function register(string $name, string $email)
    {
        if ($this->domain->register(new User($name, $email))) {
            $this->mailer->send('mail.template', [], function () {
                // for mail
            });
        }
    }
}

ここではトランザクションや、必要であればロギングなどを利用することが多いですので、
ここでもまたAspectパッケージの@Transactonalや、@Loggableを利用して、
横断処理を実行しています。
特別な処理や要件がなければトランザクションなどは直接記述することはほとんどありません。

コントローラ

laravelcollectiveのAnnotationを利用して、ルートやミドルウェアを記述しています。
その他、バリデーションをする為だけにFromRequestをタイプヒンティングするのも嫌なので、
Aspectにバリデーションを行う@Validを追加して実行しています。

<?php

namespace App\Http\Controllers\User;

use App\Annotation\Valid;
use App\Services\UserRegister;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

/**
 * Class RegistrationController
 * @Middleware("web")
 */
class RegistrationController extends Controller
{
    /**
     * @Get("/user/form", as="user.form")
     */
    public function form()
    {
        return view('user.form');
    }

    /**
     * @Post("/user/confirm", as="user.confirm")
     * @Valid(
     *     request=\App\Http\Requests\User\RegistrationRequest::class
     * )
     */
    public function confirm()
    {
        return view('user.confirm');
    }

    /**
     * @Post("/user/apply", as="user.apply")
     * @param Request   $request
     * @param UserRegister $service
     *
     * @return mixed
     */
    public function apply(Request $request, UserRegister $service)
    {
        if ($request->get('_return')) {
            return redirect()->route('user.form')->withInput();
        }
        $service->register($request->get('name'), $request->get('email'));
        // to redirect...
    }
}

ぱっと見、ネット上でよく見かける所謂Modelクラス、Controllerクラス、Viewと分けていない為、 取っ付き辛いかもしれませんが、それぞれの関心ごとを分離するように分けています。

AspectやDoctrineでデフォルトの状態から大きく使い勝手も変えています。
もちろんPresenterなどのディレクトリもこれに追加されます。

みなさんも自分のスタイルや開発チームに合わせて変更して、自分流のLaravelをシェアしてください!

サンプルリポジトリはこちら github.com