見出し画像

PHPでデータベースをCRUD操作する④

 前回まででCRUD操作は完成しましたが、このままでは少々システムが使いづらいので、少しでも使いやすくなるように改良します。


筆者の開発環境

PC:Apple M1 チップ搭載MacBook Air
OS:macOS Ventura 13.6
MAMP:6.8
PHP:8.2.0

エラーハンドリング

エラー表示の制御

 本番環境ではエラーが発生した時にエラーの情報が画面に表示されると危険なのでエラー情報を隠します。エラー情報の画面表示のオンオフはphp.inniで行います。
 まず、システムが稼働している環境、ローカル開発環境なのか本番環境なのか、を判定するために.envファイルに「APP_ENV」という項目を追加します。ローカル開発環境では「local」、本番環境では「production」とします。

APP_ENV=production

bootstrap.phpを下記のように修正してください。

<?php

use App\Controllers\UsersController;

session_start();

require_once __DIR__ . '/vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

// 本番環境ではエラー情報を表示しない
if ($_ENV['APP_ENV'] === 'production') ini_set('display_errors', 0);

わざとエラーを発生させるために下記のように存在しない関数を呼び出してみましょう。bootstrap.phpに追記してください。

// 本番環境ではエラー情報を表示しない
if ($_ENV['APP_ENV'] === 'production') ini_set('display_errors', 0);
test();

https://localhost:8888/users にアクセスしてください。下記のような画面が表示されればOKです。

500エラー画面

成功したらtest()関数は削除しておいてください。

// 本番環境ではエラー情報を表示しない
if ($_ENV['APP_ENV'] === 'production') ini_set('display_errors', 0);

.envのAPP_EMVを「local」に戻しておいてください。

APP_ENV=local

ユーザーが取得出来ない時

 Userモデルのfind()メソッドに存在しないidや不正な値が渡された時には、ユーザーインスタンスが取得できないので、例外を発生させて独自の404画面を表示してみましょう。
 独自の例外クラスを定義します。下記のコマンドを実行し例外クラスを格納するフォルダを作成してください。

mkdir app/Exceptions

下記のコマンドを実行しNotFoundException.phpを作成してください。

touch app/Exceptions/NotFoundException.php

 NotFoundException.phpを下記のように編集してください。Exceptionクラスを継承してください。

<?php

namespace App\Exceptions;

use Exception;

class NotFoundException extends Exception {}

Userモデルを下記のように編集してください。

    /**
     * プライマリーキーで取得
     *
     * @param integer $id
     * @return self
     * @throws NotFoundException
     */
    public static function find(int $id): self
    {
        $dbh = DatabaseConnector::connect();
        $sql = 'SELECT * FROM users WHERE id = :id';
        $stmt = $dbh->prepare($sql);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        $stmt->setFetchMode(PDO::FETCH_CLASS, self::class);
        $user = $stmt->fetch();

        if (!$user) throw new NotFoundException('ユーザーが見つかりませんでした。');

        return $user;
    }

下記のコマンドを実行し、エラー画面用のテンプレートを格納するフォルダを作成してください。

mkdir resources/views/errors

下記のコマンドを実行し404.phpを作成してください。

touch resources/views/errors/404.php

404.phpを下記のように編集してください。

<?php ob_start(); ?>

<h1 class="mt-5"><?= $errorMessage; ?></h1>
<a href="/users">ユーザーリストへ戻る</a>

<?php $content = ob_get_clean(); ?>

例外を補足するために下記のようにboostrap.phpを編集してください。

(省略)

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        echo '404 Not Found';
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        echo '405 Method Not Allowed';
        break;
    case FastRoute\Dispatcher::FOUND:
        try {
            $handler = $routeInfo[1];
            $vars = $routeInfo[2];
            echo $handler($vars);
            break;
        } catch (NotFoundException $e) {
            (new View)->render('errors/404.php', ['errorMessage' => $e->getMessage()], ['title' => '404 Not Found']);
        }

 編集できたら実際に存在しないidを指定してhttps://localhost:8888/users/100にアクセスしてみましょう。「100」の数字は存在しないidであれば何でも大丈夫です。下記の画面が表示されれば成功です。

404画面

