nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

Nuxt.jsにおけるサーバーサイドレンダリングの挙動とライフサイクル

Nuxt.jsというかSSR関係を触る際にまず疑問になるのは「どこまでがサーバーサイドレンダリング (SSR) でどこからがクライアントサイドレンダリング (CSR) なの?」ということだと思います。

基本

簡単に言えば、ブラウザでURL直接指定で開けばSSR、それ以降はCSR、ということになります。

当たり前ですが、サーバーサイドレンダリングはサーバーでの実行なので、windowオブジェクトも存在しないし、そこから外部サーバー(APIサーバーなど)にアクセスする際はそこからのアクセスになります。そのままではブラウザのcookieも利用できません。

axios-moduleは何をしているのか

上記の通り、SSRになった場合とCSRになった場合は通信経路も変わるしcookieの取扱も変わります。

これらを真面目に対応すると結構大変なのですが、 axios-module を使うと大体のことを解決してくれます。

axios-moduleは大きく以下のようなことを行ってくれます。

  • SSRの場合とCSRの場合でエンドポイントを切り替える
  • クライアントサイドのHTTPヘッダ(cookieなど)をサーバーにそのまま透過的に送信する

他にもいろいろありますが、重要なのはこの2点で、これにより通信時のレンダリング場所がサーバーかクライアントかを意識せずにAPIアクセス処理を書くことができるようになります。Nuxtで開発する場合は必須と言っても過言ではないでしょう。

なお、複数のエンドポイントを指定したりはできないので、API通信先は1箇所に固定されることになります。複数のエンドポイントが発生しそうな場合はクライアント(Nuxt)だけで解決せず、BFF (Backends For Frontends) パターンを用いて処理するのが良いでしょう。

ライフサイクル

Nuxtを使うとコンポーネントにメソッドが増えたり、ミドルウェアプラグインのような仕組みが追加されます。これらがSSRの場合とCSRの場合でどのように呼ばれるかは使ってみないとわからないところです。

そこで、サンプルプロジェクト を用意し、調査してみました。

初回アクセス (SSR)

そこからの遷移 (CSR)

つまりどういうことか

Nuxtでアプリを作る際は、SSRでもCSRでも問題なく表示できるように作る必要があります。また、クローラーなどからのアクセスは常にSSRになるため、クローラーなどに拾ってほしい情報はSSRの段階で確定させる必要があります。

一方で、windowオブジェクトにアクセスするもの、例えばlocalStorageを使うようなものはクライアントサイドのみでしか実行することはできません。 mounted 以外で書く場合は process.server 変数などを使って分岐させる必要があります。

特に注意したいのが created で( beforeCreate も)、SSR時とCSR時の両方で呼ばれます。普通に書くと二重に処理してしまうことになるので、意識して書かないと意図しない挙動になる可能性があります。

まとめ

このあたりの挙動は分散して書かれてはいるものの、1ページにまとまっているものが見つからなかったため、今回用意してみました。

SSRは便利ではあるものの、どうしても層が1つ増えるために挙動が複雑になります。ちゃんと把握していれば怖くないので、どこで何が動いているのかを意識しながら書くようにしましょう。

WEB+DB PRESS Vol.95で特集記事を書きました

こちらではお久しぶりです。nazoです。

この度、現在所属しているUUUM株式会社のメンバーと、HTTPに関する特集記事をWEB+DB PRESS Vol.95に書きました。2016年10月22日発売です。

雑誌記事の執筆というのは人生初で、わずか8ページ程度かと思いきや執筆はハードでした。本一冊書くのとかどれだけ大変なんだよと思いましたが、いつかは挑戦したいものです。

私のパートはHTTPヘッダの詳細についてということで、このページ数で何を伝えるべきか考えたのですが、この特集に興味がある人がどのようなこと知ることができると嬉しいのかを考えて厳選したつもりです。

他の記事も興味深いものばかりですので、書店で見かけたらよろしくお願いします。

APCキャッシュを安全に扱うSafeApcを作った

https://github.com/nazo/safeapc

なにこれ?

APC(APCu)のユーザーキャッシュ(アプリから指定するキャッシュ。ソースコードのキャッシュではない)を「そこそこ安全に」扱うための簡単なラッパーです。

どう使うの?

packagistに登録してあるので普通にcomposerから入れてください。

基本的には普通にapc_fetchとかするのがSafeApc::get等に変わっただけです。

あと、リクエストの最初に、以下を入れる必要があります。

// キャッシュで使用するリクエスト開始時間を指定
SafeApc::setCacheStartTime($_SERVER['REQUEST_TIME']);

// キャッシュのバージョン番号を指定(この例では外部ファイルから)
SafeApc::setCacheVersionKey(file_get_contents('apc_version'));

これだと何がいいの?

これにより、APCキャッシュの問題2つが改善されます。

ttlの時間は常にtime()を参照する問題

APCttlを指定した場合、そのttlの判定は、常に「APC関数を呼び出した時点でのtime()の戻り値」となります。

通常、Webアプリでは、処理中の内部時間がずれてしまうと内部時間の一貫性がなくなるため、リクエスト開始時刻をそのリクエスト中の時間とし、リクエスト中の処理が1秒以上経過しても、全ての更新時刻などはリクエスト開始時刻で扱うことが多いですが、APCttlはそこは当然無視されるので、ttlを厳密に扱おうとした場合に、微妙にずれる、ということが発生します。

