PHP --- フレームワークを利用しないでToDoアプリ作成(3)
前回は、ルーティングの機能まで実装しました。
PHP --- フレームワークを利用しないでToDoアプリ作成(2) - 何でもプログラミング
今回はデータベースと連携して、実際のToDoアプリを構築してみたいと思います。
データベースはSQLiteを利用しており、CSRF対策はまだ入れていません。
今回の作成物
下図のように、ToDoの一覧ページと、追加ページからなります。
簡単のため、編集の機能は搭載してありません。
View部分は、layout.php、todoList.php、todoAdd.phpの3ファイルからなります。
データベースは、database.sqliteという名前のファイルで、下記のtodoテーブルが定義してあります。
id | INTEGER | PRIMARY KEY AUTOINCREMENT |
date | TEXT | |
text | TEXT |
layout.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title><?php safeEcho($title) ?></title> </head> <body> <header> <p>ToDo</p> </header> <main> <?php echo $body ?> </main> </body> </html>
todoList.php
<?php ob_start(); ob_implicit_flush(0); ?> <a href="/add">追加</a> <table> <?php foreach ($todos as $todo) { ?> <tr> <td> <?php safeEcho($todo->date->format('Y/m/d')) ?> </td> <td> <?php safeEcho($todo->text) ?> </td> <td> <form action="/delete" method="post"> <input type="hidden" name="id" value="<?php safeEcho($todo->id) ?>"> <button type="submit">削除</button> </form> </td> </tr> <?php } ?> </table> <?php $title = 'ToDo一覧'; $body = ob_get_clean(); require 'layout.php'; ?>
todoAdd.php
<?php ob_start(); ob_implicit_flush(0); ?> <?php if (empty($errors) === false) { ?> <ul> <?php foreach ($errors as $error) { ?> <li> <?php safeEcho($error) ?> </li> <?php } ?> </ul> <?php } ?> <form action="/add" method="post"> <ul> <li> <label> 日付 <input type="text" name="date" value="<?php safeEcho($date) ?>"> </label> </li> <li> <label> テキスト <input type="text" name="text" value="<?php safeEcho($text) ?>"> </label> </li> <li> <button type="submit">送信</button> </li> </ul> </form> <?php $title = 'ToDo追加'; $body = ob_get_clean(); require 'layout.php'; ?>
index.php
<?php function connectDB() : PDO { return connectSQLite('database.sqlite'); } class ToDo { const DATE_FORMAT = 'Y/m/d'; public $id; public $date; public $text; public static function insert(PDO $db, DateTime $date, string $text) : void { $db->prepare('INSERT INTO todo(date, text) VALUES (?, ?)') ->execute([ $date->format(self::DATE_FORMAT), $text ]); } public static function delete(PDO $db, int $id) : void { $db->prepare('DELETE FROM todo WHERE id = ?') ->execute([ $id ]); } public static function exists(PDO $db, int $id) : bool { $statement = $db->prepare('SELECT * FROM todo WHERE id = ?'); $statement->execute([ $id ]); return empty($statement->fetchAll()) === false; } public static function fetchAll(PDO $db) : array { $statement = $db->prepare('SELECT * FROM todo ORDER BY date DESC'); $statement->execute(); return array_map(function($x) { $todo = new ToDo(); $todo->id = $x['id']; $todo->date = DateTime::createFromFormat(self::DATE_FORMAT, $x['date']); $todo->text = $x['text']; return $todo; }, $statement->fetchAll()); } } function todoList() : Response { $db = connectDB(); $todos = ToDo::fetchAll($db); $content = render('todoList.php', compact('todos')); return Response::ok($content); } function todoAdd() : Response { $errors = []; $date = (new DateTime())->format('Y/m/d'); $text = ''; $content = render('todoAdd.php', compact('errors', 'date', 'text')); return Response::ok($content); } function post_todoAdd() : Response { $date = assertPOST('date'); $text = assertPOST('text'); $validate = new Validate(); $validate->isTrue(empty($text) === false, 'テキストが空です。'); $validate->isTrue(mb_strlen($text) <= 100, 'テキストは100文字までです。'); $dateObj = DateTime::createFromFormat('Y/m/d', $date); $validate->isTrue($dateObj !== false, '日付が****/**/**の形式ではありません。'); $errors = $validate->messages; if (empty($errors)) { $db = connectDB(); ToDo::insert($db, $dateObj, $text); return Response::redirect('/'); } else { $content = render('todoAdd.php', compact('errors', 'date', 'text')); return Response::badRequest($content); } } function post_todoDelete() : Response { $id = assertPOST('id'); $db = connectDB(); sqlTransaction($db, function($db) use ($id) { assert400(ToDo::exists($db, $id)); ToDo::delete($db, $id); }); return Response::redirect('/'); } function onError(Exception $e) : Response { if ($e instanceof Exception400) { return Response::badRequest('400 Bad Request'); } else if ($e instanceof Exception404) { return Response::notFound('404 Not Found'); } else { return Response::internalServerError('500 Internal Server Error'); } } $routesGET = [ '/' => 'todoList', '/add' => 'todoAdd', ]; $routesPOST = [ '/add' => 'post_todoAdd', '/delete' => 'post_todoDelete' ]; respond(getPathInfo(), $routesGET, $routesPOST, 'onError');
connectSQLite
SQLiteに接続する関数になります。
エラーを例外で送出、連想配列でデータを取得、静的プレースホルダーを利用するようにしています。
なお、データベースファイルは絶対パスで指定する必要があります。
<?php function connectSQLite(string $name) : PDO { $db = new PDO("sqlite:{$name}"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); return $db; }
sqlTransaction
トランザクションを毎回記述するのを回避するため、callableを受け取る関数を定義しました。
<?php function sqlTransaction(PDO $db, callable $f) : void { try { $db->beginTransaction(); $f($db); $db->commit(); } catch (Exception $e) { $db->rollBack(); throw $e; } }
Validate
POSTパラメータの検証の記述量削減のため、下記のようなクラスを準備してみました。
<?php class Validate { public $messages = []; public function isTrue(bool $condition, string $message) : void { if ($condition === false) { array_push($this->messages, $message); } } }
ついでにException400を送出するassert関数も定義してみました。
<?php function assert400(bool $condition) { if ($condition === false) { throw new Exception400(); } } function assertPOST(string $key) { if (isset($_POST[$key])) { return $_POST[$key]; } else { throw new Exception400(); } }