ytake Hatena

Web Application Developer

Lumenで実装するAPI REST拡張HATEOAS

API実装してますか?

弊社では最近APIにLumen(PHP), Zend Expressive(PHP), echo(Go)が利用されています。
そのなかでも徐々にAPIを表題のHATEOASへとシフトしつつあります。

HATEOASに関しては下記を参照ください postd.cc

そのHATEOASですが、Lumenを使って実装する簡単な例を紹介します。

記事を返却するAPI

jsonで返却するAPIの実装をしてみましょう。

Article Entity

<?php

namespace App\Domain\Entity;

/**
 * Class Article
 */
final class Article
{
    /** @var int */
    private $id;

    /** @var string */
    private $title;

    /** @var Comment[] */
    private $comments;

    /**
     * Article constructor.
     *
     * @param int    $id
     * @param string $title
     */
    public function __construct(int $id, string $title)
    {
        $this->id = $id;
        $this->title = $title;
    }

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

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

    /**
     * @param Comment $comment
     */
    public function setComment(Comment $comment)
    {
        $this->comments[] = $comment;
    }

    /**
     * @return Comment[]
     */
    public function getComments() : array
    {
        return $this->comments;
    }
}

Comment Entity

<?php

namespace App\Domain\Entity;

/**
 * Class Comment
 */
final class Comment 
{
    /** @var int */
    private $id;

    /** @var string */
    private $comment;

    /**
     * Article constructor.
     *
     * @param int    $id
     * @param string $comment
     */
    public function __construct(int $id, string $comment)
    {
        $this->id = $id;
        $this->comment = $comment;
    }

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

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

Repository

データベースなどを使わないシンプルな例です。

<?php

namespace App\Domain\Repository;

use App\Domain\Entity\Article;
use App\Domain\Entity\Comment;

/**
 * Class ArticleRepository
 */
class ArticleRepository
{
    /** @var Article */
    protected $article;

    /**
     * @return ArticleRepository
     */
    public function findOne() : ArticleRepository
    {
        $article = new Article(1, 'testing');
        $article->setComment(new Comment(1, 'comment 1'));
        $article->setComment(new Comment(2, 'comment 2'));

        $this->article = $article;

        return $this;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        if (!$this->article instanceof Article) {
            return [];
        }

        return [
            'id'       => $this->article->getId(),
            'title'    => $this->article->getTitle(),
            'comments' => array_map(function (Comment $comment) {
                return [
                    'id'      => $comment->getId(),
                    'comment' => $comment->getComment(),
                ];
            }, $this->article->getComments()),
        ];
    }
}

Service

<?php

namespace App\Domain\Services;

use App\Domain\Repository\ArticleRepository;

/**
 * Class ArticleReader
 */
class ArticleReader
{
    /** @var ArticleRepository */
    protected $repository;

    /**
     * ArticleReader constructor.
     *
     * @param ArticleRepository $repository
     */
    public function __construct(ArticleRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * @return array
     */
    public function readOne() : array
    {
        return $this->repository->findOne()->toArray();
    }
}

Controller

jsonでレスポンスを返却する簡単な例です

<?php

namespace App\Http\Controllers;

use App\Domain\Services\ArticleReader;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class ArticleController
 */
class ArticleController extends Controller
{
    /**
     * @param ArticleReader $reader
     *
     * @return Response
     */
    public function invoke(ArticleReader $reader) : Response
    {
        return response()->json($reader->readOne());
    }
}

Response

上記の様に実装すると、次の様に返却されます

{
  "id": 1,
  "title": "testing",
  "comments": [
    {
      "id": 1,
      "comment": "comment 1"
    },
    {
      "id": 2,
      "comment": "comment 2"
    }
  ]
}

簡単な例ではあるものの、想定された形式になっていると思います。

routes例

<?php
$app->get('/articles/{id}', ['uses' => 'ArticleController@invoke', 'as' => 'articles']);

記事をHATEOASへ

willdurand/hateoasを利用して組み込む例です。
Annotationを利用して、簡単に実装することができます。

github.com

コマンドなどで追加します

$ composer require willdurand/hateoas

Responseの実装

lumenでは response() ヘルパーが返却するインスタンスは変更できないため、
makeを利用するなどで対応しましょう。
下記はhalメソッドを追加する例です。

<?php
declare(strict_types = 1);

namespace App\Http;

use Hateoas\Hateoas;
use Hateoas\HateoasBuilder;
use Illuminate\Http\Response;
use App\Http\Hateoas as HateoasResource;
use Hateoas\UrlGenerator\CallableUrlGenerator;

/**
 * Class ResponseFactory
 */
class ResponseFactory extends \Laravel\Lumen\Http\ResponseFactory
{
    /** @var string[] */
    protected $headers = [
        'Content-Type' => 'application/hal+json',
    ];

