API開発に便利なPHPフレームワーク(4)- Symfony –

PHPでウェブアプリケーションを開発する場合、フルスクラッチで書くことはほとんどなくなり、なんらかのフレームワークを使うことが一般的になってきました。
API開発ももちろんそうで、いろんなフレームワークでAPI開発を加速させる機能が実装されています。
今回は、PHPフレームワークの中で歴史も長く、PHPに多くのインパクトを与えてきたSymfonyを使ったAPI開発を見てみたいと思います。

目次

  1. PHPの発展に大きな貢献をしてきたSymfony
  2. Symfonyの導入方法
  3. SymfonyでHello World APIを作成してみる
  4. Doctrineを使ったDB接続
  5. 便利なコンポーネントをかいつまんで紹介
  6. まとめ

PHPの発展に大きな貢献をしてきたSymfony

Symfonyは2007年1月にバージョン1.0がリリースされ、今年で12年目(奇しくも私が所属している株式会社ビヨンドと同じ)という、古参の部類に入るPHPフレームワークです。
全体的にしっかり作られていて、規模としては中規模~大規模なシステムと相性が良いように思います。
今はLaravelなどの人気に押されていますが、とりわけ大規模なプロジェクトになるとSymfonyが採用されることが多いように思います。

Symfonyでの開発におけるメリットをLaravelと比較してみると、初期開発の進捗はLaravelの方に軍配が上がると思います。
しかしながらSymfonyは、テストツールが標準で揃っていることや規約を優先するよう設計されているため、規約に沿って複数人で(それも多い人数で)開発を行った場合、規約を知っていればコードの読み書きがしやすく、継続的なメンテナンスがしやすい、といったメリットがあると思います。

その長い歴史の中で開発されたコンポーネント群の成熟度は高く、さまざまなプロジェクトやツールの中でも活用されています。

たとえば、PHPフレームワークで今一番勢いがあるLaravelもその内部では、Symfonyのコンポーネントが多数使われています。

また、サイトのスクレイピングをするGoutteというライブラリがありますが、蓋を開けてみるとSymfonyコンポーネントの塊のようなライブラリです。サイトへのアクセスを行う部分はBrowserKitというコンポーネントを拡張して作られていて、取得したサイトデータのパーサーには、DomCrawlerという別のコンポーネントが利用されています。
Laravelも、Symfonyをベースに作られたフレームワークなので、Symfonyの開発で培ったノウハウは流用できる部分もありますし、その逆も然りです。

Symfonyは、全体として一つの大きなフレームワークですが、内部的にはたくさんのコンポーネントの集まりで構成されるよう設計されています。あるコンポーネントだけを取り出して他のPHPフレームワーク上で利用することが簡単にできるようになっています。

また、Symfonyを使って開発をしていると、こういったコンポーネントに触れる機会は多くなります。Symfony以外のフレームワークを使って開発をすることになっても、外部コンポーネントという形でSymfonyのコンポーネントを使って開発効率を上げることが可能ですし、しっかりテストされたコンポーネントを使うことで品質向上にも一役買うことでしょう。

逆に面白いのが、フレームワークで大きなウェイトを占めるORM(※)は自前で開発しておらず、DoctrinePropelといったサードパーティ製のORMに依存しているという点です。
これもSymfonyがうまくコンポーネント化されていて疎結合に構成されていることを示すポイントなのかなと思います。

ここまでがすべてOSSとして提供されているSymfony自体を開発で使わなくても、「知っていて損はないフレームワーク」とも言えますね。

(※)ORM : オブジェクト関係マッピングのことで、データベースから取り出したデータをオブジェクト指向のプログラミングの中でも扱いやすくするライブラリのこと。

Symfonyの導入方法

SymfonyはapacheやNginxなどのwebサーバーで動作させるために特別な設定を必要としません。
PCやサーバーにPHPがインストールされていれば、開発を始めることが可能です。
また、composerを経由して必要なものがすべて手に入りますので導入も非常に簡単です。