例えば、以下のような感じです。

// このキャッシュは1秒だけ持たせよう
apc_store('key', 'value', 1);

// 適当な処理
sleep(3);

// 同一リクエスト中だしさっきのキャッシュ残ってるよね?→ない!
apc_fetch('key');

これを回避するために、ttlは86400で固定し、PHP側で生存時間を判定するようにしています。なんで86400固定なのかは後述。

全キャッシュクリアは高負荷だよ問題

APCの全キャッシュクリアは、同プロセスのAPC処理を全てブロックします。このため、確実に他の処理に悪影響が発生します。

また、PHP5.3以前&apache2 mod_phpでは、このブロックされている途中にmax_execution_time超え等で強制終了されると、ロックを開放せずに全プロセスを巻き込んで死ぬという挙動があります。(このへんの話

どちらにしても、全キャッシュクリアは危険なので、キャッシュクリアは2つの方法を組み合わせて行っています。というか、行ってください。

  • 無期限設定でも、必ず86400秒(1日)経過で自動で消える。(変更できます)
    • この時、86400秒で一斉に消えると高負荷になるので、ランダムで+0〜30秒増やしています。これによりキャッシュクリアのタイミングが、よりバラつくようになります。
    • デフォルトが86400秒なのには特に意味はありませんが、なんとなく1日くらい残ってくれれば大丈夫かなーという感じで指定しています。
  • キャッシュのバージョンを変えることによって、そもそも前のキャッシュキーにアクセスしないようにしている。
    • 内部的にはキャッシュのキーは「バージョン#本来のキー」という文字列になります。そのため、バージョンを変えると、古いキーには絶対にアクセスしません。
    • これはこれで、古いキーが1日残り続ける、という問題はあります。
    • 従って、「SafeApc::setCacheVersionKeyに与える引数を変える=キャッシュクリア」となります。
    • ついでに言うと、通常APCのキャッシュクリアは、Webサーバーのプロセス上で行わないといけないので、コマンドラインからの実行には一手間必要でしたが、この方法だとただバージョン番号を変えるだけなので、簡単にキャッシュクリアをコマンドライン上で行うことができます。

必要な設定

  • apc.user_ttlは何でもいいので必ず設定してください。0だと、メモリが溢れた場合に、apc_storettlを無視して全キャッシュクリアを行うという問題があります。
  • 削除を行わない方針のため、比較的メモリ使用量が増えやすいです。apc.shm_sizeは、「キャッシュの最大量 * 想定される1日のキャッシュクリア回数」くらいのサイズを指定しておきましょう。
  • 内部的にはapc_fetch等を呼んでいるので、APCuで使う場合はAPC互換モードでビルドしてください。(Enable full APC compatibility [yes] : yes)

まとめ

ここまで書いておいてアレですが、APCキャッシュは外部との通信がない分高速ですが、基本的にmemcachedかredisでいいと思います。APCだと複数サーバあるとその台数分キャッシュ生成処理が走るし。状況次第ですね。

某所で散々検証した内容なので、理論的には問題ないはずですが、このコード自体は全く使っていないしろくにテストしていないので、バグがあれば教えてください。

Laravel5を業務で一通り使ってみた感想

2ヶ月くらい使ってみたけど、悪くはないんだけど、なんか最近持ち上げられすぎてる気がするので、気になったところを書き出してみる。

あくまで個人的な感想なので、マサカリ投げるのは歓迎。

概要

Noticeを含むエラーが全て例外になる

最近のフレームワークは大体やってそうだけど、エラーを出すと全て例外を出すので、不要にcatchしなければ問答無用で落ちる。これにより、NOTICE出しっぱなしで何となく動いているコードを殲滅できる。標準にしてほしい。Fatalも例外として扱ってくれるが、Fatalを取れるのはプログラム終了後なので、特に意味はない。出力が同一フォーマットになるくらい。

ちなみに自前で実装するにはErrorExceptionのページに書いてあるので、この通り実装しよう。簡単である。

ファイルの配置が自由

composer autoload使っていればどれでもそうなんだけど、ModelとかServiceとか別に規定のフォルダに配置する必要はない。namespaceさえ解決できればどこからでも読み込める。これにより、アプリを複数に分けたいけどModelは共有したい、というような配置も簡単にできる。自分は本体と管理画面を別アプリにしつつ、Modelと、ビジネスロジック(いわゆるDDDにおけるサービス)は同一のディレクトリから参照している。

具体的には、composer.jsonに以下のように書き足せば良い。

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Model\\": "../models/"
        },

psr-4と書いてある通り、ここに記載したものは、PSR-4に準拠した形式でautoloadされる。簡単に言えば、jsonに記載したパスを起点として、namespaceの区切りとディレクトリ構造が一致している前提で読み込みに行く。今時autoload周りは自分で書くことはない。

Laravelはディレクトリの規定がほとんど無く、namespaceからautoloadできれば何をどこに置いても大体動くので、うまく使おう。

なぜかソースフォーマットがPSR-2無視

