あざらし備忘録。

音ゲー好きなウェッブエンジニアがいろいろ思った事やった事を書いていくブログです

2.7から始めるSymfony2[PHP][Symfony]

Symfony2のLTSである、Symfony2.7がちょっと前にリリースされました。

LTSということで、Symfony2.7は3年間はサポートするよ!っていうなかなか安定感のあるバージョンになっています。

The Release Process (Contributing to Symfony)

サポート期間が長いのでギョーミーな部分にもSymfony2を導入するハードルも下がるしSymfony2を新規で導入することも増えるんじゃないかなぁと思います。

ということで今回は「これからSymfony2を始めるには」といったところの記事を書こうかなと思います!

「今まで使ったことないけど、試してみようかな?」的なところの助けになれば幸いです。

今回はSymfony2の導入から簡単な構造の説明までをできたらと思います。

インストーラーの導入

Symfony2は、2015/03/26から、新しいインストーラーが導入されて、以前よりも簡単に便利にSymfony2をインストールできるようになりました。

なのでこれからはこのインストーラーを使ってサクサクSymfony2プロダクトを始めるのが良いでしょう!

まず以下の様にしてインストーラーを入れます。

LinuxMac OS Xだったら

sudo curl -LsS http://symfony.com/installer -o /usr/local/bin/symfony
sudo chmod a+x /usr/local/bin/symfony

Windowsだったら

c:\> php -r "readfile('http://symfony.com/installer');" > symfony

このようにするだけです:)

なお、ちょっとWindowsが手元にないのでここからはLinux/Mac OS Xベースで書いていきますmm

プロジェクトの作成

インストーラーがサクッとインストールできたので、次は早速プロジェクトを作成しましょう。

symfony new <プロジェクト名> 2.7

この様にコマンドを実行すると、よしなにSymfony2.7で動くプロジェクトを作成してくれます:) なお、2.7 の部分はお察しの通りSymfony2のバージョンを表していて、任意のバージョンのSymfony2をインストール可能です。2.6でも、2.5でも、好きなように入れることができます。

また、上記の例ではSymfony2.7の中でも最新バージョンを入れてくれますが、2.7.1 のように詳細までバージョン指定することでその指定したバージョンを入れることもできます。

逆にバージョン指定をしないでコマンド実行をするとその時の最新バージョンで動くプロジェクトが作成できます。

さて、コマンドを実行すると以下のように表示されるかと思います。

 Downloading Symfony...

    4.95 MB/4.95 MB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

 Preparing project...

 ✔  Symfony 2.7.1 was successfully installed. Now you can:

    * Change your current directory to /Users/g-nakanishi/hogehoge_symfony

    * Configure your application in app/config/parameters.yml file.

    * Run your application:
        1. Execute the php app/console server:run command.
        2. Browse to the http://localhost:8000 URL.

    * Read the documentation at http://symfony.com/doc

これでプロジェクト作成は完了です。いやはや簡単。

また、Symfony2にはその環境でちゃんとSymfony2が動作するかどうかのチェックスクリプトが同梱されていて、そのスクリプトを実行すれば簡単に動作をするかも確認できます。

php <プロジェクト名>/app/check.php

こちらを実行するとphp.iniを見つつ動作チェックを行ってくれます。

プロジェクトの動作確認

もう実はバッチリSymfony2は動くようになってくれています!

確認してみましょう。

プロジェクトに入って、サーバーを起動してみます。

cd <プロジェクト名>
php app/console server:run

Server running on http://127.0.0.1:8000                                                         
Quit the server with CONTROL-C.         

サーバー起動まで行えたら、ブラウザでhttp://127.0.0.1:8000/app/exampleにアクセスしてみます。

これでHomepage.のように画面上に表示されていればSymfony2はしっかり仕事をしてくれています。

プロジェクトの構造

Symfony2プロジェクトは、以下の様な構成になっています。

├─ app/
│  ├─ console
│  ├─ cache/
│  ├─ config/
│  ├─ logs/
│  └─ Resources/
├─ bin/
├─ src/
│  └─ AppBundle/
├─ vendor/
└─ web/

app

アプリケーション固有の設定や、アプリケーションの核になるファイル、キャッシュやログの格納されるディレクトリなどが詰まったディレクトリです。

console

Symfony2が提供しているコマンド群を実行するためのスクリプトです。

先ほど実行したphp app/console server:runのようにして使用します。

また、自作したコマンドをこのconsole経由で実行可能にすることもできます。

cache

キャッシュディレクトリです。Symfony2は色々な部分でキャッシュを行って高速化を行っていますが、そのキャッシュ群はこのディレクトリに格納されます。

config