    /** @var string */
    protected $serialization = 'json';

    /**
     * @param HateoasResource $resource
     * @param int             $status
     * @param array           $headers
     *
     * @return \Illuminate\Http\Response
     */
    public function hal(HateoasResource $resource, $status = 200, array $headers = []) : Response
    {
        return new Response(
            $this->builder()->serialize($resource, $this->serialization),
            $status,
            array_merge($this->headers, $headers)
        );
    }

    /**
     * @return Hateoas
     */
    protected function builder() : Hateoas
    {
        return HateoasBuilder::create()
            ->setUrlGenerator(
                null,
                new CallableUrlGenerator(function ($route, array $parameters) {
                    return route($route, $parameters);
                })
            )
            ->build();
    }
}

Lumenではrouteヘルパーでurlなどが出力可能ですので、
ルーティングに名前指定して、返却されるレスポンスに様々なurlを追加できる様になります。
application/hal+json で返却する様にheaderに追加しましょう。

App\Http\Hateoas を実装したクラスだけを許可する例です。

Article Entity

_links を追加します。

<?php

namespace App\Domain\Entity;

use App\Http\Hateoas as HateoasResource;
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;

/**
 * Class Article
 * @Hateoas\Relation(
 *     "self",
 *      href = @Hateoas\Route(
 *          "articles",
 *          parameters = {
 *              "id" = "expr(object.getId())"
 *          }
 *      ),
 *     attributes = { "method" = "GET" },
 * )
 * @Hateoas\Relation(
 *     "comments",
 *     embedded = "expr(object.getComments())",
 * )
 */
final class Article implements HateoasResource
{
    /** @var int */
    private $id;

    /** @var string */
    private $title;

    /**
     * @var Comment[]
     * @Serializer\Exclude
     */
    private $comments;

    /**
     * Article constructor.
     *
     * @param int    $id
     * @param string $title
     */
    public function __construct(int $id, string $title)
    {
        $this->id = $id;
        $this->title = $title;
    }

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

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

    /**
     * @param Comment $comment
     */
    public function setComment(Comment $comment)
    {
        $this->comments[] = $comment;
    }

    /**
     * @return Comment[]
     */
    public function getComments() : array
    {
        return $this->comments;
    }
}

Comment Entity

<?php

namespace App\Domain\Entity;

use App\Http\Hateoas as HateoasResource;
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;

/**
 * Class Comment
 * @Hateoas\Relation(
 *     "self",
 *      href = "expr('http://example.com/comments/' ~ object.getId())",
 *     attributes = { "method" = "GET" },
 * )
 */
final class Comment implements HateoasResource
{
    /** @var int */
    private $id;

    /** @var string */
    private $comment;