タブインデントしたり改行位置が独特だったり謎が多い。自動生成した時とかにイラッと来る。何で無視しているのかよくわからないが、とりあえずしばらくはこのままらしい

そもそもSymfony Component使いまくり

これのせいで、たまにソースを追うと、コアを読んでるはずなのに突然vendor/symfony以下に移動しないといけなくなってだるい(Requestとか)。それって結局Symfony系でいいのでは…?一方で、何でもフルスクラッチすればいいってものではないというのもわかる。

DIが強い

ControllerにDIし放題(後述のリフレクション)とか、FacadeとかContractの仕組みによってDIが簡単になっている。と思う。ナチュラルに使うのであまり意識することはない。ナチュラルに使えるくらい良く出来ているのではないかと思う。押し付け感は少ない。

ルーティング

prefixやドメイン名でグルーピングできたり、ルーティングでMiddlewareを足したりできるのでかなり優秀。LaravelCollectiveを使えばannotationで指定もできる。自分は使っていない。

Middlewareというのはいわゆるフィルタ的な、共通の事前処理のようなもの。認証チェックとかビューの共通変数を当てたりするのに使うことが多い。

リフレクション

例えば、Controllerのコンストラクタに、勝手に引数を追加したりすると、そのインスタンスが何故か勝手に渡ってくる。以下はドキュメントから引用

<?php namespace App\Http\Controllers;

use Illuminate\Routing\Controller;
use App\Repositories\UserRepository;

class UserController extends Controller {

    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

}

この例では、コントローラーのコンストラクタに突然UserRepositoryが登場するが、実行するとちゃんとインスタンスが渡ってくる。例えばCakePHPだと、プロパティにprotected $uses = ['User'];のように、規定されたものを規定された通りに読み込むことしかできないが、このパターンであれば何でも注入することができる。

まあこれだけだと、普通にnewすればいいじゃん、とも思うが、Contractsと組み合わせると、Controllerに渡すのは定義だけで、実装を何にするかは別の場所で定義することができる。強い。

難点としては、リフレクション使いまくりなので、多分処理速度がかなり犠牲になっているのでは、と思う。ベンチマークはしていない。

Eloquent(O/Rマッパー)

Laravel標準のO/RマッパーであるEloquentだが、何故か今時ActiveRecordっぽい感じだったり、複合PKが扱えなかったり(ちょっと改造すると扱えるようになるが)と、時代遅れ感が強い。CakePHPですら3.0になってテーブルとエンティティを分離している

クエリビルダ自体はそこそこ優秀で、行ロックやバルクインサート、JOIN周りもちゃんと書けるので、実用的ではあるのだが、今時ならDataMapperを採用していただきたかったところ。フォローしておくと、Eloquent自体も、HasMany的なものは明示指定しない限り呼び出されない(余計なことをしない&自動Memoizeしてくれる)とか、Collectionが楽とか、いいところもある。

自分はあまり考えずに導入してしまい後戻りができなくなっているが、新規に書くのであれば、O/RマッパーだけDoctrineを使うことも十分に検討したほうがいいと思う。ただし認証周りなどのDBに依存する標準機能が全滅するので、そこはちょっと面倒。まあ認証あまり使ってないけど。

MigrationやSeeding(いわゆるfixture的なやつ)もあるけど、特にモデルと連動していないし、phpmigとかでいいんじゃないかなとも思う。Eloquent含めて全部セットで使うには便利だし、特に避ける理由はない。

認証

標準で認証周りが用意されているが、正直めんどくさい。

拡張性も十分に用意されているのだが、ドキュメントが少なく、結局ソースを読んでいるうちに最初からスクラッチで書けば済むのでは…という感じになる。ソースコード自体を再配布するようなものでなければ、標準のものは無視してスクラッチで書いてしまえばいいと思う。いずれにせよ、認証周りのソースコードAuthManagerGuardRegistrarあたり)は読んでおく必要がある。

Middleware+DIが優秀なので、自分で書くにもそんなに苦労はしないと思う。

Blade(テンプレートエンジン)

悪くないんだけど微妙。Twigに対するアドバンテージが特に思いつかない。せめてプラグイン的なものは用意してほしかったような…。テンプレート継承とかは用意されているし、PHP解釈できるから最小限のもの以外はPHPで書けばいいじゃんってのはわかるんだけど…。うーん…。

あとフォームヘルパはLaravelCollectiveに置いてある。そのままだとさすがにdefaultとかの処理が面倒なので使おう。

テンプレートの機能ではないが、old('name')と書くと、前回のフォーム入力情報を取得できるのは便利。入力失敗した時とかにわざわざController側で同じ入力値をViewに渡す必要がない。<input type="text" name="name" value="{{ old('name', $user->name) }}" />のように書くことが多い。(oldの第二引数はデフォルト値)

Storage

S3と通常のファイルシステムを同じように扱うことができる。これにより本番はS3、開発環境はファイルシステムということが簡単に書ける。便利。特定のファイルシステムの時だけの処理とかも書けるし、特に不満はない。

Laravel Elixir

gulpのラッパーだが、特にgulpのラッパーというもの以上の何かはない。素のgulpに比べて書きやすくはなっているが、複雑なことをしようとすると途端に面倒になるし、独自知識を覚えてもあまりメリットがないので、Laravelと心中するつもりでなければ、素直に素のgulpなり一般的なものを利用したほうがいい。JavaScriptなのでそもそもLaravelである必要も疑問。