アプリケーション固有の設定は、このconfigディレクトリ下に格納されます。 データベースの設定であったり、ロガーの設定であったり、ルーティングの設定であったり、各種様々な設定は軒並みこちらのディレクトリに集まります。

logs

ログが集まるディレクトリです。(特になんも言えなかった)

Resources

主にアプリケーション全体に共通となるhtml等を置くディレクトリです。

bin

実行可能な便利コマンド群を置くディレクトリです。 シンボリックリンクを置いておいたりもできます。

src

ソースコードが置かれる部分です。

Symfony2は全てがBundleという単位で管理されているため、そのBundle群がこのsrc下に集まっています。

Bundleというのはプラグインに近いような概念で、自分たちで書いていくアプリケーションコードに関してもBundleという形で作成していきます。

web

俗に言うpublicなディレクトリで、ここにはブラウザ等からアクセス可能なものが配置されます。

vendor

外部ベンダーの作成したライブラリ等が格納されます。

git等のバージョン管理時にはignoreしておいたら良いかと思います。

終わりに

今回はとりあえず「これくらいカジュアルに始められるよ!」というのを伝えたくて本当に導入部だけ書いてみました。

Symfony2、PHPでアプリを書くなら一択くらいには気に入っているので、ぜひ使う人口が増えるとうれしいです。

次やる気がでれば導入後の入門記事とかも書きたいな〜

AASMライクなPHP用StateMachine 1.1.0 released!![PHP][Doctrine]

以前このブログでも紹介した、

shiro-goma.hatenablog.com

PHP用のステートマシンライブラリのv1.1.0を公開しました!

Rubyの有名なステートマシンgemであるaasm/aasm · GitHubぽいカジュアルに使えるステートマシンが欲しくて開発しています。

v1.0.0ではステートマシンとしての最小限の機能である状態遷移を管理する機能のみを提供していましたが、v1.1.0では以下のような機能を追加しました。

  • 間違った遷移をしようとした時に例外を投げるのではなくfalseを返すようにするオプションの実装(WhinyTransitions)
  • 直接代入による状態遷移を向こうにするオプションの実装(NoDirectAssignment)
  • コールバックメソッドの有効化(Callbacks)
    • イベント開始前(beforeEvent)
    • 旧状態退出前(beforeExit)
    • 旧状態退出時(exit)
    • 新状態入場前(beforeEnter)
    • 新状態入場時(enter)
    • 遷移後(afterTransition)
    • 旧状態退出後(afterExit)
    • 新状態入場後(afterEnter)
    • イベント終了後(afterEvent)
  • 引数を伴うコールバックメソッドへの対応
  • 状態名取得メソッドの実装

これで色々なフックポイントでコールバックメソッドを実行できるようになったので、痒いところに手が届きやすくなったかなーと思っています!

また、状態名を取得するメソッドも用意出来たので、これで定数などで状態名を保持するような二重管理っぽい状況も打破できそうです:)

リポジトリは以下です。

github.com

設定ファイル不要でアノテーションだけで状態管理できるようになるのでお手軽ですよー! また、Symfony2でお馴染みのDoctrineが提供しているアノテーション機能を用いて作成していますが、Symfony2で無くてもプレーンなPHPに対して使用可能になっているのでぜひどうぞ。

せっかく作ったライブラリなので普通に使えるようにしっかり育て続けていきたいと思います!

Macの環境構築を通じて0からAnsibleに触れる[Ansible][Mac]

こんにちは。

今回は、以前@t-wadaさんが書かれていた

t-wada.hatenablog.jp

を読んで「めっちゃ良いじゃないですか」と思ったので実際に自環境もやってみました。

上記エントリをはじめ、リンクとして書かれていた@hnakamurさんやmawatariさんのブログも参考にさせて頂きましたmm

ありがとうございますmm

AnsibleでHomebrew, Cask, Atomエディターのパッケージを管理する - Qiita

HomebrewとAnsibleでMacの開発環境構築を自動化する | mawatari.jp

Ansibleも触ったことがないのでちょうどいいやということで。 また、今までHomebrew Caskも使ってみた事がなかったのでちょうどいいやということで。

なのでそのあたりの説明等も自分なりに書きつつまとめたいと思います。

基本は@t-wadaさんのブログを見ながら進めてみるということで、以下作業メモ程度に残しておきます。

Ansibleの構成

Ansibleは構成管理ツールであるということはわかっているのですがなんせ初心者なので、まずはAnsibleの構成から。