    /**
     * Article constructor.
     *
     * @param int    $id
     * @param string $comment
     */
    public function __construct(int $id, string $comment)
    {
        $this->id = $id;
        $this->comment = $comment;
    }

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

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

Repository

大きく変更はありませんが、シリアライズを利用してレスポンスを返却するため、場合によってはtoArrayの様なメソッドが不要になります

<?php

namespace App\Domain\Repository;

use App\Domain\Entity\Article;
use App\Domain\Entity\Comment;

/**
 * Class ArticleRepository
 */
class ArticleRepository
{
    /**
     * @return Article
     */
    public function findOne() : Article
    {
        $article = new Article(1, 'testing');
        $article->setComment(new Comment(1, 'comment 1'));
        $article->setComment(new Comment(2, 'comment 2'));

        return $article;
    }
}

Controller

コントローラのレスポンスを少し変更します
先に紹介したResponseFactory利用例です

<?php

namespace App\Http\Controllers;

use App\Http\ResponseFactory;
use App\Domain\Services\ArticleReader;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class ArticleController
 */
class ArticleController extends Controller
{
    /**
     * @param ArticleReader $reader
     *
     * @return Response
     */
    public function invoke(ArticleReader $reader) : Response
    {
        return (new ResponseFactory)->hal($reader->readOne());
    }
}

Response

これを実行すると、次のレスポンスが返却されます。

{
  "id": 1,
  "title": "testing",
  "_links": {
    "self": {
      "href": "http://localhost:8000/articles/1",
      "method": "GET"
    }
  },
  "_embedded": {
    "comments": [
      {
        "id": 1,
        "comment": "comment 1",
        "_links": {
          "self": {
            "href": "http://example.com/comments/1",
            "method": "GET"
          }
        }
      },
      {
        "id": 2,
        "comment": "comment 2",
        "_links": {
          "self": {
            "href": "http://example.com/comments/2",
            "method": "GET"
          }
        }
      }
    ]
  }
}

例えば、commentの削除などを追加することもできます。
下記の様にAnnotationを追加します。

<?php

namespace App\Domain\Entity;

use App\Http\Hateoas as HateoasResource;
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;

/**
 * Class Comment
 * @Hateoas\Relation(
 *     "self",
 *      href = "expr('http://example.com/comments/' ~ object.getId())",
 *     attributes = { "method" = "GET" },
 * )
 * @Hateoas\Relation(
 *     "delete",
 *      href = "expr('http://example.com/comments/' ~ object.getId())",
 *     attributes = { "method" = "DELETE" },
 * )
 */
final class Comment implements HateoasResource
{
    // 省略
}
{
  "id": 1,
  "title": "testing",
  "_links": {
    "self": {
      "href": "http://localhost:8000/articles/1",
      "method": "GET"
    }
  },
  "_embedded": {
    "comments": [
      {
        "id": 1,
        "comment": "comment 1",
        "_links": {
          "self": {
            "href": "http://example.com/comments/1",
            "method": "GET"
          },
          "delete": {
            "href": "http://example.com/comments/1",
            "method": "DELETE"
          }
        }
      },
      {
        "id": 2,
        "comment": "comment 2",
        "_links": {
          "self": {
            "href": "http://example.com/comments/2",
            "method": "GET"
          },
          "delete": {
            "href": "http://example.com/comments/2",
            "method": "DELETE"
          }
        }
      }
    ]
  }
}

簡単な実装例を紹介しました。
ぜひ取り入れてみてください。

composer scripts

おまけですが、
LumenにはLaravelで用意されているビルトインサーバ起動の php artisan serve はデフォルトでは含まれていませんが、
簡単に使いたい方はcomposer.jsonに次の様に追加すると簡単です。

  "scripts": {
    "post-root-package-install": [
      "php -r \"copy('.env.example', '.env');\""
    ],
    "serve": "php -S 0.0.0.0:8000 -t ./public"
  },

composer serve で実行できます。
artisan serveコマンドの中身と同じですので、わざわざコマンドを追加する必要はありませんよ!

PHPカンファレンス関西2016 登壇しました

PHPカンファレンス関西2016

去年参加できなかったPHPカンファレンス関西に参加してきました。

conference.kphpug.jp

その代わり今年は福岡に参加できませんでした。

今回は30分枠で設計寄りな[アスペクト指向によるアプリケーション拡張]で登壇しました。

日頃開発しているアプリケーションに、
アスペクト指向で取り上げられる関心毎を、
基盤的関心毎、ユースケースによる関心毎、
拡張の分離といった観点でお話しさせていただきました。

これらはアスペクト指向を取り入れるに当たって、大事なもの、
というわけではなく常につきまとう問題にどう立ち向かっていくか、というものです。

そして「サンプルを公開します!」の通り、
スライドの内容に合わせて、LaravelとLaravel-Aspectパッケージを利用した
サンプルコードを公開してあります。

github.com

アプリケーションの基本的な動作は、

  • POST: /product/purchase
  • POST: /product/reserve

で基本動作をするものになっています。

  • POST: /product/aspect/purchase
  • POST: /product/aspect/reserve

上記の二つは、基本動作をアスペクト指向を用いて拡張した例になっています。

たとえば、分離のためのモジュールはコインの表裏関係、という部分ですが、
具体的には下記の様になっています。

<?php

namespace App\Services\Aspect;

use App\Annotation\EarnPoint;
use App\Annotation\PaymentPoint;
use Ytake\LaravelAspect\Annotation\Loggable;

/**
 * Class ProductPayment
 * 商品購入拡張クラス
 */
class ProductPayment extends \App\Services\ProductPayment
{
    /**
     * @Loggable
     * @EarnPoint
     * @PaymentPoint
     *
     * @param int $id
     *
     * @return bool
     */
    public function purchase(int $id)
    {
        return $this->repository->createPurchase($id);
    }
}

まずはアスペクトを利用し、ほとんど手を加えていない状態に分割します。
モジュールは次の通りです

<?php

namespace App\Aspect\Interceptor;

use App\Services\PointBalance;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

class EarnPointInterceptor implements MethodInterceptor
{
    /** @var PointBalance  */
    protected $balance;