同様の謎ラッパーにEnvoy(Ansible的なやつ)等もあるが、やはりAnsible使えよって思う。ってかLaravelCollectiveにも似たようなものがあるんですがそれは…。強いて言えば、Laravel Elixirと違い、こちらはPHPなので、なんでもPHPで書きたい!という人にはいいと思う。

キュー&タスクランナー

Envoyのほうじゃなくて、アプリ内タスクを動かすタスクランナー、つまりcron的なものが用意されている。いやcronで呼び出してはいるんだけどPHPはマルチスレッド系は絶望的なので、cronでスケジューリングしたりタスクキューを作ったりすることが多いが、これがPHP側で管理する方法が標準搭載されているのは良いと思う。と言ったものの、まだ使っていないので何とも言えない。実行プロセス数の制限とかどうするんだろう。

Homestead

Laravelがいきなり動くVagrant一式だが、何故かコマンドがPHPで書かれてて謎の努力を感じる。VagrantやHHVMの使用例として参考にする程度にはいいが、結局環境によって細かい変更するだろうし、自分で構築したほうがいいとは思う。ハンズオンとかで使うには便利だと思う。「Homestead入れておいてね」で済む。

テスト

まあ普通に書ける。URLにアクセスしてレスポンスを調べるようなテストも何も考えずに書ける。Migration+SeedingでDBが絡むテストも簡単に書ける。特に問題ない。

Dotenv

Laravelあまり関係ないけど、Laravelでは.envというファイルに設定を書く。これはphpdotenvの機能で、名前の通り、環境変数に相当するものを書くだけなので、環境変数として設定できるのであれば、どこからでも設定できる。例えばAWSのアクセスキーを最初から環境変数に入れている場合、改めて設定を書く必要がない。まあAWSの設定はaws-sdkが勝手に拾ってくれるけど…。

また、dotenvのような設定形式は他言語でもあるため(例えばgolangとか)、設定の共有が簡単に行うことができる。例えば、PHPでは苦手となる生socketを扱ってリアルタイム通信するような別プログラムと、メインのPHPアプリで、同じ設定ファイルを読む、といったことが可能になる。

Carbon

またLaravel関係ないが、Carbonに標準対応しているので、DBの日付設定とかが楽。

まとめ

個人的にはSilexでいいんじゃないかと思うことが多いが、まあこれはこれでいいんじゃないかなーと思う。よく検討してから使おう。でも好きなものがあるなら好きなものを使うのが一番だと思う。

Laravel関係ない内容が多いな…。

Laravel(5.0)のCollection

LaravelでDBからリストを取得したりすると、通常の配列ではなく、Collectionというオブジェクトが返ってくる。

これはPHPの配列をラップしたもので、便利な機能がいろいろ用意されている。配列をあんなことやこんなことしたいのにPHPがunkoで面倒!ということも通常に比べて大幅に少ない。ただし処理コストは少し高め(配列をラップするオブジェクトを毎回作ることになるので)。とはいえどうせDBから取得した場合はこれになっているので、遠慮なく使おう。

いろいろあるが、覚えるのが面倒であれば普通に配列として扱うのが良い。ArrayAccessとかIteratorとか実装されているので、ほとんどの場合は普通の配列と同じ感覚で扱える。無理に覚える必要はない。

通常のCollection

Illuminate\Support\Collectionのほう。

make($items = null)

Collectionを新しく作成する。

all()

要素を全て返す。どうしても配列として扱いたい場合に使おう。(ほとんどの場合で必要ない)

collapse()

要素が全て配列またはCollectionから構成されているCollectionの場合、それを一次元に展開する。 つまり、[[1, 2], [3, 4], [5, 6]][1, 2, 3, 4, 5, 6]になるイメージ。 後述のflatten()のほうが使いやすいかも。

contains($key, $value = null)

検索メソッドだが引数によって挙動が変わる。

  • $keyスカラー値の場合はin_array相当。(つまり$keyの値をリストから検索する)
  • $keyがCallableの場合は$this->first($key)相当。(firstについては後述)
  • $valueを指定した場合は、$key番目の要素が$valueかどうかを判定する。

diff($items)

array_diff相当。

each(callable $callback)

foreachができる。DB::select('...')->get()->each(function($item) { ... });とか書ける!PHPだとあまりいらないかも…。

fetch($key)

$keyで値を絞り込む。$keyは文字列で、.で区切ると階層指定できる。この.区切りはLaravelではよく使われている(config、data_get等)。

filter(callable $callback)

array_filter相当。

where($key, $value) / whereLoose($key, $value)

DBのwhere的な感覚で、$key === $valueのものを検索して返す。==で検索したい場合はwhereLooseを使う。

first(callable $callback = null, $default = null)

要素の最初の値を返す。

flatten()

collapse()と基本的には同じだが、「要素が全て配列またはCollection」の制限がない。

flip()

array_flip相当。

forget($key)

unsetと同じ。

get($key, $default = null)

普通の配列アクセスと同じ。存在しなければ$defaultを返す。

groupBy($groupBy)