インストール

以下のコマンドを使って、composer経由でSymfonyの本体をインストールします。
便利なスケルトンが用意されているので、今回はこちらを使って進めていきたいと思います。

【bash】

composer create-project symfony/website-skeleton symfony-test

インストールが完了すると、インストールログの他にSymfonyのメッセージが表示されています。
その中に、PHPが持つ、簡易サーバーを起動して動作確認をするコマンドがありますので、今回はこちらで動作確認をしてみます。

【bash】

cd symfony-test
php -S 127.0.0.1:8000 -t public

「Press Ctrl-C to quit.」というメッセージが出ていれば、簡易サーバーの起動ができているので、ブラウザから「127.0.0.1:8000」にアクセスしてみます。
「Welcome to Symfony [バージョン番号]」という画面が表示されていれば、セットアップは完了しています。

SymfonyでHello World APIを作成してみる

セットアップが完了したら、せっかくなのでアクセスすると、Hello Worldと返す簡単なAPIを作ってみたいと思います。

Symfonyには、コンポーネントの他にバンドルという仕組みがあり、バンドルは何かの目的を果たすためのプログラムの集合体で、PHPだけではなく、HTMLやCSS、JavaScriptなどもバンドルとして一つにまとめ、再配布することができる仕組みになっています。

Symfonyのバンドルは、管理画面を手早く作るためのバンドルや画像編集機能を追加するバンドルなど用途によっていろいろあり、もちろんAPIを手早く作るためのバンドルも存在します。

今回はFOSRestBundleというバンドルを使ってHello World APIを作ってみたいと思います。また、APIが返すデータはJSON形式にしておくことが最近では多いので、JMSSerializerBundleというバンドルも導入してJSONへのエンコードをさせるようにします。

FOSRestBundleとJMSSerializerBundleの導入は非常に簡単で、composerを使ってインストールします。

【bash】

composer require friendsofsymfony/rest-bundle
composer require jms/serializer-bundle

インストールが終わったら、バンドルの導入は完了です。
過去のバージョンでは、ここからAppKernel.phpというファイル内にある配列に手動で登録する必要がありましたが、私が試した4.1のバージョンでは、config/bundles.phpという設定ファイルに自動的に登録されるようになっています。

bundles.phpでは、デバッグ用のバンドルなどは開発環境でのみ有効にしたり、細かい設定ができるようになっていて、便利ですね。

バンドルも揃ったところで、さっそくコントローラーから実装していきたいと思います。
エンドポイントは「/api/hello/world」となるように実装を進めていきます。

まずは、FOSRestBundleの設定を行います。
FOSRestBundleの設定ファイルは、config/packages/fos_rest.yaml です。

【yaml】

# config/packages/fos_rest.yaml
fos_rest:
  routing_loader:
    default_format: json
  # body_listener: true
  view:
    view_response_listener: force
  format_listener:
    rules:
      - { path: '^/api', priorities: ['json'], fallback_format: json, prefer_extension: false }

このyamlファイルでは以下の設定を行っています。

  • routing_loader の項目では、デフォルトのレスポンスフォーマットを指定しています。
  • view.view_response_listener でforceを設定しておくと、後にいいことがあります。詳しくは後ほど。
  • format_listener.rules でフォーマットを実行するルールをパスから設定することができます。この例では、「/api」から始まるURIの場合にレスポンスフォーマットをJSONとすると指定しています。

続いて、ルーターの設定を行います。

【yaml】

# config/routes.yaml
api_hello_world:
  path: /api/hello/world
  controller: App\Controller\HelloController::getWorldAction

ルーティングに関しては、「api_hello_world」はルーティングの設定名で理解できれば何でもいいと思いますが、プロジェクト内で命名規則は統一した方が良いと思います。

いろんな指定の仕方ができるのですが、今回は簡単に、/api/hello/worldというURIにアクセスされたら、「App\Controller\HelloController」クラスの「getWorldAction」メソッドで処理を行うようルーティングしました。
クラス名はネームスペースベースで指定をします。