    // 付与ポイント
    const EARN_POINT = 1;

    /**
     * PaymentPointInterceptor constructor.
     *
     * @param PointBalance $balance
     */
    public function __construct(PointBalance $balance)
    {
        $this->balance = $balance;
    }

    /**
     * @param MethodInvocation $invocation
     *
     * @return mixed
     */
    public function invoke(MethodInvocation $invocation)
    {
        $result = $invocation->proceed();
        // ポイント付与
        $this->balance->point(self::EARN_POINT);
        return $result;
    }
}

動作拡張したいメソッドやクラスなどが動作する場合に、処理を追加しています。

内部のコードでだいたいの考え方等はすぐに理解できるとおもいます。
スライドを見ながら触ってみてください。

カンファレンスに参加して

今回の関西には初めて参加しました。
コーヒースペースでなかなか本編では聞けない様な話や、 世間話をしていたり、 web compoents周りの講演に参加したりしていました。

自分の講演後にはアスペクトに興味を持った方や、実際にどう使うかなど多義にわたる質問が廊下で繰り広げられていました。

懇親会終了技も2時過ぎまで呑みに・・・ f:id:ytakezawa:20160721021839j:plain

とても楽しかったのでまた参加したい!

自分流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

Lumen with Aspect

Lumen使ってますか?

APIなどのセッションや、テンプレートを必要としないアプリケーションには、
Laravelのマイクロフレームワーク版でもあるLumenを選択する方も多いのではないでしょうか。

このフレームワークはLaravelと同様にファサードを使うかどうかの選択が可能で、
もちろん一切使わず、Packagistなどで公開されているライブラリをデータベースに利用したりと
変更することも容易です。(現在でしたら、組み替えのしやすさではzend-expressiveの方が軍配があがるでしょう。)
ファサードを利用しない場合の利用方法は過去のエントリを参考にしてください。

ytake.hateblo.jp

ちょっとの工夫

手軽なアプリケーションでもトランザクションや、キャッシュ、ログなどいろんなものを記述することが多いでしょう。
そんな場合に、拙作のLaravel向けのパッケージではありますが、
アスペクトを取り入れることができます。

github.com

install

そのままです

    "require": {
        "php": ">=5.5.9",
        "laravel/lumen-framework": "5.2.*",
        "vlucas/phpdotenv": "~2.2",
        "ytake/laravel-fluent-logger": "~1.0",
        "ytake/laravel-aspect": "~1.0"
    },

install後に、vendor/ytake/laravel-aspect/src/config/ytake-laravel-aop.phpファイルを
プロジェクト配下にconfigディレクトリを作成して設置します。
Lumenのconfigファイル設置と同じです。

register ServiceProvider

パッケージのサービスプロバイダそのままでは、Laravelを前提としているので動きません。
Lumen用に継承して下記の通りに変更します。

<?php

namespace App\Providers;

use Ytake\LaravelAspect\AspectManager;
use Ytake\LaravelAspect\AnnotationManager;
use Ytake\LaravelAspect\AspectServiceProvider as Aspect;


/**
 * Class AspectServiceProvider
 */
class AspectServiceProvider extends Aspect
{
    /**
     * @return void
     */
    public function boot()
    {

    }

    /**
     * {@inheritdoc}
     */
    public function register()
    {
        /**
         * for package configure
         */
        $this->app->configure('ytake-laravel-aop');
        $this->app->singleton('aspect.annotation.reader', function ($app) {
            return (new AnnotationManager($app))->getReader();
        });
        $this->app->singleton('aspect.manager', function ($app) {
            // register annotation
            return new AspectManager($app);
        });
    }
}

次にbootstrap/app.phpにこのサービスプロバイダと、パッケージのartisanコマンドのサービスプロバイダを追加します。

<?php
// 省略
$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AspectServiceProvider::class);
$app->register(Ytake\LaravelAspect\ConsoleServiceProvider::class);

$app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
    require __DIR__.'/../app/Http/routes.php';
});

return $app;

php artisanコマンドでパッケージのコマンドが追加されているはずです。
php artisan ytake:aspect-module-publishでパッケージがサポートしているアスペクトのモジュールを設置できますが、
Lumenでは動かないためプロジェクト内で継承して利用します。
ここでは例としてCacheを作成する @Cacheable を取り上げましょう。
利用するキャッシュは通常のファイルを指定しておきましょう。