各種リンクの設置

 一覧画面に登録フォームへのリンクを設置します。resources/views/users/index.phpを下記のように編集してください。

<?php ob_start(); ?>

<a href="/users/create" class="btn btn-success mt-5">新規登録</a>
<table class="table table-bordered mt-5">
    (省略)
</table>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/usersにアクセスしてください。リンクをクリックして登録フォームが表示されることを確認してください。

登録フォームリンク

 登録フォーム画面へ一覧画面へ戻るリンクを設置します。resources/views/users/create.phpを下記のように編集してください。

<?php ob_start(); ?>

<a href="/users" class="btn btn-secondary mt-5">戻る</a>
<form action="/users" method="POST" class="mt-5">
    (省略)
</form>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/users/createにアクセスしてください。リンクをクリックして一覧画面に戻れることを確認してください。

戻るリンク

 同じように編集フォームにも一覧画面へ戻るリンクを設置します。resources/views/users/edit.phpを下記のように編集してください。

<?php ob_start(); ?>

<a href="/users" class="btn btn-secondary mt-5">戻る</a>
<form action="/users/<?= $user->id; ?>" method="POST" class="mt-5">
    (省略)
</form>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/users/1/editにアクセスしてください。idの「1」の箇所はご自身の環境に合わせて調整してください。リンクをクリックして一覧画面に戻れることを確認してください。

戻るリンク

 同じように詳細参照画面にも一覧画面へ戻るリンクを設置します。resources/views/users/show.phpを下記のように編集してください。

<?php ob_start(); ?>

<a href="/users" class="btn btn-secondary mt-5">戻る</a>
<p class="mt-5">ID: <?= $user->id; ?></p>
<p>名前: <?= $this->h($user->name); ?></p>
<p>メールアドレス: <?= $this->h($user->email); ?></p>
<p>作成日時: <?= $user->created_at; ?></p>
<p>更新日時: <?= $user->updated_at; ?></p>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/users/1にアクセスしてください。idの「1」の箇所はご自身の環境に合わせて調整してください。リンクをクリックして一覧画面に戻れることを確認してください。

戻るリンク

フラッシュメッセージ

登録や更新に成功した時に画面へメッセージを表示します。

Flashクラスを作成

下記のコマンドを実行しFlash.phpを作成してください。

touch app/Utilities/Flash.php

Flash.phpを下記のように編集してください。

<?php

namespace App\Utilities;

class Flash
{
    /**
     * セッションに引数で渡されたキーが存在するか?
     *
     * @param string $key
     * @return boolean
     */
    public static function has(string $key): bool
    {
        return array_key_exists($key, $_SESSION);
    }

    /**
     * フラッシュメッセージを出力する
     *
     * @param string $key
     * @return string
     */
    public static function message(string $key): string
    {
        $message = $_SESSION[$key];
        unset($_SESSION[$key]);

        return $message;
    }

    /**
     * メッセージをセッションに格納する
     *
     * @param string $key
     * @param string $message
     * @return void
     */
    public static function addMessageToSession(string $key, string $message): void
    {
        $_SESSION[$key] = $message;
    }
}

フラッシュメッセージを追加

 UsersControllerのstore()メソッドとupdate()メソッドのリダイレクト処理の前に、フラッシュメッセージを保存する処理を追加します。UsersController.phpを下記のように編集してください。Flashクラスをuseするのを忘れないでください。

<?php

namespace App\Controllers;

use App\Models\User;
use App\Utilities\Flash;
use App\Utilities\Redirector;
use App\Utilities\View;
use EasyCSRF\EasyCSRF;
use EasyCSRF\Exceptions\InvalidCsrfTokenException;
use EasyCSRF\NativeSessionProvider;

class UsersController
{
    (省略)

    /**
     * 登録処理
     *
     * @return void
     */
    public function store(): void
    {
        try {
            $sessionProvider = new NativeSessionProvider();
            $easyCSRF = new EasyCSRF($sessionProvider);
            $easyCSRF->check($_ENV['CSRF_SALT'], $_POST['csrf_token']);

            $params = [];
            foreach ($_POST as $key => $value) {
                $params[$key] = trim($value);
            }

            User::create($params);

            Flash::addMessageToSession('success', 'ユーザーデータの登録に成功しました。');

            (new Redirector)->to('users');
        } catch(InvalidCsrfTokenException $e) {
            echo $e->getMessage();
        }
    }