いわゆるDBのgroupByのように、$groupByで絞り込んだリストのリストを返す。

$groupByを無名関数にした場合は、function($value, $key)が渡るので、これの戻り値でグルーピングする。

keyBy($keyBy)

groupByに似ているが、こちらは一次元配列として返す。つまりDISTINCTに近い。

has($key)

isset($array[$key])と同じ。

implode($value, $glue = null)

implode相当。 第二引数を指定すると、$this->lists($value)したものを$glueimplodeする。

intersect($items)

array_intersect相当。

isEmpty()

配列が空かどうかを返す。

keys()

キーのリストを返す。

last()

要素の最後の値を返す。

map(callable $callback)

eachと基本的に同じだが、こちらは非破壊(とは言ってもCollectionそのものが非破壊なだけなのであまり意味はないが、この後にforget()とか実行すると変わってくる)。あとfunctionにkeyが入る。->map(function($item, $key) {})

merge($items)

array_merge相当。

forPage($page, $perPage)

pagination用に分割したリストを返す。$pageが現在のページ番号(1起点)、$perPageが1ページに表示する数。

pop()

array_pop相当。最後の要素を返してそれを抜く。

prepend($value)

array_unshift相当。最初に要素を足す。

push($value)

array_push相当(内部ではarray_pushはしていない)。最後に要素を足す。

pull($key, $default = null)

$keyで検索した値を取得し配列から抜く。

put($key, $value)

$key$valueをセットする。$list[$key] = $value;と同じ。

random($amount = 1)

$amountの数の配列をランダムに取得する。

reduce(callable $callback, $initial = null)

array_reduce相当。

reject($callback)

  • $callbackfunction(配列の値)の場合、返す値が trueのものを除去する
  • $callbackがそれ以外の場合、$callbackと配列の値が ==で一致するものを除去する

count()

数を返す。つまりcount($objects);とか書かないで、$objects->count();と書ける。

toArray()

中身を全て配列にして返す。中身がobjectだった場合は、そのobjectにtoArray()が実装されている必要がある。

toJson($options = 0)

toArray()したものをjson形式として返す。

shuffle()

shuffle($array)と同じ。

shift()

array_shift($array)と同じ。

slice($offset, $length = null, $preserveKeys = false)

array_slice相当。

chunk($size, $preserveKeys = false)

array_chunk相当。

sort(callable $callback)

uasort相当。callbackをさくっと書く場合はこれ。

sortBy($callback, $options = SORT_REGULAR, $descending = false)

  • $callbackfunction(配列の値, 配列のキー)を入れた場合は、そのfunctionで返した値をソートキーとしてソートを行う。
  • $callbackが文字列の場合、その文字列でdata_getを行い、それで取得できた値でソートを行う。

sortByDesc($callback, $options = SORT_REGULAR)

sortBy$descending = true相当。

splice($offset, $length = 0, $replacement = [])

array_splice相当。

sum($callback = null)

  • $callbacknullの場合はそのままarray_sumを行う。
  • $callbackが文字列の場合はdata_getで取得した値を合計する。
  • $callbackfunction(配列の値)の場合は、functionで返した値を合計する。

take($limit = null)

$limitが正の値の場合は先頭から、負の値の場合は最後から、指定した件数を取得する。

transform(callable $callback)

array_map相当。mapと違い自身を破壊する。(mapは新しいインスタンスを返す)

unique()

array_unique相当。

values()

array_values相当。

DB用Collection

Illuminate\Database\Eloquent\Collectionのほう。Illuminate\Support\Collectionを継承している。DBからオブジェクトリストを取得した場合はこれが返る。

SQLを書かなくてもある程度絞込することが簡単にできる。既にデータを取得済みで絞込をしたい場合、わざわざSQLを発行して絞込をしなくても良い。

contains($key, $value = null)

PKを軸に検索するようになる。

toBase()とかを使ってobjectでないCollectionになった時にこれを呼ぶとエラーになる。悲しい。そんな状態になったら素直にall()してin_array()しよう。

find($key, $default = null)

Collection内のModelをgetKey()した値が$keyだった場合にそのModel、無ければ$defaultを返す。複数あれば最初に見つかるものを返す。

getKey()は通常、Eloquent ModelのPKを指すので、つまりPKで検索して1つ返す、つまりModel::find()と同じ動作と言える。

load($relations)

relationを一度に全て取得する。Model::with('relation')->get()withを後から行うような感じ。詳しくはこちら

toBase()

\Illuminate\Support\Collectionにラップして返す。継承元の動作をさせたい場合に使おう。

getDictionary($items = null)

Primary Keyをキーにした配列として返す

add($item)

$itemを要素に追加する。array_push相当と言える。(何故Baseのほうに存在しないのか謎)

modelKeys()

PKのリストを返す。

only($keys)

指定PKのみのリストを返す。配列で複数指定もできる。

except($keys)

指定PKを除外したリストを返す。配列で複数指定もできる。

Laravel5をHerokuのHHVMで動かす

アプリを作る

前回の記事を参考に適当に用意する。

Herokuにpushできるようにする

heroku loginまでは省略。cedar-14Stackじゃないと動かないので注意。新規でheroku createすれば大丈夫。