@Cacheable利用手引き

app配下にModulesディレクトリを作成します(なんでも良いです)。
以下の通りにCacheableModuleクラスを作成します。

<?php

namespace App\Modules;

/**
 * Class CacheableModule
 */
class CacheableModule extends \Ytake\LaravelAspect\Modules\CacheableModule
{
    /** @var string[] */
    protected $classes = [
        
    ];
}

次にこのアスペクトを利用するクラスを用意してみましょう。

App\Repository\CustomerRepositoryInterface

インターフェースと具象クラスをバインドする場合でも正しく動きますので、
これらのクラスを作成していきましょう

<?php

namespace App\Repository;

/**
 * Interface CustomerRepositoryInterface
 */
interface CustomerRepositoryInterface
{
    /**
     * @return string[]
     */
    public function getAll();
}

App\Repository\CustomerRepository

このクラスのgetAllメソッドの戻り値をキャッシュするように、@Cacheableアノテーションを記述します。
このアノテーションではキャッシュ名、タグ、キャッシュキーに使う引数や保存時間を指定できます。
キャッシュ名を指定しなかった場合は、メソッド名をキャッシュ名として利用します。

<?php

namespace App\Repository;

use Ytake\LaravelAspect\Annotation\Cacheable;

/**
 * Class CustomerRepository
 */
class CustomerRepository implements CustomerRepositoryInterface
{
    /**
     * @var string[]
     */
    private $customers = [
        [
            'name' => 'laravel'
        ],
        [
            'name' => 'lumen'
        ],
    ];

    /**
     * @Cacheable(cacheName="customers",lifetime=20)
     * @return string[]
     */
    public function getAll()
    {
        return $this->customers;
    }
}

このクラスを利用するサービスクラスを用意して、インターフェースと具象クラスを束縛します。
サービスクラスなどを利用しない方は、
コントローラなどの任意のクラスにインターフェースをタイプヒンティングしましょう

App\Services\CustomerService

<?php

namespace App\Services;

use App\Repository\CustomerRepositoryInterface;

/**
 * Class CustomerService
 */
class CustomerService
{
    /** @var CustomerRepositoryInterface  */
    protected $customerRepository;

    /**
     * CustomerService constructor.
     *
     * @param CustomerRepositoryInterface $customerRepository
     */
    public function __construct(CustomerRepositoryInterface $customerRepository)
    {
        $this->customerRepository = $customerRepository;
    }

    /**
     * @return array|\string[]
     */
    public function getAllCustomers() : array
    {
        return $this->customerRepository->getAll();
    }
}

App\Providers\AppServiceProvider

インターフェースと利用する具象クラスを記述します

<?php

namespace App\Providers;

use App\Repository\CustomerRepository;
use App\Repository\CustomerRepositoryInterface;
use Illuminate\Support\ServiceProvider;

/**
 * Class AppServiceProvider
 */
class AppServiceProvider extends ServiceProvider
{
    /**
     * {@inheritdoc}
     */
    public function register()
    {
        $this->app->bind(CustomerRepositoryInterface::class, CustomerRepository::class);
    }
}

キャッシュなし動作

この状態でコントローラからクラスを利用します。

<?php

namespace App\Http\Controllers;

use App\Services\CustomerService;

/**
 * Class CacheController
 */
class CacheController extends Controller
{
    /** @var \App\Services\CustomerService  */
    protected $customer;

    /**
     * CacheController constructor.
     *
     * @param \App\Services\CustomerService $customer
     */
    public function __construct(CustomerService $customer)
    {
        $this->customer = $customer;
    }

    /**
     * @return \string[]
     */
    public function index()
    {
        return $this->customer->getAllCustomers();
    }
}

routes.phpに任意のURIを追加してアクセスしてもキャッシュは作成されません。
これから実装したクラスには一切手を加えず、アスペクトが作用するように動作を拡張します。

App\Modules\CacheableModule

作成したCacheableModuleにアスペクトを利用するクラスを記述します。

<?php

namespace App\Modules;

use App\Repository\CustomerRepository;

/**
 * Class CacheableModule
 */
class CacheableModule extends \Ytake\LaravelAspect\Modules\CacheableModule
{
    /** @var string[] */
    protected $classes = [
        CustomerRepository::class,
    ];
}

次に先に作成したAspectServiceProviderにこのモジュールクラスを登録します。

