PowerCMS XでCSVをインポートした時の独自処理をプラグインで実装する

公開

昨晩はPowerCMS XにてCSVでデータをインポートした際の独自処理をどのように実装するかを考えました。先日から書いているAlgolia + PowerCMSシリーズで、CSVインポートしたデータを一括でAlgoliaのインデックスに登録できないかと思った次第です。

結論としては、ジョブとキューに情報を登録し、ワーカーが実行されたときにまとめてAlgoliaに送信するようにしました。CSVをインポートした時にpost_importがコールされるのでそこで登録しても良いのですが、1件ごとにコールされるので速度が低下したりAlgoliaのAPIコール数を消費するかなと考えました。

ジョブとキューは現在SiteSyncプラグインなどが利用しています。(おっと、これは別売りのプラグインか。どこまで書いてよいか…。)なかなか自分で使うことのないモデルで調べながら書いてみました。PowerCMS Xプラグイン開発の参考になれば幸いです。

1件目をインポートした時にジョブを作成

1回目のpost_importでジョブを作成します。別売りプラグインが使うモデルと言っても普通のモデルと変わらず、「PHPによるプログラミング・ガイド (データベース編) | PowerCMS X」で紹介されている基礎の構文で書けます。labelカラムにどのモデルに対してCSVインポートしたかを格納しました。開始日時start_onはひとまず5分後にしました。(正しくインポートされたか確認したりするかなと思いまして。)

public function post_import( $cb, $app, &$object ) {
    $name = 'Save objects into Algolia index.';
    if ( ! $this->job_id ) {
        $job = $app->db->model( 'ts_job' )->__new();
        $job->name( $name );
        $job->class( 'CSV file imported.' );
        $job->component( get_class( $this ) );
        $job->label( $object->_model );
        $job->workspace_id( $object->workspace_id );
        $start_on_dt = new DateTime();
        $start_on_dt->modify( '+5 minutes' );
        $job->start_on( $start_on_dt->format( 'Y-m-d H:i:s' ) );
        $app->set_default( $job );
        $job->save();
        $this->job_id = $job->id;
        $this->job_start_on_dt = $start_on_dt;
    }
}

キューにインポートしたオブジェクトの情報を溜める

1件ごとにpost_importが呼ばれるので、キューに情報を溜めます。例えば学校モデルにCSVインポートした際は、キューに学校オブジェクト=学校1つ1つの情報を溜めていきます。学校の情報は学校モデルに入っているので、学校モデルのIDだけキューに格納すれば良いかと考えました。ジョブに紐付けるためにジョブIDも登録します。まだ実装していませんが、公開ステータスも考慮した方が良いですね。

重要なのはworker.phpでキューを処理したときに実行してほしい関数名をmethodに格納します。componentmethodがあるクラス名ですが、通常プラグイン名かと思います。

新規オブジェクトを作成して保存する構文は先程と同じです。__new()する時に引数で値を渡す方法もあります。こちらの方がすっきり見やすいかも。

public function post_import( $cb, $app, &$object ) {
    $name = 'Save objects into Algolia index.';
    if ( ! $this->job_id ) {
        $job = $app->db->model( 'ts_job' )->__new();
        // 省略
    }
    // TODO: 公開ステータスを考慮する必要がある
    $queue = $app->db->model( 'queue' )->__new([
        'key'          => md5( $object->name ),
        'class'        => $name,
        'component'    => get_class( $this ),
        'method'       => 'save_object_by_queue',
        'start_on'     => $this->job_start_on_dt->format( 'Y-m-d H:i:s' ),
        'workspace_id' => $workspace_id
    ]);
    $queue->ts_job_id( $this->job_id );
    $queue->object_id( $object->id );
    $queue->model( $object->_model );
    $app->set_default( $queue );
    $queue->save();
}

save_object_by_queueの処理