Ansibleは以下の3つによって主に構成されています。

  • インベントリ(hosts)
    • 作業対象のホストを記述します。
    • 今回はローカル環境で動かすだけなのでlocalhostとなります。
  • モジュール
    • 実際の具体的な実行内容を記述します。
    • 例えばファイルをコピーしたり、コマンドを実行したり、と言った部分です。
  • playbook
    • 前述のモジュール群を用いた処理を記述します。
    • このplaybookがプロビジョニングの内容となっていきます。

モジュールは標準で用意されてるものや、その他公開されているもので足りればそれらを用いれば良く、なければ自作すれば良い、といった感じですね。

サーバーで実行可能なのであればモジュールの言語自体は問わないようです。(てっきりPythonで書かなきゃいけないもんだと思っていた)

また、playbookを用意しなくてもansibleコマンドで実行も可能なようですが、今回はおそらく入れる物量も多くなりますしplaybookをサッと書こうと思います。

playbookを用いたプロビジョニングの実行は以下のコマンドになります。

ansible-playbook <playbook.yml>

これでざっくり概要のまとめは完了したので、プロビジョニングの準備を行っていきます。

プロビジョニング準備

t-wadaさんのブログに習って進めて行きます。

ディレクトリ作成

作業用のディレクトリを掘る。

mkdir .macbook-provisioning
cd .macbook-provisioning

ansibleのために必要な物を用意

先に述べた

  • インベントリ(hosts)
  • モジュール
  • playbook

を準備していく。

インベントリ(hosts)の用意

今回はローカルの環境構築のみなのでlocalhostとだけ。

echo 'localhost' > hosts

モジュールの用意

いい感じのがあったら使う、なければ作る、といったところですが、今回は

があるので、こちらを使いました。

playbookの用意

@t-wadaさんの記事を存分に参考にさせていただきつつ、記述していたものの意味をまとめてみました。

他にも色々と設定もできるようですが、まずは第一歩ということで...

# hosts
#
# hostsファイルに記述したホスト情報の中からプロビジョニング対象とするホストを選択する
# hosts: プロビジョニング対象ホスト名
# hosts: host1,host2,host3
# hosts:
#   - host1
#   - host2
#   - host3
- hosts: localhost
  # connection
  #
  # connection: local とすることでlocalhostの場合必要のないSSHをしないようにできる
  # [http://docs.ansible.com/playbooks_delegation.html#local-playbooks]
  connection: local
  # gather_facts
  #
  # 対象サーバーから情報を収集するかどうか
  # noとすれば取得しない。デフォルトはyes
  # 集めた情報は配列形式となっていて、playbook内で使用可能
  gather_facts: no
  # sudo
  #
  # sudoで実行するかどうか
  # noならばしないし、yesならばrootで実行する
  # ユーザ名を指定するとそのユーザで実行する
  sudo: no
  # vars
  #
  # タスク実行の際に必要な変数を定義しておける
  vars:
   # homebrew_taps
   # 
   # brew tapする必要がある場合はこの項目に記述しておく
    homebrew_taps:
      - hoge/hogehoge
    # homebrew_packages
    #
    # brew installでインストールするものをこの項目に記述しておく
    # [http://docs.ansible.com/homebrew_module.html]
    # [http://kiririmode.hatenablog.jp/entry/20150101/1420074034]
    homebrew_packages:
      - foo
      - { name: fuga, state: linked, install_options: force }
    # homebrew_cask_packages
    #
    # brew cask installでインストールするものをこの項目に記述しておく
    # [http://docs.ansible.com/homebrew_cask_module.html]
    homebrew_cask_packages:
      - bar
      - { name: piyo }
  # tasks
  #
  # ここに書いたタスクが上から順番に実行される
  # もし途中から実行したい場合は
  # ansible-playbook localhost.yml --start-at-task="タスク名"
  # ステップ実行したい場合は
  # ansible-playbook localhost.yml --step
  tasks:
   # name:
   # 
   # タスク名を記述
    - name: homebrew の tap リポジトリを追加
      # モジュール名: モジュールのオプション
      # 
      # このタスクでどのモジュールを実行するかとどんなオプションで実行するかを指定
      # with_itemsでforeachっぽく処理して順番にtapしている
      # [http://docs.ansible.com/homebrew_tap_module.html]
      homebrew_tap: tap={{ item }} state=present
      with_items: homebrew_taps
    - name: homebrew をアップデート
      homebrew: update_homebrew=yes

    # brew
    - name: brew パッケージをインストール
      homebrew: >
        name={{ item.name }}
        state={{ item.state | default('latest') }}
        install_options={{
          item.install_options | default() | join(',')
          if item.install_options is not string
          else item.install_options
        }}
      with_items: homebrew_packages
      register: brew_result
    - name: brew パッケージの情報保存先ディレクトリを作成
      file: path=brew_info state=directory
    - name: brew パッケージの情報を保存
      shell: brew info {{ item }} > brew_info/{{ item }}
      with_items: brew_result.results | selectattr('changed') | map(attribute='item') | map(attribute='name') | list

    # cask
    - name: homebrew-cask のインストール
      homebrew: name=brew-cask state=latest
    - name: cask パッケージをインストール
      homebrew_cask: name={{ item.name }} state={{ item.state|default('installed') }}
      with_items: homebrew_cask_packages
      register: cask_result
    - name: cask パッケージの情報保存先ディレクトリを作成
      file: path=cask_info state=directory
    - name: cask パッケージの情報を保存
      shell: brew cask info {{ item }} > cask_info/{{ item }}
      with_items: cask_result.results | selectattr('changed') | map(attribute='item') | map(attribute='name') | list
  # handlers
  #
  # playbookの終了時に実行される
  # 基本はタスクの記述と変わらない
  # サービスの再起動等に用いる事が多い
  # [https://docs.ansible.com/playbooks_intro.html#handlers-running-operations-on-change]
  # タスクにnotify: ハンドラー名 といった形で指定を加えるとそのタスクがトリガーとなってハンドラーが起動する
  handlers:

