カテゴリー別アーカイブ: ソフトウェア・開発

「選定した技術が1年で死んだ話」の1年後

1年半前に以下のような記事を書きました。その記事が前提となり、その続きになります。

対応の方向性

2018年末ごろから Symfony 4.2 への移行を始めました。Symfony 4 についての日本語情報はかなり少なかったので、数少ない日本語情報に感謝をささげながら、基本は Symfony 公式のドキュメントを Google翻訳にかけて読みながら対応しました。

※ 得られた知見を本ブログ内別記事にまとめています。

Silex 1.x → Symfony 4.2 への移植の実際

現状の2代目システムの実装としては Silex 1.x での実装となり、依存性注入の仕組みは Pimple というアレイアクセスベースのシンプルなサービスロケータ、クラス構成の主要な部分としては、コントローラ、サービス、リポジトリのインタフェース、リポジトリ、DoctrineDBALで読みだしたレコードを保持したりバリデーションが実装されていたりするレコードオブジェクトという構成になっていました。

コントローラとビュー

まずはコントローラの移植になりますが、基本的にはこの部分が実コードでは一番重いものになりました。Silexのルーティングから Symfony のアノテーションを使った物に置き換えていく作業は主に以下の内容になりました。

  • SilexのクロージャベースのルーティングをSymfonyのアノテーションベースのルーティングに書き換える作業を進めます。どちらか一方でしか出来ない機能という物がなく、淡々と対応すれば完了しました。ただし物量が多い。
  • Requestオブジェクトなど要素要素の機能はもともとSymfonyコンポーネントを使っていたSilexなのもあり、そのまま使い続ける事が出来る。
  • リダイレクトなどは関数名が変わってくるので調整する。
  • ビューはTwigがそのまま使えるのでセッションに関する扱いだけ調整すればそのまま動作する。
  • Pimpleを使っていたDIは全てコンストラクタインジェクションに変更。あとはSymfonyが勝手にサービスを注入してくれる。

ルーティングは書き換えになりますがコントローラ内のアクション処理としては9割そのままコピペで動いた印象です。残りの1割を書き換えていきました。

サービス

ここでいうサービスは所謂ビジネスロジックです。サービスクラスは一部Pimpleのサービスロケータを引き回している部分が有ったので全てコンストラクタインジェクションか、コントローラから呼ばれる際のメソッドインジェクションに変更しました。ログ出力に関しても同様に Psr\Log\LoggerInterface を注入するように変更。それ以外はSilex独自の機能への依存がほとんどなく、ほぼそのまま動作しました。

リポジトリとレコードオブジェクト

リポジトリの実装はDoctrineDBALの存在にがっつり依存していましたが、これはSymfonyでも同様に利用できるので問題になりませんでした。ほとんどの処理がそのまま動作しました。

テスト

PHPUnitで実装されたテストは全体の2~3割程度に調整が必要になりました。この背景として元々使っていた phpunit/dbunit の開発終了が有ります。
https://github.com/sebastianbergmann/dbunit

データベース周りのテストを行う際にレコードの取得結果を比較したり、YAMLファイルで定義したデータをDBに流し込んだりリセットしたりなどの機能を提供しているライブラリなのですが、これが開発終了し新しいPHPUnitに対応していかなくなる事態になりました。

色々方法は考えたのですが、phpunit/dbunit の提供する機能のうち、実際に利用してたものだけについて互換実装を作り込んで使う事にしました。YAMLファイルを読み込んでDBに書き込む機能、欠けた部分のある連想配列を欠けていない部分のみで比較する機能などを関数のインタフェースに互換性を持たせながら実装して、これは1ファイル1クラスのシンプルな互換機能になりました。もちろん PHPUnit 8系で問題なく動作する物になっています。

結果

期間としては大体3か月程度でSymfony 4.2 への移植が完了しました。これは想定よりもだいぶ短い期間でした。併せて PHP 7.1 から 7.2 への移行を行ったり様々な追加機能を載せたりしていますが、現状問題なく運用し続けています。上手く行った要因としては以下が有ったのかと思います。

  • Silex 自体の開発が終了したと言っても Symfony は健在なので、置き換えが必要なクラス、機能が最小限で済んだ。結果論ではあるがSilex採用時の「Symfonyコンポーネントを使ったフレームワークにしよう」という選択に助けられた。
  • もともとサービスクラス、リポジトリ、レコードオブジェクトがほぼSilexに依存しておらず、依存関係も依存性注入を使った作りになっていたので移植コストが低かった。再設計時の設計が(ベストかどうかはともかく)悪くなかった。
  • 初代システム(1ページ1PHPファイル)とは違って2代目システムはテストコードが有ったので、動作確認のコストが抑えられた。テストが書かれていなかった部分やテストは書かれているもののカバーしきれていなかった動作、その他もろもろでバグは出たものの、それでもテストが無い状態よりははるかに楽になった実感が有る。

結論

  • フレームワークの機能を便利に使いつつも 、それに依存しすぎない疎結合な設計になっているとフレームワークが開発終了に見舞われてもコードの大部分をそのまま動かす事が出来る。それまで「疎結合大事って言ってもライブラリ、ユーティリティ的なクラス以外はあんまり再利用したりしなくない? テストが書きやすくなるのは間違いないけど」と思う部分も有ったが、テストが書きやすいだけではないフレームワークに対する疎結合の重要性を体感した。
  • さすがにフレームワークが変わるレベルだと動かなくなるテストコードも少なくないので「元と同じテストが全部通ればOK」とはならないものの、とはいえ有るのと無いのでは全く違う。テストコード大事。

