SHINYA TECH

公務員(行政職)から34歳でエンジニア転職した人

#25 ここまでの復習(WEB版タスク管理ツールの作成)

f:id:kshinya-tech:20220315092523p:plain 前回、ターミナル上で動作するタスク管理ツールを作成しました。

shinya-tech.com

今回はこれまで(#12〜#24)の復習を兼ねて、WEB版のタスク管理ツールの作成を行いました。

実装した内容はタスクの登録とタスク一覧の表示のみ。

削除及び修正は実装していません。

完成後の画面

タスク登録画面

gyazo.com

タスク一覧画面

gyazo.com

ディレクトリ構成

tasks
├── create.php
├── index.php
├── initialize_tasks_table.php
├── lib
│   ├── escape.php
│   └── mysqli.php
├── new.php
└── views
    ├── index.php
    ├── layout.php
    └── new.php

各コード

views/index.php

<a href="new.php" class="btn btn-primary mb-4">タスクを登録する</a>
<main>
    <?php if (count($tasks) > 0) : ?>
        <table class="table table-hover">
            <thead class="thead-dark text-center">
                <tr>
                    <th scope="col">タスク名</th>
                    <th scope="col">期限</th>
                    <th scope="col">進捗</th>
                    <th scope="col">備考</th>
                </tr>
            </thead>
            <?php foreach ($tasks as $task) : ?>
                <tbody>
                    <tr>
                        <td scope="row"><?php echo escape($task['name']); ?></td>
                        <td class="text-center"><?php echo escape(date('Y/m/d', strtotime(($task['limit_date'])))); ?></td>
                        <td class="text-center"><?php echo escape($task['status']); ?></td>
                        <td><?php echo nl2br(escape($task['remarks'])); ?></td>
                    </tr>
                </tbody>
            <?php endforeach; ?>
        </table>
    <?php else : ?>
        <p>タスクが登録されていません。</p>
    <?php endif; ?>
</main>

views/layout.php

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="../stylesheets/css/app.css">

    <!-- $titleはlayout.phpの呼び出し元で定義 -->
    <title><?php echo $title; ?></title>
</head>

<body>
    <header class="navbar shadow-sm p-3 mb-4 bg-white">
        <h1 class="h2">
            <a class="text-decoration-none text-reset" href="index.php">タスク管理ツール</a>
        </h1>
    </header>
    <div class="container">
        <!-- $contentはlayout.phpの呼び出し元で定義 -->
        <?php include $content; ?>
    </div>
</body>

</html>

views/new.php

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="../stylesheets/css/app.css">
    <title>タスクリスト</title>
</head>

<body>
    <header class="navbar shadow-sm p-3 mb-4 bg-white">
        <h1 class="h2">
            <a class="text-decoration-none text-reset" href="index.php">タスクリスト</a>
        </h1>
    </header>

    <div class="container text-dark mb-4">
        <h2 class="h3">登録画面</h2>
        <form action="create.php" method="POST">
            <!-- バリデーションエラーを表示させる -->
            <?php if (count($errors)) : ?>
                <ul class="text-danger">
                    <?php foreach ($errors as $error) : ?>
                        <li><?php echo $error; ?> </li>
                    <?php endforeach; ?>
                </ul>
            <?php endif; ?>
            <div class="form-group">
                <label for="name">タスク名</label>
                <input type="text" class="form-control" id="name" name="name" value="<?php echo $task['name'] ?>">
            </div>
            <div class="form-group">
                <label for="limit_date">期限</label>
                <input type="date" class="form-control" id="limit_date" name="limit_date" value="<?php echo $task['limit_date'] ?>">
            </div>
            <div class="form-group">
                <label>進捗</label>
                <div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="status" id="not_yet" value="未" <?php echo ($task['status'] === '未') ? 'checked' : ''; ?>>
                        <label class="form-check-label" for="not_yet"></label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="status" id="doing" value="着手" <?php echo ($task['status'] === '着手') ? 'checked' : ''; ?>>
                        <label class="form-check-label" for="doing">着手</label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="status" id="finish" value="完了" <?php echo ($task['status'] === '完了') ? 'checked' : ''; ?>>
                        <label class="form-check-label" for="finish">完了</label>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label for="remarks">備考</label>
                <textarea class="form-control" name="remarks" id="remarks" cols="30" rows="2"></textarea>
            </div>
            <button type="submit" class="btn btn-primary">登録する</button>
        </form>
    </div>
</body>

</html>

lib/escape.php

<?php

function escape($string)
{
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}

lib/mysqli.php

<?php
// Composerのautoloadを読み込む。
// これにより各ライブラリをPHPファイル内で呼び出せるようになる
// 後で理解すればOK.composerを使用するときは書いておく。
require __DIR__ . '/../../vendor/autoload.php';


// データベースとの接続の関数を定義
function dbConnect()
{
    // 以下の記述はオブジェクト指向を学んでから理解すればOK
    // 環境変数をPHPファイルに読み込む
    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..');
    $dotenv->load();

    $dbHost = $_ENV['DB_HOST'];
    $dbUsername = $_ENV['DB_USERNAME'];
    $dbPassword = $_ENV['DB_PASSWORD'];
    $dbDatabase = $_ENV['DB_DATABASE'];

    $link = mysqli_connect($dbHost, $dbUsername, $dbPassword, $dbDatabase);
    if (!$link) {
        echo 'Error: データベースに接続できません' . PHP_EOL;
        echo 'Debugging error: ' . mysqli_connect_error() . PHP_EOL;
        exit;
    }
    return $link;
}

create.php

<?php
// mysqli.phpファイルを読み込む
require_once __DIR__ . '/lib/mysqli.php';

// データベースにデータを登録する関数を定義する
function createTask($link, $task)
{
    $sql = <<<EOT
INSERT INTO tasks (
    name,
    limit_date,
    status,
    remarks
) VALUES (
    "{$task['name']}",
    "{$task['limit_date']}",
    "{$task['status']}",
    "{$task['remarks']}"
);
EOT;
    $result = mysqli_query($link, $sql);
    // $resultがfalseなら、エラーログを吐き出す
    if (!$result) {
        error_log('Error: fail to create task');
        error_log('Debugging Error: ' . mysqli_error($link));
    }
}

// バリデーション関数を定義
function validate($task)
{
    // エラーは複数発生しうるので配列で格納する
    $errors = [];

    // タスク名についてのバリデーション
    // 空欄及び文字列の最大値を超えていないかチェック
    if (!strlen($task['name'])) {
        $errors['name'] = 'タスク名を入力してください';
    } elseif (mb_strlen($task['name']) > 255) {
        $errors['name'] = 'タスク名は255文字以内で入力してください';
    }

    // 期限(日付)についてのバリデーション
    // 条件1 : 空欄の場合にエラー
    // 条件2 : 日付の形式(YYYY-MM-DD)でない場合にエラー
    // 条件3 : ありえない日付の場合にエラー

    // 条件3でcheckdate()を使用するため、'-'で日付を区切って配列として格納
    // 例)2020-10-8 -> 2020 10 8 に分割する
    $dates = explode('-', $task['limit_date']);
    if (!strlen($task['limit_date'])) {
        $errors['limit_date'] = '期限を入力してください';
    } elseif (count($dates) !== 3) {
        $errors['limit_date'] = '期限を正しい形式で入力してください';

        // checkdateの引数は(月, 日, 年)とする必要があるため、以下のようにする
    } elseif (!checkdate($dates[1], $dates[2], $dates[0])) {
        $errors['limit_date'] = '期限を正しい日付で入力してください';
    }

    // 進捗についてのバリデーション
    if (!strlen($task['status'])) {
        $errors['status'] = '進捗を入力してください';
    } elseif (!in_array($task['status'], ['未', '着手', '完了'])) {
        $errors['status'] = '進捗は「未」、「着手」、「完了」のいずれかを入力してください';
    }

    // 備考についてのバリデーション
    if (mb_strlen($task['remarks']) > 255) {
        $errors['remarks'] = '備考は255文字以内で入力してください';
    }

    return $errors;
}


// HTTPメソッドがPOSTだったら
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // POSTされたタスク情報を変数に格納する
    // なお、status(進捗)はラジオボタンのため、チェックされていないとデータが送信されず、エラーとなる
    // そのため、以下を追記している

    $status = '';
    if (array_key_exists('status', $_POST)) {
        $status = $_POST['status'];
    }
    $task = [
        'name' => $_POST['name'],
        'limit_date' => $_POST['limit_date'],
        'status' => $status,
        'remarks' => $_POST['remarks'],
    ];

    // バリデーションする
    $errors = validate($task);

    // バリデーションエラーがなければ
    if (!count($errors)) {
        // データベースに接続する
        $link = dbConnect();
        // データベースにデータを登録する
        createTask($link, $task);
        // データベースとの接続を切断する
        mysqli_close($link);
        // index.phpへリダイレクト
        header("Location: index.php");
    }
    // もしエラーがあれば、以下に処理を続行
    include 'views/new.php';
}