composer.jsonを編集し、hhvmの定義を追加する。 あと、scriptsブロックのphphhvmに置換する。

        "require": {
                "hhvm": "~3.5",
                "laravel/framework": "5.0.*"
        },

composerをupdateする。--no-scriptsを使うとscriptsブロックが動かなくなるので、必要であればローカルでは手動で実行しよう。ローカルにHHVMが入っていればそもそも必要ない。

% composer update --ignore-platform-reqs --no-scripts

Heroku 初期設定

% heroku create
Creating [インスタンス名]... done, stack is cedar-14
https://[インスタンス名]herokuapp.com/ | https://git.heroku.com/[インスタンス名].git

% git init
Initialized empty Git repository in /path/to/app/.git/
% heroku git:remote -a [インスタンス名]
Git remote heroku added

% heroku config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-php
Setting config vars and restarting [インスタンス名]... done, v15
BUILDPACK_URL: https://github.com/heroku/heroku-buildpack-php
% heroku config:add LD_LIBRARY_PATH=vendor/hhvm/
Setting config vars and restarting [インスタンス名]... done, v21
LD_LIBRARY_PATH: vendor/hhvm/

% git add *
The following paths are ignored by one of your .gitignore files:
vendor
Use -f if you really want to add them.
% git commit -m "initial commit"
[master (root-commit) 1232328] initial commit
 155 files changed, 19937 insertions(+)
 create mode 100644 app/Article.php
 create mode 100644 app/Commands/Command.php
 create mode 100644 app/Console/Commands/Inspire.php
 create mode 100644 app/Console/Kernel.php
….
% git push heroku master
Counting objects: 188, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (167/167), done.
Writing objects: 100% (188/188), 121.66 KiB | 0 bytes/s, done.
Total 188 (delta 10), reused 0 (delta 0)
remote: Compressing source files... done.
...
remote: Verifying deploy... done.
To https://git.heroku.com/[インスタンス名].git
 * [new branch]      master -> master

% heroku ps:scale web=1
Scaling dynos... done, now running web at 1:1X.

当たり前だが、これではDBの設定もできていないし動くわけがない。

Procfileの設定

DB以前にサーバの設定をしていない。

vim Procfile

以下の内容を入力する。nginxでは.htaccessで詰む。(もちろん設定すればどうにかできる)

web: vendor/bin/heroku-hhvm-apache2 public/
% git add Procfile
% git commit -m “add Procfile"
% git push heroku master

MySQLの設定

MySQL(ClearDB)を使う。hhvmがPostgreSQLをサポートしていない。 クレカ入力したくないけど観念して https://dashboard.heroku.com/account/billing から入力する。

% heroku addons:add cleardb:ignite
Adding cleardb:ignite on [インスタンス名]... done, v34 (free)
Use `heroku addons:docs cleardb` to view documentation.

MySQLの設定を確認する。

% heroku config
...
CLEARDB_DATABASE_URL: mysql://[ユーザー名]:[パスワード]@[ホスト名]/[DB名]?reconnect=true
...

これを見ながらHeroku側に環境変数をセットする。

% heroku config:set APP_ENV=production
Setting config vars and restarting [インスタンス名]... done, v6
APP_ENV: production
% heroku config:set APP_DEBUG=false
Setting config vars and restarting [インスタンス名]... done, v7
APP_DEBUG: false
% heroku config:set DB_HOST=[mysqlのホスト名]
Setting config vars and restarting [インスタンス名]... done, v8
DB_HOST: [mysqlのホスト名]
% heroku config:set DB_DATABASE=[mysqlのDB名]
Setting config vars and restarting [インスタンス名]... done, v9
DB_DATABASE: [mysqlのDB名]
% heroku config:set DB_USERNAME=[mysqlのユーザー名]
Setting config vars and restarting [インスタンス名]... done, v10
DB_USERNAME: [mysqlのユーザー名]
% heroku config:set DB_PASSWORD=[mysqlのパスワード]
Setting config vars and restarting [インスタンス名]... done, v11
DB_PASSWORD: [mysqlのパスワード]

migrationを実施する。

% heroku run "hhvm artisan migrate:install"
Running `hhvm artisan migrate:install` attached to terminal... up, run.3772
Migration table created successfully.
% heroku run "hhvm artisan migrate:refresh"
Running `hhvm artisan migrate:refresh` attached to terminal... up, run.8212
**************************************
*     Application In Production!     *
**************************************

Do you really wish to run this command? [y/N] y
**************************************
*     Application In Production!     *
**************************************

Do you really wish to run this command? [y/N] y
Nothing to rollback.
**************************************
*     Application In Production!     *
**************************************

Do you really wish to run this command? [y/N] y
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2015_02_05_120749_create_articles

ブラウザで確認して、一通り動けばOK。

Laravel5を試す

Laravelとか初めて使うよ!出たばっかりなので試す。

この記事はLaravel5.0が出たばかりに書いた内容です。最新の内容と必ず比較してください。

インストール

PHP5.5以上とmcrypt/openssl/mbstringとcomposerが入ってる前提。 まずインストーラのインストール

php composer.phar global require "laravel/installer=~1.1"

プロジェクトの作成

laravel new blog

とりあえず

$ php artisan -v
Laravel Framework version 5.0

Usage:
 [options] command [arguments]