マイクロフレームワークっぽく使う Symfony 4 事始め 6 ~本番環境公開編~

Symfony 4 入門。前回の記事は以下

dev モードから prod モードに切り替える

第5回のこれまではずっと dev モードで開発してきました。dev モードでは例えば以下のような機能が提供されています。

  • Webページ下部に表示されるプロファイラ
  • 発行されたSQL全てがログに出力される(プロファイラ内でも確認できます)

前者は中身が筒抜けになり、後者は動作が遅くなりログサイズが爆発する原因になるのでどちらも本番環境として公開するには無効化しておきますよね。その場合は .env ファイル内での APP_ENV=dev の定義を APP_ENV=prod に変更する事で本番環境としてのモードに切り替える事が出来ます。もちろん .env.local で上書きする方法でも問題ありません。prod モードでは以下のような挙動の違いが有ります。

  • Webページ上にプロファイラが表示されない。
  • 発行されたSQLのログがログファイルに出力されない。
  • 設定ファイルの内 config/packages/dev 内の物がが読み込まれていた物は config/packages/prod 内の物が読み込まれるようになる。
  • ログのファイル名の dev の部分が prod になる。
  • キャッシュが自動的に更新されない。
  • bin\console server:run が使えない。

このうち「キャッシュが自動的に更新されない」件に関しては注意が必要です。プログラムのコードやビューテンプレートのコードを編集しても反映されなくなる場合が有ります。prod モードで運用している場合、改修のデプロイ後はキャッシュをクリアするようにしましょう。これはSymfonyのコマンドから実行する事が出来ます。

実行するには
php bin\console cache:clear
を実行します。

これでキャッシュが削除・更新されました。CIツールなどでデプロイを自動化しているような場合はこの操作を組み込んでおくといいんじゃないかと思います。

また、 bin\console server:run が使えなくなる件については確かに開発用の機能なのでそれも仕方ないかなという感じでは有りますが、ドキュメントルートになる public ディレクトリ内でPHPのビルトインサーバーを自分で起動してやれば dev モード時と同じようにビルトインサーバーで動作確認が可能です。


マイクロフレームワークっぽく使う Symfony 4 事始め 5 ~ロギング編~

Symfony 4 入門。前回の記事は以下

Symfonyでのログ出力

Symfonyでのログ出力には基本的には monolog を使います。これはPSR-3に準拠するログ出力ライブラリで、この記事の第1回の時点で自動的に導入されています。また、DIコンテナによる注入にも初期状態で対応しているので、 Psr\Log\LoggerInterface を受け取るコンストラクタや関数を用意すればDIコンテナにより自動的に注入されます。

今回は第3回で作成したサービスクラス BooksService にログ出力を追加してみます。前回までの状態は以下。

<?php
namespace App\Service;

use App\Repository\BooksRepository;

class BooksService
{
    private $booksRepository;

    public function __construct(BooksRepository $booksRepository)
    {
        $this->booksRepository = $booksRepository;
    }

    public function bookDetail(int $book_id)
    {
        return $this->booksRepository->get($book_id);
    }
}

コンストラクタでLoggerInterfaceを受け取るようにします。受け取ったロガーを利用してログを出力します。

<?php
namespace App\Service;

use App\Repository\BooksRepository;

class BooksService
{
    private $booksRepository;

    /** @var Psr\Log\LoggerInterface logger */
    protected $logger;

    public function __construct(BooksRepository $booksRepository, \Psr\Log\LoggerInterface $logger)
    {
        $this->booksRepository = $booksRepository;
        $this->logger = $logger;
    }

    public function bookDetail(int $book_id)
    {
        $this->logger->log('info', 'book_id: ' . $book_id . ' の詳細データを取得して返します');
        return $this->booksRepository->get($book_id);
    }
}

ログを出力する関数には
log(‘ログレベル’, ‘メッセージ’)
の他にそれぞれのログレベルに対応した
emergency(‘メッセージ’)
alert(‘メッセージ’)
critical(‘メッセージ’)
warning(‘メッセージ’)
notice(‘メッセージ’)
info(‘メッセージ’)
debug(‘メッセージ’)
が利用できます。上に有るものが重要度の高いログになっています。

実際にログ出力を埋め込んだ bookDetail が呼びされる http://127.0.0.1:8000/books/detail/2/ にアクセスしてみます。

ログファイルは /var/log/dev.log に出力されています。

このファイル名の dev という部分は環境定義によるものなので、ユニットテスト実行時は test.log 、本番環境で動かす際は prod.log になります。

ログのローテーション

例えばLinux側にもログをローテーションするための仕組みは有り、それを利用してログファイルを管理してもいいですが、逆にプログラム側で最初からファイル名に日付を付けてほしいという場合も有るでしょう。それはmonologの設定で可能です。
config/packages/dev/monolog.yaml
は初期状態で以下のようになっています。

monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event"]
        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

typeの部分に指定されている stream がデフォルトの出力方法になっています。これを rotating_file に変更します。この状態でログを出力してみると

dev-2019-02-31.log と日付付きのファイル名で出力されるようになりました。

monologには他にもエラーをSlackに通知したりなどファイル出力に限らないログ出力機能を備えているようです。私自身も試せていませんが重要度の高いエラーは外部の監視に頼らずに通知できたりなども出来そうですね。

次回は本番環境での公開方法について。