index.php

<?php

require_once __DIR__ . '/lib/escape.php';
require_once __DIR__ . '/lib/mysqli.php';

function listTasks($link)
{
    $tasks = [];
    $sql = 'SELECT name, limit_date, status, remarks FROM tasks';
    $results = mysqli_query($link, $sql);

    while ($task = mysqli_fetch_assoc($results)) {
        $tasks[] = $task;
    }

    mysqli_free_result($results);

    return $tasks;
}

$link = dbConnect();
$tasks = listTasks($link);

// layout.php内の変数に渡す
$title = 'タスク一覧';
$content = __DIR__ . '/views/index.php';

// layout.phpを取り込む
include __DIR__ . '/views/layout.php';

new.php

<?php
// $errorsが未定義だとエラーとなるため、初期化
$errors = [];

// $taskが未定義だとエラーとなるため、初期化
$task = [
    'name' => '',
    'limit_date' => '',
    'status' => '',
    'remarks' => '',
];

// views/new.phpを取り込む
include 'views/new.php';

initialize_tasks_table.php

<?php
// mysqli.phpファイルを読み込む
require_once __DIR__ . '/lib/mysqli.php';

// tasksテーブルが既に存在していたら、削除する関数を定義
function dropTable($link)
{
    $dropTableSql = 'DROP TABLE IF EXISTS tasks';
    $result = mysqli_query($link, $dropTableSql);
    if ($result) {
        echo 'テーブルを削除しました' . PHP_EOL;
    } else {
        echo 'Error: テーブルの削除に失敗しました' . PHP_EOL;
        echo 'Debugging Error: ' . mysqli_error($link) . PHP_EOL . PHP_EOL;
    }
}

// tasksテーブルを作成する関数を定義
function createTable($link)
{
    $createTableSql = <<<EOT
CREATE TABLE tasks (
    id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    limit_date DATETIME,
    status VARCHAR(30),
    remarks VARCHAR(255),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) DEFAULT CHARACTER SET = utf8mb4;
EOT;
    $result = mysqli_query($link, $createTableSql);
    if ($result) {
        echo 'テーブルを作成しました' . PHP_EOL;
    } else {
        echo 'Error: テーブルの作成に失敗しました' . PHP_EOL;
        echo 'Debugging Error: ' . mysqli_error($link) . PHP_EOL . PHP_EOL;
    }
}

// データベースとの接続 -> テーブルの初期化(削除・作成)-> データベースの切断 を実行
$link = dbConnect();
dropTable($link);
createTable($link);
mysqli_close($link);