今回はControllerディレクトリに入れてある単純なコントローラークラスでしたが、バンドル内のコントローラーなども指定可能です。

コントローラーファイルはsrc/Controllerというパス配下にファイルを置きます。

【php】

# src/Controller/HelloController.php
<?php
namespace App\Controller;
use FOS\RestBundle\Controller\FOSRestController;
class HelloController extends FOSRestController
{
    public function getWorldAction()
    {
        $data['message'] = 'hello world!';
        return $data;
    }
}

ここではメソッド内部で生成した配列を返すだけの簡単なメソッドです。
通常、コントローラーメソッドは戻り値がResponseオブジェクトでないといけない決まりがありますが、fos_rest.yaml で記述した「view_response_listener: force」のおかげで最終的な戻り値が自動的にResponseオブジェクトに変換されるため、開発者が逐次Responseオブジェクトでラップする必要はありません。
こうすることで、コントローラー内の見通しが良くなり、可読性も向上します。

以上で仕込みは終わったので、/api/hello/world にアクセスしてみます。

【json】

{
    "message": "hello world!"
}

このようなデータが返ってきたら成功です。

Doctrineを使ったDB接続

Symfonyでは、PropelとDoctineの2つのORMを採用していますが、今回はDoctrineを使ったデータベース接続を試してみます。

接続対象のデータベースとテーブルがないので、サンプルとして以下のようなテーブルを用意しました(アメリカ人にありがちな名前男女トップ10のデータです)。

【sql】

