読者です 読者をやめる 読者になる 読者になる

ytake Hatena

Web Application Developer

php7 でxhp-extensionをインストール

XHP

HackでおなじみのXHP XSS対策はもちろんのこと、現在のReactの元(ほぼ同じ記述法)になったもの、
とご存知の方も多いと思います。
php5ではfacebookGithubで公開されています。

github.com

が、現php7対応版は公開されておらず、Hackのみで利用可能となっています。

for php7

がしかし、php7で動くように対応したリポジトリがありました

github.com

こちらをビルドして、php7で動かしてみましょう

Linux

$ git clone https://github.com/KMK-ONLINE/xhp-php7-extension.git
$ phpize
$./configure
$ make
$ sudo make install

Mac OS

Linux環境と同じですが、最新のOSなどではgccが古いためmakeに失敗します。 brew などでgccをinstallし、Makefileを変更するか、CC= CXX= などで指定するとmake できると思います

変更する場合は、xhp-php7-extension/xhp/Makefilegcc, g++をbrewなどでinstallしたgccに変更します。

gcc-6 g++-6の場合は以下のようになるでしょう

ifdef DEBUG
CPPFLAGS = -fPIC -ggdb -Wall -DDEBUG -std=c++11
else
CPPFLAGS = -fPIC -g -Wall -O3 -minline-all-stringops -std=c++11
endif

ifdef PROFILE
CPPFLAGS += -pg
endif

FLEX = `which flex35 2>/dev/null || which flex 2>/dev/null`

all: libxhp.a libxhp.so

clean:
    -rm libxhp.a xhpize parser.yacc.cpp scanner.lex.cpp scanner.lex.hpp parser.yacc.output parser.yacc.hpp fastpath.cpp version.h *.o 2>/dev/null

parser.yacc.cpp: parser.y
    bison --debug --verbose -d -o $@ $<

parser.yacc.hpp: parser.yacc.cpp

scanner.lex.cpp: scanner.l
    $(FLEX) \
      -C --header-file=scanner.lex.hpp -o $@ -d $<

scanner.lex.hpp: scanner.lex.cpp

fastpath.cpp: fastpath.re
    re2c -c -o $@ $<

%.o: %.cpp
    g++-6 -c $(CPPFLAGS) -o $@ $<

xhp_preprocess.o: xhp_preprocess.cpp scanner.lex.hpp parser.yacc.hpp
parser.yacc.o: scanner.lex.hpp
scanner.lex.o: parser.yacc.hpp

libxhp.a: code_rope.o rope_entity.o scanner.lex.o parser.yacc.o fastpath.o xhp_preprocess.o
    $(AR) -crs $@ $^

libxhp.so: code_rope.o rope_entity.o scanner.lex.o parser.yacc.o fastpath.o xhp_preprocess.o
    g++-6 -shared -Wl,-soname,libxhp.so -o libxhp.so $^

version.h:
    echo "#define XHPIZE_VERSION \""`git show -s --format="%h - %ci" HEAD`"\"" >> version.h

xhpize: xhpize.cpp libxhp.a version.h
    g++-6 $(CPPFLAGS) -o $@ $^

rope_test: rope_test.cpp code_rope.o rope_entity.o
    g++-6 $(CPPFLAGS) -o $@ $^

.PHONY: all clean tags

*CentOS 7, Ubuntu16.04環境で動作確認

install後php.ini にextensionを追加します

extension=xhp.so

php -mでインストールされたのを確認しましょう

composer

xhpのextensionに加え、ライブラリをインストールします(kmklabs/xhp)

  "require": {
    "kmklabs/xhp": "^1.6"
  }

インストール後は通常のphpコードに記述できます。

はじめてのxhp

Reactと同様です。
このextensionを利用する場合は、
<?hh にするか、 <?php // xhpと指定する必要があります

<?php // xhp

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

/**
 * Class
 */
class :ytake:something extends :x:element {
    protected function render()
    {
        return <div class="hello">it's work!</div>;
    }
}

echo <ytake:something /> . "\n";

動作が確認できればOKです

その他

ビルドはできましたが、自分の環境では正しく動きませんでした github.com

HHVM/Hack Dependency Injection/Service Location Container公開

About

Hackで簡単に利用できる Dependency Injection/Service Location Containerライブラリを公開しました。

github.com

Pimpleをはじめとして、
多くのライブラリをそのままHHVM上で動かすことはもちろんできますが、
typescriptやflowといった厳格さを利用できるHackを活かす為、
strictモードで利用できる + psr11 に対応しています。