今回はbrewで入れるものの吟味や記述までは手が回らなかったので、時間を作ってコツコツ進めて行きたいですmm

ざっとですがAnsibleに触れられたので、良い体験になりました。 また、本当に構造がシンプルで書きやすくて良いですね!気に入りました!

次は現状chefで書いていたvagrantのプロビジョニングを試しにAnsibleで書いてみたりするのもいいかなぁ。

Composerでインストール出来るOSSを公開するまでにやったこと[Composer][PHP][Packagist][OSS初心者]

先日自身初のOSS的なものを出しました(番宣)

shiro-goma.hatenablog.com

github.com

Composerでインストール可能なライブラリとして出したので、出すまでの過程を備忘録も兼ねて書こうかなと思います。

作るきっかけ

  1. Rubyのgemであるaasm/aasm · GitHubに触れる
  2. イイナーと思って業務で用いているPHPで使えるステートマシンライブラリを探す
  3. あんまりお手軽さや、対象クラスとステートマシンとの一心同体感がない
  4. よし、AASMっぽいのつくろう

着想

  1. どうやったら一心同体感出せるかなー
  2. AASMみたいに設定ファイルとかなくて対象クラスに記述がしたら動く、みたいなのがいいなー
  3. Doctrineが簡単に使えるアノテーションを提供してたなー
  4. 一心同体感出したいからさも元から対象クラスに状態管理関連のメソッドが生えてましたよみたいな雰囲気にしたいなー
  5. traitでは
  6. @brtriverさんの出していたbrtriver/simple-crud-bundle · GitHubっぽく出来るのでは
  7. よし、やってみよう

作成後

ドキュメント書き

英語。brokenなのしか書けないけど頑張ってかかねば。

翻訳ツール

これを入れて頑張ってました。 だいぶ捗った。

chrome.google.com

英文とかをドラッグして選択すると即時日本語へ翻訳してくれてとても便利でした!

ドキュメントの内容

  • ライブラリ名(一行目)
  • 概要(Description)
  • デモ(Demo)
  • 他ライブラリとの対比(VS.)
  • 動作環境(Requirement)
  • インストール方法(Installation (via composer))
  • 使い方や機能の説明(Usage)
  • 今後実装していきたいこと(In future)
  • ご質問はこちら(Questions)
  • コントリビュート方法(Contribution)
  • ライセンス表記(Licence)
  • 作った人(Author)
  • 謝辞(Acknowledgement)

こんな感じのレールに乗ってつらつらと書いていきました。

バージョンに対するタグ付け

バージョンを指定してインストール出来るように、タグ付けは忘れずに。

今回のライブラリでは1.0.0スタートにしましたが、そこまで深い意味あいを持って付けたわけではなく、0.0.1スタート等でもいいとは思います。

ただ、「機能は少ないけど普通に使えるよ」という意味で、今回は1.0.0からにしました。

TravisCIによるテスト実行

Rubyが対象でしたが以前チュートリアルを書いたのでご参考まで。

shiro-goma.hatenablog.com

少ないですがバッヂも付けておきました。OSSっぽさ出るし←

travis.ymlで意識したこと

複数バージョンでテストが通ることの確認をするために、

  • php5.4
  • php5.5
  • php5.6

でのテストを実施。 普段はそんなに意識しないけれど大事になりますね。 実際、php5.4での動作を怠っていたのをテストで気づけてPHP5.4対応のコミットを直前で足したりしました。

ライセンス周り

