月別アーカイブ: 2019年3月

マイクロフレームワークっぽく使う 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に通知したりなどファイル出力に限らないログ出力機能を備えているようです。私自身も試せていませんが重要度の高いエラーは外部の監視に頼らずに通知できたりなども出来そうですね。

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

マイクロフレームワークっぽく使う Symfony 4 事始め 4 ~RDB接続編~

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

SymfonyでのRDB接続

Symfonyフレームワークでは公式で用意されているレシピではDoctrine (Doctrine DBAL / Doctrine ORM)に対応しています。Doctrine ORMではORマッピングを利用したRDB処理、Doctrine DBALではPHP PDOをラップしたようなものになっています。Doctrine DBALではPDO同様にMySQL / MariaDB / PostgreSQL 等に対して SQL を実行する以外に、SQLを生成するクエリビルダ―の機能も有ります。

また、DoctrineDBALの代わりに他の illuminate/database などを利用できるレシピも公開されているようです。

今回は簡単に使い始められる Doctrine DBAL で説明していきます。RDBには第1回でも説明したようにMySQL 8.0を利用しています。データは symfony_test DBを作成し、以下のような books テーブルを用意しています。

Doctrine DBAL の導入

これまでの実装でcomposer.jsonのrequire部分は以下のようになっています。

    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "sensio/framework-extra-bundle": "^5.2",
        "symfony/console": "4.2.*",
        "symfony/debug-pack": "^1.0",
        "symfony/dotenv": "4.2.*",
        "symfony/flex": "^1.1",
        "symfony/framework-bundle": "4.2.*",
        "symfony/maker-bundle": "^1.11",
        "symfony/twig-bundle": "4.2.*",
        "symfony/web-server-bundle": "4.2.*",
        "symfony/yaml": "4.2.*"
    }

Doctrine DBALを利用するには symfony/orm-pack を導入します。名前から分かるようにDoctrine ORM / DBAL の機能はセットで導入されます。

composer require symfony/orm-pack

これで必要なパッケージ群が導入されました。

次にDB接続設定を行います。上記のログの最後にも出ている .env ファイル内に接続情報を記載します。Symfonyプロジェクト直下にある .env ファイルを開き、以下の部分を編集します。

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name

db_user, db_password, 接続先ホストとポート、DB名を適宜変更したら保存します。なお、MySQLでの設定になっていますが、PostgreSQLなどに接続したい場合はこれも上記ログに出ている config/packages/doctrine.yaml で設定します。現在は以下のようになっています。

doctrine:
    dbal:
        # configure these for your database server
        driver: 'pdo_mysql'
        server_version: '5.7'
        charset: utf8mb4
        default_table_options:
            charset: utf8mb4
            collate: utf8mb4_unicode_ci

        url: '%env(resolve:DATABASE_URL)%'
    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                type: annotation
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

この設定の中で ‘%env(resolve:DATABASE_URL)%’ という部分が .env ファイルで設定した値が投入される部分になります。この yaml 設定ファイル自体に各種接続情報を定義する事も可能ですが、.envファイルに環境ごとの設定を入れておくことで環境ごとに切り替えをしやすくするような仕組みになっています。なお、.envファイルは初期状態だとGitリポジトリへのコミット対象ですが、 .env.local といったファイルを作ると .env.local は最初から Git Ignore されており、設定的にもそちらが優先されるようになっていますので開発環境などでは .env.local を使った方が良いかもしれません。

doctrine.yaml でのdriverの指定はデフォルトではPDO MySQLになっているのでPostgreSQLの場合はここをPDO PostgreSQLを使う設定に変更しておきます。

実際にRDBにアクセスしてみる

実際にDoctrine DBALを利用してRDBから値を取り出してみます。

リポジトリ

RDBへのアクセスはコントローラやサービスなどからも可能でべた書き出来るのですが、今回はアクセスするためのリポジトリとなるクラスを作成します。/src/Repository ディレクトリを作成し、BooksRepository.php を作成します。

<?php

namespace App\Repository\Impl;

use App\Repository\BooksRepository;

class BooksRepositoryImpl implements BooksRepository
{

    /** @var \Doctrine\DBAL\Connection connection */
    protected $db;

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

