📝

Symfony ExpressionLanguage #2 使用例 CSV の出力 (エクスポート)

2022/10/02に公開

PHP のフレームワークとして有名な Symfony には ExpressionLanguage というコンポート (ライブラリ) があります。

このコンポーネントの概要は Symfony ExpressionLanguage #1 概要と使い方 に書いていますので、そちらもご覧ください

とても面白く使い勝手のあるコンポーネントなのですが、あまり使用例が見当たらないので、僕が実際の案件で実装したものをアレンジして紹介したいと思います。

今回は ExpressionLanguage を使用して CSV を出力 (エクスポート) するシステムを実装していきます。

仕様

まずは仕様を考えてみましょう。

ExpressionLanguage を使用するときに僕がよくやるのは YAML (YAML Ain't a Markup Language) で構成ファイルを作成し、ExpressionLanguage を記述する手法 です。
(YAML は XML よりも読みやすく簡潔に書けるので、PHP では構成ファイルとしてよく使用されています。)

これには次のような利点があります。

  • 構成を PHP ではなく YAML にすることで不正な処理が書けなくなり、セキュアになる。
  • YAML を使うことで実際に動かすことができなくなるが、そこを ExpressionLanguage で補うことができる。

前提

今回はダミーデータとして顧客データを用意し、それを CSV で出力 (エクスポート) します。

顧客 (Customer) クラスは姓、名、生年月日、性別、有効・無効のプロパティを持っています。

出力 (エクスポート) する CSV のフォーマットのルールは以下のとおりです。

  • 姓と名をひとつ列に半角スペースでつなぐ必要がある。
  • 生年月日のフォーマットは YYYY年MM月DD 日にする。
  • 性別は「男性」、「女性」、「不明」のいずれかで出力する。
  • 有効・無効は「有効」、「無効」のいずれかで出力する。

構成ファイルを定義する

CSV のフォーマットを見ると何らかの加工が必要なのがわかります。

これらはもちろん PHP で直接記述することが可能ですが、出力のフォーマットが変わったり、フィールドが増えたりするたびにプログラムを改修するのは手間がかかります。

そこで ExpressionLanguage を使って、顧客データを加工する仕組みを実装したいと思います。

CSV を出力 (エクスポート) するにはどういった情報が必要でしょうか?

まず、各列が何のデータであるかを示すためのタイトル。

次に各データをどのように出力するかを決める式 (ExpressionLanguage)。

このふたつがあれば CSV は出力 (エクスポート) できそうです。

これらをふまえ、以下のような構成ファイルにしてみました。

rules:
    氏名: customer.name1 ~ ' ' ~ customer.name2
    生年月日: customer.birthday.format('Y年m月d')
    性別: gender(customer.gender)
    有効/無効: "customer.enabled ? '有効' : '無効'"

キー (列のタイトル) と値 (式) の形式で構成しています。

また、性別を変換する gender 関数も使用できるようにします。

準備

プロジェクトの作成

最初にプロジェクトのためのディレクトリを作成します。

mkdir el-csv-export
cd el-csv-export

Composer がまだ使用できない方は次に こちら を参考に Composer をダウンロードしてください。

php composer.phar require symfony/yaml symfony/expression-language

今回は YAML も使用しますので symofny/yaml もいっしょにインストールしています。

顧客データ

今回は顧客データを出力するので、そのためのクラスとダミーデータを用意します。

customer.php

<?php

declare (strict_types=1);

define('MALE', 1);
define('FEMALE', 2);

class Customer
{
    public function __construct(
        public string $name1,
        public string $name2,
        public \Datetime $birthday,
        public int $gender,
        public bool $enabled
    ) {}
}

function getCustomers(): array {
    return [
        new Customer('スズキ', 'ハナコ', new \DateTime('2000-01-01'), FEMALE, true),
        new Customer('ヤマダ', 'タロウ', new \DateTime('1988-11-06'), MALE, true),
        new Customer('カトウ', 'タカシ', new \DateTime('1999-07-16'), MALE, false),
        new Customer('サトウ', 'ユイ', new \DateTime('1999-07-16'), FEMALE, true),
    ];
}

これで準備完了です。

実装

ここから実装していきます。

とはいえ、require 文や use 文を合わせても 50 行に満たないプログラムなので、先に全体を掲載します。(この例では明瞭さを重視して、エラー処理は省略しています。)

export.php

<?php

declare (strict_types=1);

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

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Yaml\Yaml;

$export = Yaml::parseFile(__DIR__ . '/../config/export.yaml');
$rules = $export['rules'];

// ExpressionLanguage の準備
$expressionLanguage = new ExpressionLanguage;
$expressionLanguage->register(
    'gender',
    function() {},
    function($args, $gender) {
        switch ($gender) {
            case MALE:
                return '男性';
            case FEMALE:
                return '女性';
            default:
                return '不明';
        }
    }
);

$file = new \SplFileObject('php://output', 'w');

// ヘッダー
$file->fputcsv(array_keys($rules));

// データ
foreach (getCustomers() as $customer) {
    $csv = [];
    foreach ($rules as $el) {
        $csv[] = $expressionLanguage->evaluate($el, ['customer' => $customer]);
    }
    $file->fputcsv($csv);
}

flush();

まず 11-12 行目で Symfony の YAML のライブラリを利用して、先程定義した構成ファイルを読み込みます。

次に 15-29 行目で ExpressionLanguage の準備をしています。

16 行目では gender 関数の定義を行っています。

register メソッドの引数は2つ目がコンパイル時、3つ目が評価時に実行される関数を渡しますが、今回はコンパイルは行わないので何もしない関数を渡しています。

評価時の関数は $gender の値に合わせて、「男性」「女性」「不明」のいずれかを返す簡単なものです。

31-45 行目が CSV を出力 (エクスポート) する部分です。

今回はコマンドラインで実行されることを想定しており、出力 (エクスポート) は標準出力に対して行っています。

34 行目で rules のキーを取得して、それをタイトルにしています。

あまり使用されませんが、YAML のキーには日本語も使えるのでこういう使い方も可能です。

37-43 行目がデータの出力部分です。

rules から各列の式言語を取り出し、それを評価します。

評価の際には顧客データ ($customer) を引数に渡すことで、式言語内で customer 変数が使用できるようになります。

とてもシンプルですね。

実行すると以下のように出力 (エクスポート) されます。

CSV 出力 (エクスポート) フレームワーク

ExpressionLanguage を使うことでデータの出力の仕方に関する部分を外から定義できるようになっています。

そのため PHP のコードにはどのプロパティをどのように変換するか、といった記述が一切見当たりません。

今回の実装は CSV を出力 (エクスポート) することに特化したミニフレームワークと言うことができます。

このフレームワークの使用者は CSV の列やデータの変換を定義するだけでよく、CSV の出力方法については何も気にする必要はありません。

今後の課題

この実装のコンセプトは実用的ですが、本番環境で使用する場合は以下のような点を考慮する必要があります。

対象データの定義

今回はサンプルということで顧客データ固定ですが、実際にはさまざまなデータタイプに対応する方が便利です。

式言語のエラー

式言語に不備があった場合、その内容をユーザーに伝える必要があります。

条件の指定

対象のデータをすべて出力したいとは限りません。条件を指定してデータを抽出できると便利ですね。


「CSV 出力 (エクスポート) フレームワーク」という視点でこれらをどのように実装するかはなかなか面白い挑戦です。

まとめ

今回は ExpressionLanguage を使って、CSV 出力 (エクポート) フレームワークを実装してみました。

これは過度な実装でしょうか?

素直に CSV を出力 (エクスポート) するコードを書く方がシンプルだと言われる方もいるかもしれません。

確かにこのサンプルだけを見ると過度な実装ですが、項目がたくさんあったり、いろんなデータタイプに対応しなければいけないような場合にはその複雑さを十分カバーできるでしょう。

ExpressionLanguage は安全に詳細を隠蔽し、抽象度を一段押し上げる手助けをしてくれます。

自分の道具箱に入れておくと設計の幅を広げてくれます。

Discussion