*ライブラリの名前はtypoではなくわざとです

Headacke

インストール方法

hhvm上で次のコマンドを利用する、composer.jsonに加えるなどしてください

$ hhvm --php $(which composer) require ytake/headacke
"require": {
  "hhvm": ">=3.11.0",
  "ytake/headacke": "~0.0"
},

利用方法

サービスロケータとして任意のものをコンテナに追加することができます

<?hh // strict
$container = new \Headacke\FactoryContainer();
$container->set('testing', $container ==> 'testing');
$container->get('testing'); 

上記の場合は コンテナに'testing' サービスを登録し、文字列の ‘testing’ が返却されます。

簡単ですね

Singleton

当然シングルトンで利用する場合は次のように登録します。

<?hh // strict

$container = new \Headacke\FactoryContainer();
$container->set('scope:singleton', $container ==> new \stdClass(), \Headacke\Scope::SINGLETON);

stdClassをシングルトンで利用する例です。
Scopeを指定してください

内部はHackで簡単に利用できるMemoizeです

<?hh // strict

  <<__Memoize>>
  protected function shared(string $id): mixed
  {
    return call_user_func($this->bindings->at($id), $this);
  }

Prototype

都度新しいインスタンスが欲しい場合は次の通りです。

<?hh // strict

$container = new \Headacke\FactoryContainer();
$container->set('scope:prototype', $container ==> new \stdClass(), \Headacke\Scope::PROTOTYPE);

scopeの指定をしていない場合はデフォルトでprototypeとなります

Dependency Injection

サービスロケータとして使う場合はこれまで紹介したものだけで十分ですが、
やはりアプリケーション内にcontainerが出てくるのは良いとは言えません。

この為、インスタンス生成したいクラスの定義を先に記述することで簡単にDIが利用できます。

parameter registration

<?hh // strict

$container->parameters(
  'string className',
  'parameter name',
  $container ==> 'parameter value'
);

インスタンスを生成したいクラスの引数などを事前に登録します。

lamdaでコンテナ自体にアクセスできますので、サービスロケータに登録したものを利用できます

次のクラスを例にしましょう

<?hh // strict

// Constructor Parameter Promotion 

final class MessageClass {
  public function __construct(protected string $message) {
  }
  public function message(): string {
    return $this->message;
  }
}

final class MessageClient {
  public function __construct(protected MessageClass $message) {

  }
  public function message(): MessageClass {
    return $this->message;
  }
}

MessageClientクラスのコンストラクタにMessageClassがコンストラクタで指定されています。

これをコンテナ経由で取得する場合は次のように記述できます。

<?hh // strict

$container = new \Headacke\FactoryContainer();
$container->set('message.class', $container ==> new MessageClass('testing'));
$container->parameters(
  MessageClient::class, 
  'message', 
  $container ==> $container->get('message.class')
);
$instance = $container->get(MessageClient::class);

コンテナに'message.class'サービスとして MessageClass を登録します
MessageClientクラスの引数として、'message.class' を利用するにように指定します。
最後にコンテナからインスタンス生成を行います。

簡単に利用できるようになっていますが、
残念ながらlaravelやleague/containerのようにauto wiringのサポートはしていません。
(getしたらそれに関連するクラスも全て依存解決する機能)
あくまで取得したいものは事前に定義しなければ動作しません!

Hackで開発をしたい方のヒントになればと思います。

vagrant box / gardening update information

laravel/homesteadのCentOS7版のようなbox ytake/gardening というものをatlasで公開しています。

ytake.hateblo.jp

先月このボックスのアップデートを行いました。 CentOSのすぐ開発できるboxが欲しい!という方にはおそらくピッタリでしょう!

github.com

アップデート内容

PHP エクステンション追加

このboxには実はかなり多彩なエクステンションが含まれています(!)

大規模アプリケーションで利用されるcassandra、couchbase(最新の4.5対応版)、rdkafka(apache kafka)
など万人向けではありませんが、導入が少し厄介なものに加えて、
microsoftsql serverがすぐに利用できるようにpdoドライバなども導入してあります。

その中でも、最近利用されるケースも多いv8jsエクステンションですが、
CentOSで利用するにはビルドや導入が結構難しかったりする場合もありますが、
今回のアップデートでエクステンションに追加しました。

CentOSでもサーバサイドレンダリングができるようになりますので、気軽に試すことができます。

含まれているエクステンションは以下の通りです

