PHPでメッセージ管理を統一する設計:info / warning / error を一元管理する MessageHandler の実装方法

アフィリエイト広告を利用しています

このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です

PHP では、バリデーションや保存処理、API 通信など、成功・失敗・注意のメッセージを返す場面が必ず発生します。これを毎回クラスごとに実装すると、コードが散らかり、分類も曖昧になり、後から見て全く整合性が取れなくなります。本記事では、メッセージ処理を info / warning / error の 3 種類に統一し、MessageHandler という専用クラスに一元化する方法を解説します。

結論

メッセージ処理は共通化しないと確実に破綻する

バリデーション、保存処理、API 通信など、メッセージはどのクラスでも必ず使います。これを毎回書くのは無駄であり、整合性も取れません。共通化は必須です。

継承で共通化するのは間違い

「メッセージ処理を基底クラスに入れて継承すればいい」と考える人が多いです。しかし継承は密結合を生み、本来不要なメソッドを派生クラス全てに押し付けます。責務が混ざり、変更も困難です。

正しい設計は Composition(プロパティで保持)

MessageHandler のような横断的機能は「持つ(has-a)」設計が正解です。差し替えやすく、テストしやすく、責務が混ざりません。

3分類(info / warning / error)で統一すべき理由

  • 成功(info)
  • 注意(warning)
  • 失敗(error)

実務では warning が存在しない設計は不自然で、後から UI を作る際に破綻します。

MessageHandler が解決する問題

  • 散らかったメッセージ処理を集約できる
  • UI とロジックを分離できる
  • どのクラスでも同じ形式で扱える
  • ログや JSON などへの拡張が容易

背景

メッセージは全クラスで必ず発生する

成功通知、注意喚起、エラー表示。これらを統一できないと可読性が落ち、プロジェクト全体の品質に直結します。

ありがちな誤解:継承すれば楽になる

一見便利に見えますが、継承は不要な責務を巻き込み、後から絶対に面倒になります。

継承が破綻する理由

  • メッセージ処理と業務ロジックが混ざる
  • 意図しないメソッドが派生クラスへ漏れる
  • 変更の影響範囲が広すぎる

Composition にすることで責務が明確になる

MessageHandler は「メッセージ処理だけ」を担当し、業務クラスはロジックに集中できます。

基礎:info / error の 2分類では足りない理由

info と error の切り分けだけでも改善はされる

成功と失敗を分けるだけでも多少整理されます。これは最初の一歩としては正しいです。

しかし実務では warning が必須

  • 入力は正常だが補正した
  • 通信は成功したが品質が低い
  • 保存したがいくつかのレコードのみ成功した

warning がないと UI が作れない

warning がない設計は、閲覧者に正しい状況を伝えられません。UX を損ないます。

基礎版コードはここでは省略

この記事では最終形である「3分類版 MessageHandler」を直接採用します。

設計:3分類 MessageHandler(info / warning / error)

warning が重要になる実務ケース

多くの現場では「エラーではないが、注意すべき状況」が山ほどあります。warning を導入するだけでロジックが明確化します。

3分類化のメリット

  • 利用者に優しい UI が作れる
  • 開発側のロジックが明確になる
  • ログや API の構造にも使いやすい
  • メッセージ分類の曖昧さが消える

MessageHandler の責務は「分類と保持」だけ

業務処理や HTML の責務を混ぜないことが重要です。

3分類 MessageHandler(PSR-12 + phpDoc 完全版)

<?php

declare(strict_types=1);

namespace App\Core;

/**
 * Class MessageHandler
 *
 * info / warning / error の 3 種類のメッセージを一元管理するクラス。
 * クラス横断的に発生するメッセージ処理を統一し、責務を分離する目的で利用する。
 */
class MessageHandler
{
    /**
     * @var array<int, string> 通常メッセージ一覧
     */
    private array $info = [];

    /**
     * @var array<int, string> 警告メッセージ一覧
     */
    private array $warning = [];

    /**
     * @var array<int, string> エラーメッセージ一覧
     */
    private array $error = [];

    /**
     * info(通常メッセージ)を追加する。
     *
     * @param string $message 追加するメッセージ
     * @return void
     */
    public function addInfo(string $message): void
    {
        $normalized = trim($message);

        if ($normalized !== '') {
            $this->info[] = $normalized;
        }
    }

    /**
     * warning(注意メッセージ)を追加する。
     *
     * @param string $message 追加するメッセージ
     * @return void
     */
    public function addWarning(string $message): void
    {
        $normalized = trim($message);

        if ($normalized !== '') {
            $this->warning[] = $normalized;
        }
    }

    /**
     * error(エラーメッセージ)を追加する。
     *
     * @param string $message 追加するメッセージ
     * @return void
     */
    public function addError(string $message): void
    {
        $normalized = trim($message);

        if ($normalized !== '') {
            $this->error[] = $normalized;
        }
    }

    /**
     * info(通常メッセージ)を取得する。
     *
     * @return array<int, string>
     */
    public function getInfo(): array
    {
        return $this->info;
    }

    /**
     * warning(注意メッセージ)を取得する。
     *
     * @return array<int, string>
     */
    public function getWarning(): array
    {
        return $this->warning;
    }

    /**
     * error(エラーメッセージ)を取得する。
     *
     * @return array<int, string>
     */
    public function getError(): array
    {
        return $this->error;
    }

    /**
     * info が存在するか。
     *
     * @return bool
     */
    public function hasInfo(): bool
    {
        return count($this->info) > 0;
    }

    /**
     * warning が存在するか。
     *
     * @return bool
     */
    public function hasWarning(): bool
    {
        return count($this->warning) > 0;
    }