<?php

namespace App\Providers;

use App\Modules\CacheableModule;
use Ytake\LaravelAspect\AspectManager;
use Ytake\LaravelAspect\AnnotationManager;
use Ytake\LaravelAspect\AspectServiceProvider as Aspect;

/**
 * Class AspectServiceProvider
 */
class AspectServiceProvider extends Aspect
{
    /**
     * @return void
     */
    public function boot()
    {
        /** @var \Ytake\LaravelAspect\AspectManager $aspect */
        $aspect = $this->app['aspect.manager'];
        $aspect->register(CacheableModule::class);
        $aspect->dispatch();
    }

    /**
     * {@inheritdoc}
     */
    public function register()
    {
        /**
         * for package configure
         */
        $this->app->configure('ytake-laravel-aop');
        $this->app->singleton('aspect.annotation.reader', function ($app) {
            return (new AnnotationManager($app))->getReader();
        });
        $this->app->singleton('aspect.manager', function ($app) {
            // register annotation
            return new AspectManager($app);
        });
    }
}

コンテナに登路したaspect.managerサービスにアクセスして、アスペクトカーネルクラスに登録します。
登録が完了したら再度URIにアクセスしてみましょう。

storage/framework/cache配下にキャッシュが作成されているのが確認できると思います。
このLaravel-Aspectは様々な処理に対して横断的に処理を追加することが簡単に行えます。
ライブラリではデフォルトでCacheの追加、削除が可能なアスペクト、アノテーション
データベースのトランザクションや、ロギングといったものを用意していますので、
これらを利用してアプリケーション開発に役立ててみてください。

ytake/gardening HomesteadライクなCentOS

ytake/gardening という、
HomesteadのCentOS版みたいなものを公開中です。

laravel/homestead との違いは、
OSはUbuntu14.04ではなく、CentOS7となっています。

またwebサーバが、ApacheとNginxが切り替えて利用できるようになっています。
この環境ではデフォルトで用意していませんが、リバースプロキシなども利用できると思います。

またPHPはPHP7が標準となっており、
hhvmは3.9が利用できます。

hhvmを利用する場合は、プロジェクト直下に.hhconfigも設置されますので、
hackもすぐに利用できます。

インストール済みのものは下記の通りです。

  • Git
  • PHP 7.0(remi repository)
  • HHVM(3.9)
  • Apache(2.4.6)
  • Nginx(1.8)
  • MySQL(5.6)
  • Sqlite3
  • PostgreSQL(9.4)
  • Composer
  • Node.js (With Grunt, and Gulp)
  • Redis
  • Memcached
  • Elasticsearch
  • MongoDB
  • Java(1.8)
  • fluentd
  • Couchbase

PHPはremiリポジトリとなっていますので、アップデート時などにはremiリポジトリを指定してください。

Homesteadと利用方法はほとんど同じですが、
上記のもののうち、
MongoDB、Elasticsearch、fluentd、couchbaseはvagrant起動時にオフにするなど、
利用状況に合わせてオンオフができるようになっています。

詳しい利用方法や、設定項目はGitHubでご確認ください。
タイムゾーンは日本に設定してありますので、ほとんどなにもせずに開発環境が利用できます。

急ぎでCentOSの開発環境が必要な方でHomesteadのようなものが必要であれば、利用してみてください。

2015年ytakeまとめ

本年は特にたくさんの方とご一緒する機会が多く、
周りの環境も多く変わる一年となりました。

今年作ったもの

今年はLaravel関連のパッケージを作ることが多かった一年でした。
後悔しているパッケージの他にも自作フレームワーク作りを行い、
何度も作っては削除、作っては削除・・の繰り返しですが、
作ることで学べることがたくさんありますので、来年度も引き続き何かしら作っていこうと思っています。

Laravel.Smarty

LaravelでBladeと併用してSmartyを使用可能にするパッケージです。
海外の開発者が作った同種のパッケージは、テストコードもなく、
パッケージにSmarty自体が内包されていたり、ほとんど拡張性がないものであったりと、
実際に使う上で頭を悩ますことも多く、当時自分で利用していたものをパッケージとして切り出したのが始まりです。

このパッケージ自体は去年作ったものですが、
Smartyを根強く使い続けている海外の方からの問い合わせも多く、
テンプレートのキャッシュをファイル以外にmemcachedやredisなどにも対応させて、
色々改良を加えました。

github.com

Laravel-Aspect