    (省略)

    /**
     * 更新処理
     *
     * @param integer $id
     * @return void
     */
    public function update(int $id): void
    {
        $user = User::find($id);

        try {
            $sessionProvider = new NativeSessionProvider();
            $easyCSRF = new EasyCSRF($sessionProvider);
            $easyCSRF->check($_ENV['CSRF_SALT'], $_POST['csrf_token']);

            $params = [];
            foreach ($_POST as $key => $value) {
                $params[$key] = trim($value);
            }

            $user->update($params);

            Flash::addMessageToSession('success', 'ユーザーデータの更新に成功しました。');

            (new Redirector)->to('users');
        } catch(InvalidCsrfTokenException $e) {
            echo $e->getMessage();
        }
    }

    (省略)
}

resources/views/layouts/app.phpを下記のように編集してください。

<?php ob_start(); ?>

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><?= $title ?? 'ユーザー管理'; ?></title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col">
                <?php if (\App\Utilities\Flash::has('success')) : ?>
                    <div class="alert alert-success mt-5" role="alert">
                        <?= \App\Utilities\Flash::message('success'); ?>
                    </div>
                <?php endif; ?>
                <?php if (\App\Utilities\Flash::has('error')) : ?>
                    <div class="alert alert-danger mt-5" role="alert">
                        <?= \App\Utilities\Flash::message('error'); ?>
                    </div>
                <?php endif; ?>

                <?= $content; ?>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>

</html>

<?php $content = ob_get_clean(); ?>

 http://localhost:8888/usersにアクセスしてユーザーを新規登録してください。一覧画面に下記のようなメッセージが表示されれば成功です。

フラッシュメッセージ

更新処理も同様に確認してください。下記のように表示されたら成功です。

フラッシュメッセージ

バリデーション

 現在、名前とメールアドレスが空白のまま登録処理に進むと値なしのまま登録できてしまいます。それでは困りますので、データが空白の場合にフォーム画面に戻してエラーメッセージを表示できるようにします。

UserValidatorクラスを作成

 下記のコマンドを実行しバリデーターを格納するフォルダを作成してください。

mkdir app/Validators

下記のコマンドを実行しUserValidator.phpを作成してください。

touch app/Validators/UserValidator.php

 UserValidator.phpを下記のように編集してください。エラーがあった場合、UserValidatorインスタンスをセッションに格納し、登録フォーム内でエラーメッセージを表示します。

<?php

namespace App\Validators;

class UserValidator
{
    /**
     * @var array
     */
    private $messages;

    /**
     * コンストラクタ
     *
     * @param array $params
     */
    public function __construct(
        private array $params
    ) {}

    /**
     * バリデーション実行
     *
     * @return boolean
     */
    public function validate(): bool
    {
        if (mb_strlen($this->params['name']) === 0) {
            $this->addMessage('name', '名前を入力してください。');
        }

        if (mb_strlen($this->params['email']) === 0) {
            $this->addMessage('email', 'メールアドレスを入力してください。');
        }

        if ($this->messages > 0) {
            $_SESSION['errors'] = serialize($this);
            return false;
        }

        return true;
    }

    /**
     * エラーメッセージを取得
     *
     * @param string $key
     * @return string
     */
    public function getMessage(string $key): string
    {
        return $this->messages[$key];
    }

    /**
     * エラーメッセージが存在するか?
     *
     * @param string $key
     * @return boolean
     */
    public function hasMessage(string $key): bool
    {
        return array_key_exists($key, $this->messages);
    }

    /**
     * エラーメッセージを追加
     *
     * @param string $key
     * @param string $message
     * @return void
     */
    private function addMessage(string $key, string $message): void
    {
        $this->messages[$key] = $message;
    }
}