キューを処理したときに実行されるsave_object_by_queue関数(キューモデルのmethodで指定した関数)を書くのですが、ここに処理を書いてしまうと1件ずつ実行されてしまいます。よって今回は何も処理を書かずreturn true;を返します。return false;を返すと処理に失敗した扱いでキューにそのまま残ります。

public function save_object_by_queue( $app, $queue, &$error ) {
    return true;
}

ジョブに対する処理を書く

ジョブが実行されたときの処理を書きます。先程return true;を返したキューたちが$queuesで渡ってくるようなので、ループを回して学校モデルのオブジェクトIDを集めます。そして先日の記事「PowerCMS Xのモデルに登録したデータをAlgoliaに登録し、返値のObjectIDsをモデルに保存する」で紹介したsave_multiple_objects関数にIDを渡します。IDを渡した時は指定されたIDのオブジェクトだけloadしてAlgoliaに送信するように手を加えました。

public function ts_job_post_run( $cb, $app, $object, $queues, $log ) {
    if ( $object->component !== get_class( $this ) ) {
        return true;
    }
    $target_object_ids = [];
    foreach ( $queues as $queue ) {
        $target_object_ids[] = $queue->object_id;
    }
    // TODO: 汎用化の際はモデル or インデックス名を引数にとる
    $this->save_multiple_objects( $app, $target_object_ids );
}

複数のモデルにCSVインポートをし、複数のジョブがある時はどのように情報が渡ってくるかvar_dump()を仕込んで観察したのですが、ジョブ毎に関連するキューが渡ってくるようです。よって複数のモデルにCSVインポートした時も目的のAlgoliaインデックスにきちんと登録できそうです。

[hideki@Honu]$ php ./tools/worker.php
AlgoliaSupport.php:119:
  string(22) "que id, model: 17, entry"
AlgoliaSupport.php:119:
  string(22) "que id, model: 18, entry"
AlgoliaSupport.php:127:
  string(10) "ts_job_id: 16"
AlgoliaSupport.php:130:
  string(21) "ts_job_loop_queue: 17"
AlgoliaSupport.php:130:
  string(21) "ts_job_loop_queue: 18"
AlgoliaSupport.php:134:
  string(14) "ts_job_end: 16"

AlgoliaSupport.php:119:
  string(23) "que id, model: 19, school"
AlgoliaSupport.php:119:
  string(23) "que id, model: 20, school"
AlgoliaSupport.php:127:
  string(10) "ts_job_id: 17"
AlgoliaSupport.php:130:
  string(21) "ts_job_loop_queue: 19"
AlgoliaSupport.php:130:
  string(21) "ts_job_loop_queue: 20"
AlgoliaSupport.php:134:
  string(14) "ts_job_end: 17"

CSVをインポートした後でworker.phpを実行する

CSVで5件のデータをインポートすると、以下のようにキューに5件データが溜まりました。
画面キャプチャ:PowerCMS Xのキューモデルの様子

worker.phpを実行するとキューが処理されて一覧からは消えます。
画面キャプチャ:PowerCMS Xのキューモデルの様子(オブジェクトがない)

Algoliaのインデックスを確認すると5件ヒットし、正しく登録されていることが確認できました。
画面キャプチャ:Algoliaのインデックスで先程CSVインポートしたデータを検索した画面

むすび

今まで紹介したコードの他に、編集画面でオブジェクトを保存した時にAlgoliaのインデックスに登録する処理も書いています。1件だけ送信するところが違うだけで、ほぼsave_multiple_objects関数と同一です。これで一通りの登録処理が書けたので、あとはReact等でフロントの表示を作り込めばよいことになります。

PHPによるプログラミング・ガイド (データベース編) | PowerCMS X」にある$app->model($model)->new()$app->model($model)->load()をマスターし、ロジックを上手く組み立てると目的の機能はひとまず実装できそうです。汎用的にすることを考えるとモデル毎にカラムが違うなどするので、要求される技術力が上がるのかなと思いました