これはLaravelでAOPを利用可能にするパッケージで、
文字通りMVC(Model, View, Controller)としてフレームワークを利用している方にはあまり利用する機会がないかもしれませんが、
PHPにおいてもコンポーネント指向や、設計パターンなどに注力される方も増え、
特にドメイン駆動などで利用できるアスペクトによる関心の分離などをLaravelでサポートするものです。

ドメイン駆動開発ではアスペクトは必須ではありませんが、
多くのヒントや、実際に適所に使うとさらに考え方を広めてくれると思います。
これは後述するアスペクト指向の書籍で色々考えさせられることが多かったため、
実際の開発でも利用することが多くなり、来年度も引き続き利用していこうと思っています。
個人的には自分で作ったライブラリの中で一番使っているものかもしれません。
改良点はまだ多くありますので、引き続きバージョンアップを行う予定です。

github.com

Laravel-FluentLogger

LaravelのログをFluentdと連携するためのパッケージです。
大規模開発や、ログ解析などを行うことも多くなり、
Laravel向けのパッケージもなかったのでパッケージとして切り出したものです。

LogのデフォルトをFluentdに変更することや、pushHandlerで追加なども簡単にできるようになっています。
実際の開発でも使っています。

github.com

上記のパッケージのうち、Laravel-FluentLoggerとLaravel.SmartyはともにLumenでも利用できるようになっており、
どのパッケージも最新のLaravel5.2でも利用できるようになっています。

Laravel-Couchbase

データベースのドライバ拡張と、SessionやCacheでCouchbaseを利用するパッケージです。
このデータベースがサポートしているN1QLにも対応したもので、
Laravelのクエリービルダで利用できるようにしました。

<?php 

$this->app['db']->connection('couchbase')
    ->table('testing')->key($key)
    ->returning(['*'])
    ->where('click', 'to edit')->update(
        ['click' => 'testing edit']
    );

github.com

書籍

今年は2冊執筆させていただく機会がありました。
初めての執筆でいろんなアドバイスを頂いたり、うまく進まず迷惑をかけたりなどもあり、
反省するとともに貴重な経験となり、今後の糧としていきたいと思います。

Laravelリファレンス

www.amazon.co.jp

Laravelエキスパート養成読本

www.amazon.co.jp

嬉しいことに来年も引き続き執筆させていただける機会をいただいており、
嬉しい悲鳴が続きそうです。
Laravelリファレンス執筆時に掲載から漏れたスピンオフ電子版書籍は近いうちに詳細をお伝えできるかもしれません。

発表

今年はPHP勉強会をはじめ、PHPカンファレンス福岡、PHPカンファレンスなどで発表させていただきました。
いろんな方との交流も増え、参加して学ぶことも多くあり大変有意義に過ごしました。

php開発で使うタスクランナー gulp

今のバージョンでは動かないと思いますが、一応・・

www.slideshare.net

Laravel / Lumen 次の一歩

中級者へのヒントとなるような内容を発表しました

www.slideshare.net

Laravel5.1 Release

Laravel5.1発表直前に発表したものです。

www.slideshare.net

phpspecで始めるBDD

www.slideshare.net

LaravelとMVCの先へ

www.slideshare.net

PHPデプロイツールの世界

www.slideshare.net

zend-expressiveを触ってみよう

今年の後半に発表されたzend expressiveを実際の開発に利用した時の話をまとめたものです

www.slideshare.net

後半になるにつれてLaravel以外のもに変わっていってるのも面白いところです。
執筆時期と重なっていたため、あまりLaravel関連のことを発表しませんでした。

今年できなかったもの

LaravelJp Recipe

Laravelレシピサイトの5.1、もしくは5系以降のサポートサイトが途中まで進めたにもかかわらず、
書籍執筆に注力していたため、あまり進捗がない状態となってしまいました。

recipes.laravel.jp

来年は、このレシピサイトを5系以降にも対応させるか、
Laravel学習サイトとして、初心者から中上級者向けのサイトへと変更するかを検討しているところです。
さすがに古くなってきたので何かしないと・・・

仕事、その他

今年は転職したこともあり(去年もしたではないか!)、
身の回りも大きく変わり、いろんなチャレンジができる良い環境となりました。 また、今年はPHPに加えてgolangも取り入れることになり、幾つかに導入したりしていました。

家も建ち、ローンを返済する身となりました。

まとめ

今年はいろんな方に声をかけてもらうことが多く、
なかなか応えられないこともありましたが、大変貴重な体験を多くさせていただきありがとうございました。
2016年はさらに加速させて、いろんな物事にチャレンジしていこうと思います。