バリデーションの導入

 UserConstrollerのcreate()メソッドとstoreメソッドにバリデーション関連の処理を追加します。UserController.phpを下記のように修正してください。

    /**
     * 登録フォームの表示
     *
     * @return void
     */
    public function create(): void
    {
        $sessionProvider = new NativeSessionProvider();
        $easyCSRF = new EasyCSRF($sessionProvider);
        $csrfToken = $easyCSRF->generate($_ENV['CSRF_SALT']);

        // 直前の入力内容を取得
        $oldInputs = [];
        if (!empty($_SESSION['old_inputs'])) {
            $oldInputs = $_SESSION['old_inputs'];
            unset($_SESSION['old_inputs']);
        }

        // エラーメッセージを取得
        $errors = null;
        if (!empty($_SESSION['errors'])) {
            $errors = unserialize($_SESSION['errors']);
            unset($_SESSION['errors']);
        }

        (new View)->render('users/create.php', compact('csrfToken', 'oldInputs', 'errors'), ['title' => '新規登録 | ユーザー管理']);
    }

    /**
     * 登録処理
     *
     * @return void
     */
    public function store(): void
    {
        try {
            $sessionProvider = new NativeSessionProvider();
            $easyCSRF = new EasyCSRF($sessionProvider);
            $easyCSRF->check($_ENV['CSRF_SALT'], $_POST['csrf_token']);

            $params = [];
            foreach ($_POST as $key => $value) {
                $params[$key] = trim($value);
            }

            // バリデーション実行
            $ret = (new UserValidator($params))->validate();

            // もしバリデーションに失敗したら
            if (!$ret) {
                // ユーザーの入力内容をセッションに格納しておく
                $_SESSION['old_inputs'] = $params;

                // フラッシュメッセージを準備し
                Flash::addMessageToSession('error', '入力内容をご確認ください。');

                // 登録フォームにリダイレクトする
                (new Redirector)->to('users/create');
            }

            User::create($params);

            Flash::addMessageToSession('success', 'ユーザーデータの登録に成功しました。');

            (new Redirector)->to('users');
        } catch(InvalidCsrfTokenException $e) {
            echo $e->getMessage();
        }
    }

 resources/views/users/create.phpを下記のように修正してください。
 Bootstrap5のルールに従います。

  • formタグにnovalidate属性を付与。エラーがあった場合was-validatedクラスを付与

  • 名前とメールアドレスのinputにrequired属性を付与

  • エラーメッセージにはinvalid-feedbackを付与

 名前とメールアドレスのvalue値はoldInputsに値が入っていればその値を使用し、入っていなければ空白のままにしておきます。

<?php ob_start(); ?>

<a href="/users" class="btn btn-secondary mt-5">戻る</a>
<form action="/users" method="POST" class="mt-5 <?php if (!empty($errors)) : ?>was-validated<?php endif; ?>" novalidate>
    <div class="mb-3">
        <label for="name" class="form-label">名前</label>
        <input type="text" id="name" class="form-control" name="name" maxlength="50" value="<?= $oldInputs['name'] ?? null; ?>" required>
        <?php if (!empty($errors) && $errors->hasMessage('name')) : ?>
            <div class="invalid-feedback"><?= $errors->getMessage('name'); ?></div>
        <?php endif; ?>
    </div>
    <div class="mb-3">
        <label for="email" class="form-label">メールアドレス</label>
        <input type="text" id="name" class="form-control" name="email" maxlength="100" value="<?= $oldInputs['email'] ?? null; ?>" required>
        <?php if (!empty($errors) && $errors->hasMessage('email')) : ?>
            <div class="invalid-feedback"><?= $errors->getMessage('email'); ?></div>
        <?php endif; ?>
    </div>
    <input type="hidden" name="csrf_token" value="<?= $csrfToken; ?>">
    <button type="submit" class="btn btn-primary">登録</button>
</form>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/users/createにアクセスしてください。空白のまま「登録」ボタンをクリックしてエラーメッセージが表示されることを確認してください。