あまり馴染みがないのであわあわしましたが、

d.hatena.ne.jp

のようなまとめを調べていったり、権利を主張するような気はないのと、使用しているライブラリや、文化圏の傾向などを追って行って、色々とライセンスを確認していった所、メジャーどころはMITライセンスが大多数を占めていたことから、今回はMITライセンスを採用しました。

今後も特段問題なければMITライセンスで出していくかなぁと思っています。

Composerでのインストール

今回はPHPのライブラリなので、Composerでインストールできるようにもしていきました。

composer.jsonの準備

あとで登録するPackagistでいい感じに表示するためにも、色々とcomposer.jsonに記載しました。

{
    "name": "gomachan46/state-machine",
    "description": "simple state machine with annotations for PHP, inspired by AASM known as a Ruby state machine.",
    "keywords": [
        "statemachine",
        "workflow",
        "state",
        "status",
        "annotation",
        "doctrine2",
        "event",
        "transition",
        "symfony",
        "bundle",
        "SM",
        "trait",
        "finite",
        "aasm"
    ],
    "require": {
        "php": ">=5.4.0",
        "doctrine/annotations": "~1.0",
        "doctrine/inflector": "~1.0"
    },
    "require-dev": {
        "phpunit/phpunit": "4.*"
    },
    "license": "MIT",
    "authors": [
        {
            "name": "Go Nakanishi",
            "email": "shiro.gomachan46@gmail.com"
        }
    ],
    "autoload": {
        "psr-4": {
            "": "src/",
            "StateMachine\\Tests\\": "tests/"
        }
    }
}

っていう感じに書いたりすると、以下のように表示されたり、検索時に引っかかってくれたりするようになるので書いておきました。 ただ今確認したらキーワード多すぎて切れてるな...w反省w

f:id:shiro_goma:20150417223644p:plain

Packagistへのリポジトリ登録

Composerでのインストールを簡単にするために、Packagistへ、作成したライブラリのリポジトリ登録を行いました。

gomachan46/state-machine - Packagist

Composerでのインストール時にリポジトリをわざわざ記載しなくても良いし、上記のようなページもできるし、タグ付けのようなこともできるので検索もされやすくなるし、いいこと付くめです:)

ぜひ登録しましょう!

Packagistにログイン

GitHubログインがあるので簡単にログインできます:)

リポジトリのURLを登録する

でかでかとSubmit Package というようなボタンがあるので、そこを押すと、リポジトリのURLを入力するフォームが表示されます。

GitHubのURLでもいいですし、GitのSSH clone URLとかでも大丈夫です。

登録はたったこれだけ!簡単です。

OSSで出してみて

一人でやってただけだけど、めっちゃ楽しかったので本当良かった。

ただ、一人でやってみたとはいえ、ギョームに組み込む、つまり「自分が使う側にまわる」というモチベーションはすごく大きくて、チームのメンバーからのフィードバックもすごくやる気につながったし、楽しさにつながったし、モチベーションの維持は大事感はありました。

OSSで出せるようにつくろう」としたことは色々と手間もあったけれど、可能性を広げる、世界を広げることができる非常に大きなチャンスなので、苦労してでも外に出せるような形にして出しきる、っていうのは非常に収穫が多い動きだなぁと体感できたのでこれからも積極的にやっていこうと思います:)

まだまだ拙いけど続けていったら磨かれそうな、少なくとも磨かれるまでの時間は短くなるだろうなという感覚を身をもって味わえたのは良かった!

とりあえず小さな1歩を踏み出せたので、世に出すための障壁が少し低くなりました。 何よりこれが一番の収穫です!

今回の記事を見て少しでもカジュアルに物を世に出せるようになった人が増えれば幸いです:)

簡単に状態遷移を管理できるStateMachineを公開しました[PHP][StateMachine][AASM][オートマトン]

StateMachineというPHP用のライブラリを公開しました!

github.com

ステートマシンとは?っていうところは以下のエントリをどうぞ。

shiro-goma.hatenablog.com

PHPで使えるコレ!っていうステートマシンがなかったので、トレイト+アノテーションという手法でステートマシンを作ってみました。

メリットは対象クラスとの距離が近いためステートマシンを用いたステータスの変更が対象クラスに即時反映されるところや、アノテーションによる記述のお陰で対象クラスで設定が完結するため非常にシンプルな作りになっているところかなと思います。

Doctrineを内部で用いていますがSymfony2でないと使えないライブラリではなく、素のPHPでも使えます!

構想としてはRubyのステートマシンの代表格であるAASMを大いに参考にしました。

とても便利なgemなのでぜひPHPでも使いたいなーと思って作った感じです。