Options:
 --help (-h)           Display this help message
 --quiet (-q)          Do not output any message
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
 --version (-V)        Display this application version
 --ansi                Force ANSI output
 --no-ansi             Disable ANSI output
 --no-interaction (-n) Do not ask any interactive question
 --env                 The environment the command should run under.

Available commands:
 clear-compiled       Remove the compiled class file
 down                 Put the application into maintenance mode
 env                  Display the current framework environment
 help                 Displays help for a command
 inspire              Display an inspiring quote
 list                 Lists commands
 migrate              Run the database migrations
 optimize             Optimize the framework for better performance
 serve                Serve the application on the PHP development server
 tinker               Interact with your application
 up                   Bring the application out of maintenance mode
app
 app:name             Set the application namespace
auth
 auth:clear-resets    Flush expired password reset tokens
cache
 cache:clear          Flush the application cache
 cache:table          Create a migration for the cache database table
config
 config:cache         Create a cache file for faster configuration loading
 config:clear         Remove the configuration cache file
db
 db:seed              Seed the database with records
event
 event:generate       Generate the missing events and handlers based on registration
handler
 handler:command      Create a new command handler class
 handler:event        Create a new event handler class
key
 key:generate         Set the application key
make
 make:command         Create a new command class
 make:console         Create a new Artisan command
 make:controller      Create a new resource controller class
 make:event           Create a new event class
 make:middleware      Create a new middleware class
 make:migration       Create a new migration file
 make:model           Create a new Eloquent model class
 make:provider        Create a new service provider class
 make:request         Create a new form request class
migrate
 migrate:install      Create the migration repository
 migrate:refresh      Reset and re-run all migrations
 migrate:reset        Rollback all database migrations
 migrate:rollback     Rollback the last database migration
 migrate:status       Show a list of migrations up/down
queue
 queue:failed         List all of the failed queue jobs
 queue:failed-table   Create a migration for the failed queue jobs database table
 queue:flush          Flush all of the failed queue jobs
 queue:forget         Delete a failed queue job
 queue:listen         Listen to a given queue
 queue:restart        Restart queue worker daemons after their current job
 queue:retry          Retry a failed queue job
 queue:subscribe      Subscribe a URL to an Iron.io push queue
 queue:table          Create a migration for the queue jobs database table
 queue:work           Process the next job on a queue
route
 route:cache          Create a route cache file for faster route registration
 route:clear          Remove the route cache file
 route:list           List all registered routes
schedule
 schedule:run         Run the scheduled commands
session
 session:table        Create a migration for the session database table
vendor
 vendor:publish       Publish any publishable assets from vendor packages

とか出ればOK。コマンド眺めるだけでも充実してるのがわかる。 artisanはアルチザンと読むらしい。ちなみにlaravelはララベル。

ついでにサーバも立ち上げてみる。

$ php artisan serve
Laravel development server started on http://localhost:8000

昔はphp artisan serveとかあったらしいけど何故か消えたらしい。まあ別にphpコマンドで十分だから必要ないのかもしれない。

アプリ名の設定

php artisan app:name MyBlog

app以下のnamespaceが変わるだけっぽい。 ただしこのままだとcomposerの設定が残ってて動かないので、php composer.phar updateを一発叩く必要がある。

その他の設定

環境によってはstorageに書き込み権限が必要。 面倒だったらHomesteadというVagrant boxを使うと初期設定を全部スキップできる。

ルーティング

app/Http/routes.phpが最初に読まれるファイルになる。 ここに処理を直接書くこともできる。マイクロフレームワークっぽい。

// app/Http/routes.php

Route::get('/hello', function()
{
    return 'Hello World';
});

これでhttp://localhost:8000/helloにアクセスするとHello Worldと表示される。

ルーティング(コントローラー)

まあ正直これだとだるいので、コントローラーにも書ける。どっちも選べるのは良い。 routes.phpを見た時点で気がついているだろうけど、ルーティングに文字列を入れるとコントローラー指定になる。

// app/Http/routes.php
Route::get('/', 'WelcomeController@index');

でもこれでも個別で指定するの面倒なので

// app/Http/routes.php
Route::controller('article', 'ArticleController');

とかでコントローラーまとめて指定できる。 この場合、URLとメソッドの対応をどのようにすればいいのかと言うと、[リクエストメソッドの小文字][URLパラメータの頭大文字]という指定でできる。つまり/article/indexは、public function getIndex()となる。このへんリフレクションでやってるっぽいので、ちょっと遅いかもしれない。Route::get()のような指定方法のほうが高速に見える。コントローラー使わないのが不便だけど最速っぽい。 ちなみにいわゆるsnake_caseCamelCase変換ではなく、url-patternFunction_nameという変換が入る。ちょっと変な感じ。function getShow_user()みたいな変なメソッド名になる。 Controllerの書き方は他のやつをコピペするか、この後のCRUD自動生成で生成でも良い。 laravel-annotationsを入れるとアノテーションでも書ける。

CRUD自動生成

昔懐かしのscaffoldみたいなやつ。

php artisan make:controller ArticleController

で、routes.php

// app/Http/routes.php
Route::resource('article', 'ArticleController');