2015年ありがとうございました。

Lumen活用

これは Laravelリファレンス発売記念、販売促進アドベントカレンダー www.adventar.org の2015年12月18日分です。

今回は書籍では取り上げていませんが、Lumenについてです。 弊社のAPIでもLumen(とgolang)を活用していたりします。

Lumenおさらい

LumenはLaravelのilluminateコンポーネントを組み合わせて構成されているフレームワークで、
マイクロフレームワークともいわれていますが、多くの機能を有していて、
通常のフレームワークと言っても差し支えないのかもしれません。

Laravelとの違いは

  • RouterはfastRouteを利用
  • configなどは必要なものしか読み込まない(コンテナから取得時に読み込まれる)
  • サービスプロバイダは単純にコンテナに登録するのみ
  • ファサードを利用するかしないか選択可
  • APP_KEY生成コマンドなし

ぐらいでしょうか。

簡単な使い方はブログに書かれている方や、公式マニュアルを見るだけで十分理解できると思います。

.env使う場合

bootstrap/app.phpで忘れずにDotenv::loadのコメントアウトを外しておきましょう。

<?php

require_once __DIR__.'/../vendor/autoload.php';

\Dotenv::load(__DIR__.'/../');

.envを利用しない場合は、Laravelと同じように $value = getenv($key); が先に動きますので、
webサーバやphp-fomなどで環境変数を利用できます。
複数台構成の場合は.envで管理するよりもサーバなどで環境変数と組み合わせた方が事故が少ないと思います。

タイムゾーン変更

アプリケーションクラスでAPP_TIMEZONEを利用していますので、
必ず利用したいタイムゾーンを.envまたはsetEnvなどで指定しておきましょう。

APP_TIMEZONE=Asia/Tokyo

アプリケーションクラス継承

アプリケーションに合わせてアプリケーションクラスを継承する場合が多いでしょう。

利用しない機能はLaravel同様に除外できます。
$availableBindingsプロパティを上書きしてコンパクトにすることができます。
Laravel同様いらないものを削除していきましょう。

ルータキャッシュ

Lumenはルータのディスパッチャを変更できますので、fastRouteのキャッシュルータに変更してみましょう。
bootstrap/app.phpなどを利用するといいでしょう。

<?php 

$app->setDispatcher(FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) use ($app) {
    /** @var \Laravel\Lumen\Application $app */
    foreach ($app->getRoutes() as $route) {
        $r->addRoute($route['method'], $route['uri'], $route['action']);
    }
}, [
    'cacheFile' => storage_path('route.cache'),
]));

ルータにクロージャを利用している場合は利用できませんので注意しましょう。
キャッシュファイルがある場合は/app/Http/routes.phpを読み込まないようにするなどしておきましょう

artisanコマンド削除

デフォルトで用意されているコマンドを利用しない場合は、簡単に除外できます。
App\Console\Kernelクラスの$includeDefaultCommandsプロパティを利用しましょう。

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /** @var bool  */
    protected $includeDefaultCommands = false;
    
    // 省略

綺麗に書く

ファサードを使わずに、データベースなどを綺麗に利用したい場合はサービスプロバイダを利用しましょう。
簡単な例を紹介します。

<?php

namespace App\Repositories;

interface CacheRepositoryInterface
{

    /**
     * @return mixed
     */
    public function getHoge();
}
<?php

namespace App\Repositories;

use Illuminate\Cache\CacheManager;

class CacheRepository implements CacheRepositoryInterface
{
    /** @var CacheManager  */
    protected $cache;

    /**
     * CacheRepository constructor.
     *
     * @param CacheManager $cache
     */
    public function __construct(CacheManager $cache)
    {
        $this->cache = $cache;
    }

    /**
     * @return mixed
     */
    public function getHoge()
    {
        return $this->cache->driver()->get('hoge');
    }
}

Laravelではこのまま動作せることができますが、
Lumenの場合は、CacheManagerの依存を自動で解決できません。
コンテナからcacheを取得してコンフィグなどを読み込ませます。

サービスプロバイダ例

    public function register()
    {
        $this->app->bind('App\Repositories\CacheRepositoryInterface', function ($app) {
            return new CacheRepository($app->make('cache'));
        });
    }

あとは利用したいクラスなどでコンストラクタインジェクションなどを使いましょう。
LaravelでもLumenでも、どんなものでも再利用できるようになります。