詳しくはREADMEを御覧くださいませmm

初のライブラリ公開でしたが楽しかった!&引き続き機能作成していこっと。

実際に公開してみて、READMEやらタグやら英語やらなんやら色々と勉強になったので公開まで持って行って本当によかった。

PHPのステートマシンFiniteを触ってみた[PHP][Finite][StateMachine]

今回はステートマシンをPHPで扱えるFiniteというライブラリを触ってみたので備忘録として。

ステートマシンとはっていうところからFiniteの簡単な使い方までを軽くまとめてみようかなと思います。

ステートマシンとは

ステートマシンとは、すごく簡単に言うと状態遷移を管理するもので、ある状態Aからある状態Bへの遷移が可能かどうかを判定したり実際に遷移を実行したりします。

細かいところはステートマシンやオートマトンなどで調べると出てくるので興味があればmm

雄弁に語れるほどわかりきれてないので今の理解だと上記程度の感じですw

アプリケーションでステートマシンを用いるようにすると次のようなメリットがあります。

  • ステータスをもつオブジェクトの状態遷移の定義を閉じ込めておける
  • 定義通りにしか状態遷移できなくなるためフローが確立される
  • 定義に反した状態遷移を行おうとした際には例外等が起きることで適切に処理できる

状態遷移は往々にして管理が複雑になって、プロダクト内の色々な箇所で判定を都度都度行うようになったり、都度都度行うせいでバグの温床になったり、状態遷移のフローが変わった時には一斉に判定している部分を修正しなければならなかったりと、厄介な目に合うことが多いです。

例えば、ある作業を行うチケットの状態遷移を考えてみます。

f:id:shiro_goma:20150222041357p:plain

●が初期状態、◎を終了状態としています。

さっと考えてみただけでもこんな感じに状態遷移が出てきそうに思います。

他にもチケットをクローズ状態を追加したり、クローズからリオープンしたり、どんな状態からでも強制的に終了状態に持っていける却下状態を追加したり。考え方によっては色々な状態遷移フローが出来上がります。

「この状態だったり矢印だったりをいい感じに取り扱いたい!」そんなときに是非使っていきたいのがステートマシンです!

Finiteとは

上記で説明したステートマシンのPHP実装です。

Finite, A PHP5.3+ Finite State Machine | Finite

PHP5.3以上なら使えるので導入の敷居は低いと思います!

インストール

composerで入ります!

stableとしては1.0.3がタグ付けされていますが、現在開発中?のmasterは1.1になるようで、結構仕様が変わっていきそうなので(StateMachineのコンストラクタ引数が変わっていたり)、今回はdev-masterを指定してインストールしました。

"require": {
    "yohang/finite": "dev-master"
}

使い方

公式のサンプルコードに則って説明していこうと思います。

Finite/basic-graph.php at master · yohang/Finite · GitHub

<?php

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


/**
 * Class Document
 * 状態を持つオブジェクトのサンプルとしてドキュメントが使われています。
 * 管理したい状態を持つオブジェクトはFinite\StatefulInterfaceをimplementsします。
 *
 * StatefulInterfaceはgetFiniteState()とsetFiniteState()を実装することを要求しているので、
 * 状態管理したいプロパティをset、getできるようしておいてあげます。
 */
// Implement your document class
class Document implements Finite\StatefulInterface
{
    private $state;

    public function getFiniteState()
    {
        return $this->state;
    }

    public function setFiniteState($state)
    {
        $this->state = $state;
    }
}

// Configure your graph
$document     = new Document;

/**
 * ドキュメントインスタンスを渡してステートマシンのインスタンスを作成します。
 * この渡したものに対してステートマシンは状態管理を行います。
 */
$stateMachine = new Finite\StateMachine\StateMachine($document);

/**
 * ローダーを作成します。
 * ここで状態遷移のルールを決めていきます。
 * 下記の例だとドキュメントに対してのルールを定めていっています。
 * ローダーには以下の情報を設定することができます。
 *   * class
 *     * 設定したいクラス名をいれます。
 *   * states
 *     * 状態の設定をします。
 *     * type
 *       * 初期状態、通常状態、終了状態の3つから選択します。
 *       * デフォルトは通常状態です。
 *     * properties
 *       * 状態に持たせたい設定情報があれば、連想配列形式で自由に持たせる事ができます。
 *   * transitions
 *     * states間の遷移を設定します。
 *     * どの状態からどの状態には遷移可能、というのを記述します。
 *   *
 *
 * "draft"というstateは「初期状態であり、削除可能かつ編集可能」という設定になっています。
 */