[PHP Modules]
amqp
apc
apcu
bcmath
bz2
calendar
cassandra
Core
couchbase
ctype
curl
date
dom
event
exif
fileinfo
filter
ftp
gd
gettext
hash
iconv
igbinary
imagick
json
ldap
libxml
mbstring
mcrypt
memcached
mongodb
msgpack
mysqli
mysqlnd
openssl
pcntl
pcre
pcs
PDO
pdo_dblib
pdo_mysql
pdo_pgsql
pdo_sqlite
pdo_sqlsrv
pgsql
phalcon
Phar
posix
rdkafka
readline
redis
Reflection
session
shmop
SimpleXML
soap
sockets
SPL
sqlite3
sqlsrv
standard
Stomp
sysvmsg
sysvsem
sysvshm
tokenizer
uopz
uuid
v8js
wddx
xdebug
xhprof
xml
xmlreader
xmlwriter
xsl
Zend OPcache
zlib
zmq

[Zend Modules]
Xdebug
Zend OPcache

php-dbgも利用できます

HHVM アップデート

HHVM 3.15 に更新しています。
LTSの3.18が利用できるようになればまた更新します。

MySQL PostgreSQL アップデート

これまでMySQL5.6でしたが5.7へやっとアップデートしました。
5.6から5.7に変わり、初回パスワードなどの対応が面倒くさく、後回しにしていましたw
PostgreSQLは9.5へアップデートしています。

これに伴い、データベースのアカウント、パスワードを下記のものに変更しました。

user: gardening
password: 00:secreT,@

Symfony対応

symfonyの開発がすぐにできるようにと対応しました。

対応方法は導入時に配置されるgardening.yaml(or gardening.json)に次のように記述します。

sites:
    - map: gardening.app
      to: /home/vagrant/yourProject/public
      type: symfony

かんたん! (HHVMで動かす場合は hhvm: true)

node.js周り

vagrantでの開発環境ということで以下のものをグローバルにインストールしています。

npm install -g typescript
npm install -g webpack
npm install -g nuclide
npm install -g yarn

HHVMに対応しているboxということもあり、nuclideで開発するための環境も一応用意しています。

その他細かい変更

  • supervisorの追加
  • elasticsearch 5.1アップデート
  • couchbase 4.5アップデート
  • golang 削除(環境問わず開発できるため削除しました)

RDBMS以外のデータベースの利用方法については、readmeに記載しています

オプションの利用方法

*気づかれていませんが、このboxはNginxとApache HTTP Serverの切り替えができます(!)

HHVM/Hackはじめの一歩

phpの拡張として、魅了的な要素がたくさん詰まったHHVM/Hack

挿入を始めるにあたって、壁にぶち当たるのがphpstormなどの様な高機能なIDEがない、

などがあげられるかもしれません

IDEに代表される様な補完機能がなかなか効かないだったり、
typecheckerで既存のライブラリが動かない、なんてこともあるのかもしれません。

hhvm-autoload

github.com

HHVMでcomposerを最適化するプラグインとして動作するライブラリです

hhi

Hackで開発する場合、厳格な型指定はメリットであり、
アプリケーション開発時にはstrict指定をすることがほとんどだと思います。

<?hh // strict
// 厳格モード

ちなみにdeclにするとtype checkされません

<?hh // decl

*何も指定しない場合はパーシャルモードである程度はtype checkしますが、それ以上のことはしないモードです

厳格モードにした場合は、phpライブラリを流用する場合に型エラーが多く発生します。

その場合は、.hhconfigassume_php=false を記述するなどの方法があります。

Typechecker: Setup

この他にhhiファイルと呼ばれる実装が無い型宣言ファイルを作成し、strictで動作させる様にすることができます。

type scriptやflowを使って開発している方はイメージがつくと思います。

このファイルを使うことで、type checker(nuclideも)がphpのライブラリであってもスムーズに利用できる様になります。

*nuclideで補完が効かないと云う方は是非お試しください

PHPUnit

HackUnitなどもありますが、デファクトスタンダードでもあるPHPUnitを利用する機会の方が多いでしょう。

PHPUnitはHackの様な完全な厳格さはありませんが、91-carriage/phpunit-hhi を使うことでコード補完などが利用できます。

git.simon.geek.nz

Hackにのみ存在する様なコードをテストする場合は前述のHackUnitなどを使用しましょう。

なにもない状態でHackを使っている場合は
挙げたものを加えるだけで今までよりもスムーズに開発できる様になるでしょう!

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