とすると、indexとかstoreとかshowとかってメソッドにすぐアクセスできるようになる。 昔懐かしのscaffoldと違うのは、中身は用意してくれないので、自分で書く必要がある。まあ管理画面作るくらいなら便利なんじゃないのって感じ。余計なお世話はしてくれない。

ビュー(テンプレート)

ビューはresources/views/に入れる。なんでControllerと同階層じゃなくてトップレベルなの?という疑問が残るが、それがいいと思ったのだろう。 テンプレートエンジンはBladeと呼ばれるLaravel用エンジンが使われる。{{ $hoge }}で変数を出力して、条件式などは@から始まる書式を使う。拡張子.blade.php。末尾phpである必要あるのかな?

// app/Http/Controllers/ArticleController.php

class ArticleController extends Controller {
     public function getIndex()
     {
        return view('article.index', ['hoge’=><fuga>']);
     }

とか作って

<!— resources/views/article/index.blade.php —>
@extends('app')

@section('content')
article index {{ $hoge }}
@endsection

で表示ができる。 ちなみに昔は{{ }}はエスケープせずに表示で、エスケープする場合は{{{ }}}だったらしいが、5からはどちらもエスケープするようになり、エスケープしない場合は{!! !!}という書式を使う必要がある。

モデル

モデルはデフォルトでapp/直下に置くらしい。うーん。 とりあえずここではそのままapp/直下に置くが、autoloadされるディレクトリであればどこでもいいらしいので、必要であれば変えれば良い。 app/Article.phpを作る。

<?php namespace MyBlog;
// app/Article.php

use Illuminate\Database\Eloquent\Model;

class Article extends Model {
     protected $table = 'articles';
}

スキーマはここには書かず、database/migrations/に書く。このファイルはartisanコマンドで生成する。

$ php artisan make:migration --table=articles create_articles
Created Migration: 2015_02_05_120749_create_articles

中身を書く

// database/migrations/[日付とか]_create_articles.php

     /**
      * Run the migrations.
      *
      * @return void
      */
     public function up()
     {
          Schema::create('articles', function(Blueprint $table)
          {
            $table->increments('id');
            $table->string('title');
            $table->text('body');
            $table->dateTime('created_at');
            $table->timestamp('updated_at');
          });
     }

     /**
      * Reverse the migrations.
      *
      * @return void
      */
     public function down()
     {
          Schema::drop('articles');
     }

流す前にDBの設定をしておく。config/database.phpで設定する…と思いきや、ルートにある.envファイルを書き換えれば大抵は大丈夫なようになっている。.env.exampleをコピーして.envファイルを作り、DB_xxxを変更する。なおMySQLがデフォルト。 変更したらデータベースは事前に作っておいて、以下のコマンドを流す。

$ php artisan migrate:install
Migration table created successfully.

$ php artisan migrate:refresh
Nothing to rollback.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2015_02_05_120749_create_articles

こんな感じ。

とりあえずインタラクティブシェルで試す。

$ php artisan tinker
Psy Shell v0.3.3 (PHP 5.6.4 — cli) by Justin Hileman
>>> $articles = \MyBlog\Article::all();
=> <Illuminate\Database\Eloquent\Collection #00000000104d1a7300000001133ee921> {}
>>> $article = new \MyBlog\Article();
=> <MyBlog\Article #00000000104d1a7200000001133ee921> {
       incrementing: true,
       timestamps: true,
       exists: false,
       snakeAttributes: true,
       manyMethods: [
           "belongsToMany",
           "morphToMany",
           "morphedByMany"
       ]
   }
>>> $article->title = 'test';
=> "test"
>>> $article->body = 'test test';
=> "test test"
>>> $article->save();
=> true
>>> $articles = \MyBlog\Article::all();
=> <Illuminate\Database\Eloquent\Collection #00000000104d1a7000000001133ee921> {}
>>> $articles = \MyBlog\Article::all()[0];
=> <MyBlog\Article #00000000104d1a7800000001133ee921> {
       incrementing: true,
       timestamps: true,
       exists: true,
       snakeAttributes: true,
       manyMethods: [
           "belongsToMany",
           "morphToMany",
           "morphedByMany"
       ]
   }
>>> $articles = \MyBlog\Article::all()[0]->title;
=> "test"
>>> $articles = \MyBlog\Article::all()[0]->body;
=> "test test"
>>> \MyBlog\Article::all()[0]->delete();
=> true
>>>

画面に出してみる。

// app/Http/Controllers/ArticleController.php

     /**
      * Display a listing of the resource.
      *
      * @return Response
      */
     public function getIndex()
     {
        $articles = \MyBlog\Article::all();
        return view('article.index', ['articles'=>$articles]);
     }

     /**
      * Show the form for creating a new resource.
      *
      * @return Response
      */
     public function getCreate()
     {
        $article = new \MyBlog\Article();
        $article->title = 'test';
        $article->body = 'test test';
        $article->save();

        return redirect('article/index');
     }
<!— resources/views/article/index.blade.php —>
@extends('app')

@section('content')

@foreach ($articles as $article)
<section>
    <h3>{{ $article->title }}</h3>
    <p>{{ $article->body }}</p>
</section>
@endforeach

@endsection

これで/article/createを叩くと、延々とデータが増えるのを観察することができる。