$loader       = new Finite\Loader\ArrayLoader(array(
    'class'  => 'Document',
    'states'  => array(
        'draft' => array(
            'type'       => Finite\State\StateInterface::TYPE_INITIAL,
            'properties' => array('deletable' => true, 'editable' => true, 'comment' => '下書き'),
        ),
        'proposed' => array(
            'type'       => Finite\State\StateInterface::TYPE_NORMAL,
            'properties' => array('comment' => '提出済'),
        ),
        'accepted' => array(
            'type'       => Finite\State\StateInterface::TYPE_FINAL,
            'properties' => array('printable' => true),
        )
    ),
    'transitions' => array(
        'propose' => array('from' => array('draft'), 'to' => 'proposed'),
        'accept'  => array('from' => array('proposed'), 'to' => 'accepted'),
        'reject'  => array('from' => array('proposed'), 'to' => 'draft'),
    ),
));


/**
 * 上記で作成したローダーで、ステートマシンに設定をロードします。
 * これでステートマシンがコンストラクタ引数にて受け取ったインスタンスに対してどのように状態を管理していくかを
 * 理解することができるようになります。
 */
$loader->load($stateMachine);

/**
 * ステートマシンの初期化を行います。
 * ここでローダーのルールに従って状態をもつインスタンスの初期化を行います。
 * 同時に、ステートマシンも「現時点での状態」を知ります。
 * 上記の例だと初期化するとtypeがINITIALである"draft"がセットされることになります。
 */
$stateMachine->initialize();


// Working with workflow

// Current state

var_dump($stateMachine->getCurrentState()->getName()); // 初期状態なので"draft"が返ります。
/**
string(5) "draft"
 */
var_dump($stateMachine->getCurrentState()->getProperties()); // 初期状態のプロパティが返ります。
/**
array(2) {
["deletable"]=>
bool(true)
["editable"]=>
bool(true)
["comment"]=>
string(9) "下書き"
} */
var_dump($stateMachine->getCurrentState()->has('deletable')); // draftが'deletable'プロパティを持つかを返します。持っているのでtrue
/**
 * bool(true)
 */
var_dump($stateMachine->getCurrentState()->has('printable')); // draftが'printable'プロパティを持つかを返します。持っていないのでfalse
/**
 * bool(false)
 */

var_dump($stateMachine->getCurrentState()->get('comment')); // draftがもつ'comment'プロパティを返します。'下書き'が返ります
/**
 * string(9) "下書き"
 */

var_dump($stateMachine->getCurrentState()->get('printable')); // draftがもつ'printable'プロパティを返します。持っていない場合はNULL
/**
 * NULL
 */

// Available transitions
var_dump($stateMachine->getCurrentState()->getTransitions()); // draftの持っているtransitionsを返します。
/**
array(1) {
    [0]=>
  string(7) "propose"
}
 */

var_dump($stateMachine->can('propose')); // 'propose'トランジションを適用可能かを判定します。draftはproposeトランジションの開始状態なのでtrue
/**
 * bool(true)
 */

var_dump($stateMachine->can('accept')); // 'accept'トランジションを適用可能かを判定します。draftはproposeトランジションの開始状態ではないのでfalse
/**
 * bool(false)
 */

// Apply transitions
try {
    $stateMachine->apply('accept'); // 適用可能でないトランジションを適用しようとするとStateExceptionが投げられます。
} catch (\Finite\Exception\StateException $e) {
    echo $e->getMessage(), "\n";
    /**
     * The "accept" transition can not be applied to the "draft" state of object "Document" with graph "default".
     */
}

// Applying a transition
$stateMachine->apply('propose'); // 適用すると状態が遷移します。

var_dump($stateMachine->getCurrentState()->getName()); // 状態が遷移したのでdraftからproposedに変化しています。
/**
 * string(8) "proposed"
 */
var_dump($document->getFiniteState()); // 状態が遷移したのでdraftからproposedに変化しています。
/**
 * string(8) "proposed"
 */
var_dump($stateMachine->getCurrentState()->getProperties()); // 取得できるプロパティもproposedのものに変化しています。
/**
array(1) {
["comment"]=>
string(9) "提出済"
}
 */

この様に、現在の状態の情報を管理したり、管理対象の状態を変更可能かを判定したり、管理対象の状態を変更したり、といったことを上手くラップして操れるようになっています。

便利ですね!

今回は基本的な動かし方についてチュートリアルをやってみました。

他にも状態遷移の直前、直後にコールバックを噛ませて任意のコードを実行するように設定を加えることも可能だったりして、なかなか考えられているなぁと感じました。 あとは今回はfiniteStateというプロパティに強制されていましたが、任意の、しかも複数個の状態を管理しようとすることもできます。