    public function __construct(\Doctrine\DBAL\Connection $con, \Psr\Log\LoggerInterface $logger)
    {
        $this->db = $con;
        $this->logger = $logger;
    }

    public function get(int $book_id)
    {
        $sql = "SELECT * FROM books WHERE book_id = :book_id";

        $stmt = $this->db->prepare($sql);
        $stmt->bindValue("book_id", $book_id);
        $stmt->execute();

        $book = $stmt->fetch();

        return $book;
    }
}

DoctrineDBALではSELECT時に連想配列やオブジェクト、任意のクラスを元にしたオブジェクトなど様々な方法で値を返す事が出来ますが、今回はデフォルトの連想配列のまま扱います。変更する場合は Doctrine\DBAL\Driver\Statement::setFetchMode を呼び出します。

また、上記の例ではSQLの文字列に :book_id というプレースホルダを利用し、bindValue 関数で値を設定しています。この際に値が自動でエスケープされるので値を埋め込む際はプレースホルダを使うようにしましょう。また、DoctrineDBAL にはクエリビルダ機能が有るので、簡単なSQLであればクエリビルダを使って行ってもいいと思います。

コントローラ

順番通りに進めていれば Controller/RootController.php に以下の定義が既にあるはずです。無ければ追加します。

    /**
     * @Route("/books/detail/{book_id}/", name="book_detail", 
     * requirements={"book_id"="\d+"})
     */
    public function bookDetailAction($book_id)
    {
        return $this->render('books/detail.html.twig', [
            'book_id' => $book_id,
            'book_detail' => 'dumy text'
        ]);
    }

これをサービスクラスの呼び出しを追加します。サービスクラスに処理の中核となる部分を移し、コントローラではルーティングと送信されたデータの読み込み、ビューの呼び出しに専念させます(今回はIDを受け取ってDBからSELECTするだけですが)。

    /**
     * @Route("/books/detail/{book_id}/", name="book_detail", 
     * requirements={"book_id"="\d+"})
     */
    public function bookDetailAction($book_id, BooksService $booksService)
    {
        $book = $booksService->bookDetail($book_id);

        return $this->render('books/detail.html.twig', [
            'book_id' => $book_id,
            'book_detail' => $book
        ]);
    }

サービス

コントローラから呼び出され諸々の処理を行うサービスクラスを作ります。今回はリポジトリから値を受け取るだけです。/src/Service ディレクトリを作成し、BooksService.php を作成します。

<?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);
    }
}

これでコントローラ、サービス、リポジトリの準備は完了です。該当URLにアクセスすると
RootController::bookDetailAction
BooksService:: bookDetail
BooksRepositoryImpl:: get
という段階を踏んでアクセスされます。最後にビュー detail.html.twig を調整しておきます。

{% extends 'base.html.twig' %}

{% block title %}Book Detail{% endblock %}

{% block body %}

<h1>Book Detail ID:{{ book_id }}</h1>
<p>
    <div>書籍名: {{ book_detail.book_name }}</div>
    <div>価格: {{ book_detail.book_price|number_format }}円</div>
    <div>発売日: {{ book_detail.book_release }}</div>
</p>

{% endblock %}

ではSymfonyのサーバーを起動し、アクセスを確認してみます。
php bin\console server:run
でビルトインサーバーを起動し、Webブラウザから http://127.0.0.1:8000/books/detail/1/ にアクセスします。

DBから取得した値を表示できました。導入方法としてはこのような感じでRDBに接続する事が出来ます。導入~SELECTまでの説明ですが、INSERTやUPDATE、DELETEもググりながら簡単に実装できるかと思います。とはいえDoctrine に関する日本語情報ってあまりないので増えてほしいなぁ……。

ここまでのソースの状態は以下を参照
https://github.com/lf-uraku-yuki/symfony4_tutorial/tree/tutorial04

なお、MySQL 8.0ではデフォルトの認証方法が変わった事も有り、「The server requested authentication method unknown to the client”」というエラーになる場合が有ります、これはググれば出てきます該当ユーザーの認証方法を caching_sha2_password から mysql_native_password を使った物に変更する事で回避可能です。

次回はログ出力について