バリデーションエラー

 なお、ブラウザのバリデーションチェックを有効にしたい場合は、formタグに付与したnovalidateを削除してください。削除する場合は必ずサーバーサイド側のバリデーションが有効であることを確認してから行ってください。

 続けて、更新処理の場合にもバリデーションを導入します。
 UserController.phpのedit()メソッドとupdate()メソッドを下記のように編集してください。追加するコードは基本的に登録処理の場合と一緒ですが、エラー時のリダイレクト先が編集フォームですので注意してください。

    /**
     * 編集フォームを表示
     *
     * @param integer $id
     * @return void
     */
    public function edit(int $id): void
    {
        $user = User::find($id);

        $sessionProvider = new NativeSessionProvider();
        $easyCSRF = new EasyCSRF($sessionProvider);
        $csrfToken = $easyCSRF->generate($_ENV['CSRF_SALT']);

        $oldInputs = [];
        if (!empty($_SESSION['old_inputs'])) {
            $oldInputs = $_SESSION['old_inputs'];
            unset($_SESSION['old_inputs']);
        }

        $errors = null;
        if (!empty($_SESSION['errors'])) {
            $errors = unserialize($_SESSION['errors']);
            unset($_SESSION['errors']);
        }

        (new View)->render('users/edit.php', compact('user', 'csrfToken', 'oldInputs', 'errors'), ['title' => '編集 | ユーザー管理']);
    }

    /**
     * 更新処理
     *
     * @param integer $id
     * @return void
     */
    public function update(int $id): void
    {
        $user = User::find($id);

        try {
            $sessionProvider = new NativeSessionProvider();
            $easyCSRF = new EasyCSRF($sessionProvider);
            $easyCSRF->check($_ENV['CSRF_SALT'], $_POST['csrf_token']);

            $params = [];
            foreach ($_POST as $key => $value) {
                $params[$key] = trim($value);
            }

            $ret = (new UserValidator($params))->validate();
            if (!$ret) {
                $_SESSION['old_inputs'] = $params;
                Flash::addMessageToSession('error', '入力内容をご確認ください。');
                (new Redirector)->to("users/{$user->id}/edit");
            }

            $user->update($params);

            Flash::addMessageToSession('success', 'ユーザーデータの更新に成功しました。');

            (new Redirector)->to('users');
        } catch(InvalidCsrfTokenException $e) {
            echo $e->getMessage();
        }
    }

 resources/views/users/edit.phpを下記のように修正してください。基本的にはcreate.phpと一緒ですが、直前の入力値を表示する箇所が異なります。直前の入力値が存在する場合はそれを使うのは一緒ですが、存在しない場合はデータベースから取得した値を使用します。

<?php ob_start(); ?>

<a href="/users" class="btn btn-secondary mt-5">戻る</a>
<form action="/users/<?= $user->id; ?>" method="POST" class="mt-5 <?php if (!empty($errors)) : ?>was-validated<?php endif; ?>" novalidate>
    <div class="mb-3">
        <label for="name" class="form-label">名前</label>
        <input type="text" id="name" class="form-control" name="name" maxlength="50" value="<?= $oldInputs['name'] ?? $user->name; ?>" required>
        <?php if (!empty($errors) && $errors->hasMessage('name')) : ?>
            <div class="invalid-feedback"><?= $errors->getMessage('name'); ?></div>
        <?php endif; ?>
    </div>
    <div class="mb-3">
        <label for="email" class="form-label">メールアドレス</label>
        <input type="text" id="name" class="form-control" name="email" maxlength="100" value="<?= $oldInputs['email'] ?? $user->email; ?>" required>
        <?php if (!empty($errors) && $errors->hasMessage('email')) : ?>
            <div class="invalid-feedback"><?= $errors->getMessage('email'); ?></div>
        <?php endif; ?>
    </div>
    <input type="hidden" name="csrf_token" value="<?= $csrfToken; ?>">
    <button type="submit" class="btn btn-primary">更新</button>
</form>

<?php $content = ob_get_clean(); ?>

 編集できたらhttp://localhost:8888/users/1/editにアクセスしてください。idの「1」の箇所はご自身の環境に合わせて調整してください。名前かメールアドレスを空白にして「更新」ボタンをクリックし、エラーメッセージが表示されることを確認してください。

バリデーションエラー

 まだまだ改善点はあると思いますが、いったん今回はこれで完成とします。
 素のPHPではシンプルなシステムを作るだけでもそれなりの量のコードが必要でした。次回はLaravelで同じシステムを作成したいと思います。どの位短いコードで同じ機能を実現できるかお楽しみに。
それではおつかれさまでした。

PHP/Laravelのシステム開発は株式会社パパグラムへぜひご相談ください。

いいなと思ったら応援しよう!