また、DIコンテナを用いることも想定されていて、セットするひな形みたいなのも用意されています。 PimpleやSymfony2のサービスコンテナへのファクトリーの追加用のクラスもありました。

Finite/src/Finite/Factory at master · yohang/Finite · GitHub

ただ、Pimpleは1系想定のものに見えるので、2系対応にしたい!という場合等、ここにはないもので扱いたい場合はだとAbstractFactoryをextendsしたりFactoryInterfaceをimplementsしたりして対応するものを自作する流れになりそうです。

この辺りの応用編についても書く機会があったら書きたいなぁと思います!

とにかくはじめはライトに使うとしてもステートマシンはとても便利で手堅くアプリケーションを作ることができるようになると思うので、ぜひ使っていってみてください!

今回のブログ用に触ってみたリポジトリは以下になります。

gomachan46/finiteStudy · GitHub

Doctrineのバルクインサートはマルチインサートではない[Doctrine][Symfony2][ORM]

今回はDoctrineの注意点的な所の備忘録として。

Doctrine 2 Batch Processing — Doctrine Project

doctrineのバルクインサートはinsert文を1つにまとめられるわけではない

僕はバルクインサートといえば次のようなSQLを想像していました。

insert into (...) values (...), (...), (...), ...

あるテーブルに対して複数行インサートをしたいときに、insert文を並べなくてもインサートする値を並べていけばその個数分インサートするというやつです。

普通のインサートを並べるよりも非常に高速かつ問い合わせるのが1回で済むのでスマートです。

ってきりDoctrineでもこの動きができるんだーと簡単に思ってしまったのですが、よく読むとDoctrineの提供するbulk insertはこのような挙動ではないようなのです。

Doctrine曰く

Some people seem to be wondering why Doctrine does not use multi-inserts (insert into (...) values (...), (...), (...), ...

First of all, this syntax is only supported on mysql and newer postgresql versions. Secondly, there is no easy way to get hold of all the generated identifiers in such a multi-insert when using AUTO_INCREMENT or SERIAL and an ORM needs the identifiers for identity management of the objects. Lastly, insert performance is rarely the bottleneck of an ORM. Normal inserts are more than fast enough for most situations and if you really want to do fast bulk inserts, then a multi-insert is not the best way anyway, i.e. Postgres COPY or Mysql LOAD DATA INFILE are several orders of magnitude faster.

These are the reasons why it is not worth the effort to implement an abstraction that performs multi-inserts on mysql and postgresql in an ORM.

I hope that clears up some questionmarks.

僕が想像していたバルクインサートはマルチインサートという言葉で表現されていて、Doctrineはそうではないよと書かれています。

このマルチインサート記法はmysqlと新しいpostgresqlでしかサポートされていないから、その一歩抽象化レイヤーであるDoctrine ORMとしてはその形をサポートはしないと。

ORMとしては普通のinsertがボトルネックになることはまぁ稀(使い方が違う)で、どうしても早さを求めるinsertをしたいならマルチインサートよりもより高速なファイルロードによるインサートLOAD DATA INFILEをしましょうよと。

こんなような旨の記述がされています。

感想

確かにORMだしねっていう着地になるのですが、あまりじっくり読んでなかった僕は「おぉ、よく使うもんだからORMにあのバルクインサート機能つけてカジュアルにできるようになったのかー!責務はちょっと超えてる感あるけど便利さとトレードオフしたらありかも :)」と思ってしまいましたw

だから上で貼ったドキュメントの例でもbulk sizeが20なんていう小さいサイズのインサートサイクルだったのですね。bulk sizeを普通のマルチインサートっぽく大きくするとその分のinsertが走ってDBへのinsert攻撃みたいになってしまうのでw

バッチで即効性を求められることもないと思う(ないようにバッチを回すべき)ので、メモリ効率と速さのバランスを考えた時にぬるぬると時間はまぁ気にせず鈍行運転する、みたいなのをこのDoctrineの提供するbulk insert機能で実装したら良いのではないかなーと思いました。

どうしてもマルチインサートがしたければDBALレイヤーを使って直に書いてしまえばOKと思います:)

という自分的には腹落ちするところでおちついたので良かったです。

まとめ

  • Doctrineのbulk insertはよく見るマルチインサートではない
  • マルチインサートがしたければDBALを用いてSQLを書く
  • 大規模データを高速に処理したければマルチインサートではなくファイルでのインサートを行う

おわりに

もし僕のように俗に言うマルチインサートの感覚で使っていた方はご留意を! 大規模データの時に想像以上に時間がかかったり想像以上に負荷がかかったりしてしまうかもしれません。

解釈の間違ってる所とかあったら教えてくださるととても嬉しいですmm