CREATE TABLE `names` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `sex` varchar(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

【sql】

INSERT INTO NAMES (`name`, `sex`) VALUES ("Noah", "male"),("Liam", "male"),("Mason", "male"),("Jacob", "male"),("William", "male"),("Ethan", "male"),("James", "male"),("Alexander", "male"),("Michael", "male"),("Benjamin", "male"),("Emma", "female"),("Olivia", "female"),("Sophia", "female"),("Ava", "female"),("Isabella", "female"),("Mia", "female"),("Abigail", "female"),("Emily", "female"),("Charlotte", "female"),("Harper", "female");

DBへの接続情報ですが、プロジェクトルートに存在する「.env」というファイルで設定します。
「###> doctrine/doctrine-bundle ###」という項目がありますので、「DATABASE_URL=」の後ろの部分を適宜打ち替えてください。
ベーシック認証付きのサイトにアクセスする際のURIのような記法なので、こちらも理解しやすいです。

ここまで終わったら事前の設定は完了しているので、namesテーブルに対するEntityクラスをconsoleコマンドから作成します。

【bash】

php bin/console make:entity
 Class name of the entity to create or update (e.g. OrangeElephant):
 > names
 created: src/Entity/Names.php
 created: src/Repository/NamesRepository.php
 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.
 New property name (press <return> to stop adding fields):
 > name
 Field type (enter ? to see all types) [string]:
 >
 Field length [255]:
 > 32
 Can this field be null in the database (nullable) (yes/no) [no]:
 >
 updated: src/Entity/Names.php
 Add another property? Enter the property name (or press <return> to stop adding fields):
 > sex
 Field type (enter ? to see all types) [string]:
 >
 Field length [255]:
 > 6
 Can this field be null in the database (nullable) (yes/no) [no]:
 >
 updated: src/Entity/Names.php
 Add another property? Enter the property name (or press <return> to stop adding fields):
 >
  Success!

console make:entityコマンドは対話型のEntity作成ツールで使いやすいですね。
入力が終わると、各カラムのgetter/setterメソッドが用意されたApp\Entity\Namesクラスが作成されます。

注意点として、DBのテーブル名は複数形、モデルファイルは単数形のような命名規則を持つフレームワークが多い中、Doctrineは「テーブル名 = モデルファイル名」となる点です。

先程作成したHello World APIを、namesテーブルからランダムに名前を取ってきて出力するAPIに書き換えてみます。

【php】

<?php
namespace App\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use App\Entity\Names;
class HelloController extends FOSRestController
{
    public function getWorldAction()
    {
        $message = 'hello %s!';
        // レコード数は今回決め打ちしちゃっています
        $id = mt_rand(1, 20);
        $name = $this->getDoctrine()
            ->getRepository(Names::class)
            ->find($id);
        $data['message'] = sprintf($message, $name->getName());
        return $data;
    }
}

Entityはその名の通りテーブルの構成を示し、データの入れ物に徹し、実際のデータベースとのやり取りに利用するメソッド(findなど)は、Repositoryクラスに存在しています。
あまり深く考えなくてもID指定のデータ参照は簡単に実装できるので手軽ですね。

/api/hello/world にアクセスしてみます。

【json】

{
    "message": "hello Emma!"
}

このようなデータが返ってきたら成功です。

便利なコンポーネントをかいつまんで紹介

最後に、Symfonyプロジェクトで開発されているコンポーネントで、外部でもよく使われているコンポーネントを紹介します。

BrowserKitコンポーネント

PHPプログラムからブラウザの挙動をシミュレートするためのコンポーネントです。クローラーを作る場合、Goutteを採用するか、少し細かい調整が必要な場合はBrowserKitを利用するというのが一番の近道ではないでしょうか。

この場合、次に紹介するDomCrawlerも合わせて利用すると、かなり開発が楽になります。

DomCrawlerコンポーネント

クローラーで取得したHTMLのDOMを解析し、アクセスしやすいメソッドを提供してくれるコンポーネントです。

XPathやjQueryっぽい方法を提供してくれますので、フロントエンドメインの開発者でも気軽に扱えると思います。また、HTMLやXMLの操作全般の機能を持っているので、出力するHTMLを作るといった用途にも使えます。

Vardumperコンポーネント

Vardumperはデバッグをより効率よく使いやすい形で提供してくれるコンポーネントです。
var_dump()はよく使う割にメソッド名が長い、アンダースコアが邪魔といった問題(慣れの問題かも知れませんが)を抱えているので、単純にそれを解決してくれるdump()メソッドを提供してくれます。

オブジェクトを内容もしっかり表示してくれるので、開発時には大いに役に立つと思います。

Consoleコンポーネント

ターミナルなどで接続した後、コマンドライン上で実行するCLIツールを作成する際に便利な機能を提供してくれるコンポーネントです。

サーバー管理などであれば、bashなどのシェルスクリプトでツールを作ることが多いですが、コマンドラインからDBの操作を伴う処理を書いたりする時は、既存の処理を一部再利用してツールの作成ができるので便利に使えます。

まとめ

今回は、PHPフレームワークでも歴史の長い、Symfonyを紹介しました。

再利用による開発効率アップが根底にあり、フレームワーク自体がコンポーネントの塊でできている構成は他ではあまり見ない形でした。また、全体的に疎結合ゆえ、Symfony以外のフレームワークでSymfonyコンポーネントを利用することもできるのも面白く、知らず知らずのうちにSymfonyの恩恵を受けているということもあるでしょう。

ORMが独自のものではないところなど、Symfony自体も外部の優秀なライブラリをコンポーネントやバンドルといった形で再利用していて、オープンソースの使い方も勉強になります。

フレームワーク自体も良くできていて、使い方を覚えて損はないSymfony、これを機会に使ってみてはいかがでしょうか?

次回は、日本では少し下火になってきましたがまだまだ使っている人が多いCakePHPを取り上げます。

  1. API開発に便利なPHPフレームワーク(1)- Laravel –
  2. API開発に便利なPHPフレームワーク(2)- Phalcon –
  3. API開発に便利なPHPフレームワーク(3)- FuelPHP –
  4. API開発に便利なPHPフレームワーク(4)- Symfony –
  5. API開発に便利なPHPフレームワーク(5)- CakePHP –
  6. API開発に便利なPHPフレームワークを比較 超主観的にランキングしてみた