    /**
     * error が存在するか。
     *
     * @return bool
     */
    public function hasError(): bool
    {
        return count($this->error) > 0;
    }

    /**
     * info を HTML リストで返す。
     *
     * @return string
     */
    public function renderInfoHtml(): string
    {
        if ($this->hasInfo() === false) {
            return '';
        }

        $html = '';

        foreach ($this->info as $msg) {
            $html .= '<li><span class="msg-info">' . htmlspecialchars($msg, ENT_QUOTES) . '</span></li>';
        }

        return '<ul class="msg-list info">' . $html . '</ul>';
    }

    /**
     * warning を HTML リストで返す。
     *
     * @return string
     */
    public function renderWarningHtml(): string
    {
        if ($this->hasWarning() === false) {
            return '';
        }

        $html = '';

        foreach ($this->warning as $msg) {
            $html .= '<li><span class="msg-warning">' . htmlspecialchars($msg, ENT_QUOTES) . '</span></li>';
        }

        return '<ul class="msg-list warning">' . $html . '</ul>';
    }

    /**
     * error を HTML リストで返す。
     *
     * @return string
     */
    public function renderErrorHtml(): string
    {
        if ($this->hasError() === false) {
            return '';
        }

        $html = '';

        foreach ($this->error as $msg) {
            $html .= '<li><span class="msg-error">' . htmlspecialchars($msg, ENT_QUOTES) . '</span></li>';
        }

        return '<ul class="msg-list error">' . $html . '</ul>';
    }

    /**
     * error → warning → info の順でまとめて HTML を返す。
     *
     * @return string
     */
    public function renderAllHtml(): string
    {
        return $this->renderErrorHtml()
            . $this->renderWarningHtml()
            . $this->renderInfoHtml();
    }

    /**
     * すべてのメッセージをクリアする。
     *
     * @return void
     */
    public function clearAll(): void
    {
        $this->info = [];
        $this->warning = [];
        $this->error = [];
    }
}

HTML 出力仕様

重要度順(error > warning > info)で表示します。

実装:Sample クラスへの組み込み

Composition が正解である理由

メッセージ処理と業務ロジックの責務が完全に分離されるため、後から変更しても影響範囲が最小になります。

Sample の構造

  • 値セット(バリデーション)
  • 保存処理
  • MessageHandler にメッセージを追加
  • UI でまとめて表示

Sample(PSR-12 + phpDoc 完全版)

<?php

declare(strict_types=1);

namespace App\Core;

use App\Core\MessageHandler;

/**
 * Class Sample
 *
 * 値のバリデーションと保存処理を行うクラス。
 * メッセージは MessageHandler に委譲する。
 */
class Sample
{
    /**
     * @var int サンプル値
     */
    private int $value = 0;

    /**
     * @var MessageHandler メッセージ管理クラス
     */
    private MessageHandler $messages;

    /**
     * コンストラクタ
     *
     * @param MessageHandler|null $messages 差し替え可能なメッセージ管理クラス
     */
    public function __construct(?MessageHandler $messages = null)
    {
        $this->messages = $messages ?? new MessageHandler();
    }

    /**
     * MessageHandler インスタンスを返す。
     *
     * @return MessageHandler
     */
    public function messages(): MessageHandler
    {
        return $this->messages;
    }

    /**
     * 値をセットする。
     *
     * @param mixed $input 入力値
     * @return void
     */
    public function setValue(mixed $input): void
    {
        if ($input === null || $input === '') {
            $this->messages->addWarning('値が空だったため 0 を設定しました。');
            $this->value = 0;
            return;
        }

        if (is_numeric($input) === false) {
            $this->messages->addError('value は数値である必要があります。');
            return;
        }

        $this->value = (int) $input;
        $this->messages->addInfo('値をセットしました。');
    }

    /**
     * 値を保存する。
     *
     * @return void
     */
    public function save(): void
    {
        $success = true; // 想像:実際は保存処理を行う

        if ($success === true) {
            $this->messages->addInfo('保存が完了しました。');
        } else {
            $this->messages->addError('保存に失敗しました。');
        }
    }
}

実行例(整形不要)

$sample = new Sample();
$sample->setValue('abc');
$sample->save();

echo $sample->messages()->renderAllHtml();

注意点

MessageHandler の責務を混ぜない

ロジックや例外、翻訳などを入れると破綻します。メッセージ分類と保持だけに限定してください。

バリデーションとメッセージ処理は完全に別物

MessageHandler が判定ロジックを持つべきではありません。業務クラスが判定し、結果だけ渡します。

HTML とロジックは分離可能

必要なら render 系を別クラスに切り離し、Twig/Blade 側でレンダリングすることも可能です。

DI で差し替え可能にする

テスト用の NullMessageHandler や、JSON 出力用の Handler に差し替えるのも容易です。

分類の境界線を曖昧にしない

info / warning / error の分類ルールは明確にしておく必要があります。曖昧だと UI が崩れます。

まとめ

メッセージ処理は絶対に共通化すべき領域

散らばったメッセージ処理は確実に破綻します。早い段階で MessageHandler を導入したほうが良いです。

Composition が現代 PHP の正解

継承は不要な依存を生み、責務も混在させます。MessageHandler をプロパティとして持つのが最適解です。

3分類 MessageHandler の利点

  • 重要度が明確
  • UI が組みやすい
  • コードの重複がなくなる
  • ログ・API への応用が簡単

MessageHandler はプロジェクト全体を整える

導入するだけで、開発効率・品質・保守性が大きく向上します。今日から使うべき仕組みです。