PHP+MySQLでポートフォリオ作成

# 記事の概要

私が作成したポートフォリオの解説です。
作った背景から、作成手順、機能、工夫したところ、課題を
まとめました。

# 背景

私は愛知県名古屋市東山を拠点とする社会人バンドサークルに所属しています。
そこではサークルメンバー同士でバンドを組み、定期的にライブを開催します。

この夏のライブでは1日12組の3days、計**36**組ものバンドが熱演を繰り広げる予定です。

このライブのおよそ3ヶ月前のミーティングで、出演バンドを集計するのですが、
今まで紙ノートにバンド名と出演メンバー、出演日程を記録しており、
ホームページでの情報公開もただ文章で並べただけのものでした。

それではメンテナンス性が悪く、簡単に編集が出来る状態になっていないことから、
いつでもどこでもライブ出演バンドを登録し表示出来るWebアプリを作ろうと考えました。

また、現職を退職し、Web系開発企業のバックエンドエンジニアになろうと考えていたので、
このアプリを転職活動のポートフォリオとすることに決めました。

# 目的

- PHPを用いたWebアプリケーション開発経験を積む
- フルスクラッチ開発によってWebアプリの基本的な構成、動作を知る
- Web系企業さんの即戦力になれる技術を身につける(そこまでは厳しかった)
- 作成アプリによってバンドサークルのライブ運営を円滑化する

# スペック

<dl>
 <dt>言語</dt>
 <dd>PHP 7.1.23</dd>
</dl>
<dl>
 <dt>DBMS</dt>
 <dd>MySQL 8.0.15</dd>
</dl>
<dl>
 <dt>CSSフレームワーク</dt>
 <dd>milligram</dd>
</dl>
<dl>
 <dt>開発環境</dt>
 <dd>MacOS Mojave 10.14.5</dd>
 <dd>Apache 2.4.34</dd>
</dl>
<dl>
 <dt>バージョン管理</dy>
 <dd>Git 2.21.0</dd>
</dl>
<dl>
 <dt>本番環境</dt>
 <dd>Heroku</dd>
</dl>

# 主な機能

サークル内でのライブに出演するバンドを管理することができます。

- ライブ登録

ライブ名とライブIDを入力することで、開催するライブを登録することができます。

![live_insert.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/8fefee32-91ff-df7f-6cb6-3f23da0dba77.gif)


- メンバー登録

名前とメンバーIDを登録することでメンバー登録ができます。
一度登録したら、そのIDで複数のバンドに登録することができ、
バンドやライブを消去しても、メンバーのデータは消えません。

![member_insert.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/3ed8faf6-98c6-a91d-4b6b-da6081dd6f17.gif)



- バンド登録

バンド名、出演順、持ち時間、バンドIDを入力することでバンド登録をすることができます。
バンドはライブと関連し、ライブが親データとなっており、ライブが消去されればバンドも消去されます。

![band_insert.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/d1cad56a-4b37-ebe8-a72b-9fa4dc943c51.gif)


- バンドメンバー結成

バンドに参加するメンバーのIDを入力することにより、
バンドメンバーを登録できます。
もちろんバンドが親データとなっており、
バンドが消えたりライブが消えたら、
「メンバー結成データ」のみが消去されます。
メンバー情報自体は消去されません。

![formation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/51b440c7-e370-94b7-58ca-24e8e81f180b.gif)


- 各項目の削除

ライブ定府、バンド情報、メンバー情報、バンドメンバー結成情報を
それぞれごとに削除することができます。
前述した通り、親データが消えた場合、それに所属する子データも消去されます。

# 開発手順

1. 要件定義
1. 環境選定
1. データベース設計
1. GUI設計
1. コーディング
1. Herokuデプロイ

### 1.要件定義

今回作成するアプリに必要な機能は

- ライブ登録
- バンド登録
- メンバー登録

の為、それらの情報が保存できるデータベースと、
データベースからユーザーが分かりやすいようにまとめて表示する
動的なビューの機能が必要となる。

また、ユーザーが登録〜削除まで行えるように、
各項目に対する登録画面、削除画面を設ける。

### 2.環境選定

学習当初はRuby、Ruby on Railsを学習していたが、

- 言語としての需要
- 初心者にとっての難易度
- Webアプリケーションの仕組みを1から作って理解する

という観点から見て、使用言語をPHPとしました。

DBMSは、PHPを調べた時に関連して取り上げられていることが多い、
情報量の多さを考慮し、MySQLの使用を決めました。

そもそもLAMP環境としての情報が多いですね。
なのでwebサーバーはApacheを使用。

ただ、上記環境はMacOSローカル上で構築しました。
Windowsでドットインストールやっていた時はVagrantを用いて
CentOS上で練習していたので、その時と違った環境でやりたかったのと、
単純にMacBookを買った嬉しさにより、そうなりました。

### 3.データベース設計

必要機能よりどんな情報を保存するか、
それらをどう関連付けて管理するか考えながら
データベース設計を行いました。

データベース設計のノウハウは、
書籍「達人に学ぶDB設計徹底指南書」にて学習。
正規化やバッドノウハウ、グレーノウハウを理解した上で
設計を進めれた事が、後工程のアプリ内でのSQL文発行にて
役立ちました。

ただ、下記のGUI設計で不便さに気づき、そこから立ち返って
修正することもありました。
そうなるとGUI設計も1からやり直しとなりましたが...

使用ソフト:[WWW SQL Designer](https://ondras.zarovi.cz/sql/demo/)

![スクリーンショット 2019-06-08 22.31.51-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/1a11540d-cfdd-7cb8-f106-fa56929a1bdb.png)


### 4.GUI設計

データベースの仕様が決まったところで、
そのデータをどう入力して、どう出力するか、を
簡易的な画面遷移図にまとめました。

使用ソフト:PlantUML Salt

![スクリーンショット 2019-05-26 16.12.07-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/138dc14f-ea65-8e67-4fa8-786ad45a8b3e.png)

### 5.コーディング

設計が固まったところで、コーディングをスタート。

#### 5.1データベース作成

ローカル環境のMySQLサーバに
専用ユーザ作成し、データベースを作成。
その後、テーブルを作成。


<details><summary>テーブル作成SQL</summary>
<div>

```sql
CREATE TABLE IF NOT EXISTS live(
    live_id CHAR(7) PRIMARY KEY,
    live_name VARCHAR(30)
);

CREATE TABLE IF NOT EXISTS band(
    band_id CHAR(4) PRIMARY KEY,
    live_id CHAR(7),
    band_name VARCHAR(30),
    performance_time VARCHAR(20),
    performance_num INT
);

CREATE TABLE IF NOT EXISTS formation(
    member_id CHAR(7),
    band_id CHAR(4),
    PRIMARY KEY(member_id,band_id)
);

CREATE TABLE IF NOT EXISTS member(
    member_id CHAR(7) PRIMARY KEY,
    member_name VARCHAR(20)
);

```
</div>
</details>

#### 5.2 全体の形、フォームをHTMLで記述

GUI設計に従い画面を構築。

#### 5.3 データベース接続確認

PDOを用い、データベースに接続(PDOオブジェクトの生成)。
try~catchを用い、成功しているかどうか判断。

安全性を考慮し、データベース情報は別ファイルに記述し、
require_onceで各画面に適用する。
GitHubへのpush時は、
.gitignoreにデータベース情報が書かれたファイルを記述し
公開されないようにした。

<details>
<summary>(一例)データベース接続部分</summary>
<div>

```php:データベース接続
//DB設定読み込み
require_once __DIR__ . '/conf/database_conf.php';


//例外処理
try{

    //DB接続
    $db = new PDO("mysql:host=$dbServer;dbname=$dbName;charset=utf8","$dbUser","$dbPass");
    $db ->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $db ->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
}catch (PDOException $e) {
    echo 'データベースエラー発生：' . h($e->getMessage());
}catch (Exception $e){
    echo 'エラー発生：' . h($e->getMessage());
}

```

</div>
</details>

#### 5.4 PDOにてSQL文発行

入力、出力、削除、更新に必要なSQL文を
カラム名にプレースホルダーを代入する形で準備。

その後SQL文のプレースホルダーに値をバインドし、
クエリを実行。

<details>
<summary>(一例)バンドごとメンバー抽出部分</summary>
<div>

```php:バンドごとメンバー抽出部分
//SQL準備
$sql = "SELECT
    member.member_name
    FROM member
        INNER JOIN formation
            ON member.member_id=formation.member_id
        INNER JOIN band
            ON formation.band_id=band.band_id
            WHERE band.live_id=:live_id AND band.band_id=:band_id";
$prepare = $db->prepare($sql);
//ライブIDでバインド
$prepare -> bindValue(':live_id',$live_id,PDO::PARAM_STR);
//バンドIDでバインド
$prepare -> bindValue(':band_id',$band_id,PDO::PARAM_STR);
//クエリ実行
$prepare->execute();
```

</div>
</details>

#### 5.5 データベース内容表示

クエリを実行した結果を格納したオブジェクトを
foreachで1レコードずつHTMLのテーブル上に表示させていく。

<details>
<summary>(一例)メンバー一覧表示部分</summary>
<div>

```php:メンバー一覧表示部分
<table>
    <thead><th>メンバーID</th><th>名前</th><th></th></thead>
    <tbody>

<?php
//SQL準備
$sql = 'SELECT * FROM member ORDER BY member_id';
$prepare = $db->prepare($sql);
//クエリ実行
$prepare->execute();

//メンバー名をひとつずつrowに設定
foreach ($prepare as $row) {
?>

        <tr>
            <td>
                <!--メンバーID表示-->
                <?= h($row['member_id']) ?>
            </td>
            <td>
                <!--名前表示-->
                <?= h($row['member_name']) ?>
            </td>
            <td>
                <!--削除ボタン表示 POSTメソッドでmember_idを削除部分に渡す-->
                <form method="POST">
                    <input type="submit" name="member_delete" value="delete">
                    <input type="hidden" name="member_id" value="<?= $row['member_id'] ?>">
                </form>
            </td>
        </tr>

<?php
}
?>

    </tbody>
</table>
```

</div>
</details>

#### 5.6 HTTP通信

データベースへの入力や削除、更新部分はPOST、
データベースからの出力はGETを用いた。

<details>
<summary>(一例)ライブ追加部分と入力フォーム</summary>
<div>

```php:ライブ追加部分
try {
    //DB接続
    $db = new PDO("mysql:host=$dbServer;dbname=$dbName;charset=utf8","$dbUser","$dbPass");
    $db ->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $db ->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

    //追加ボタンが押されたら
    if (isset($_POST['live_insert'])) {
        //live_nameバリデーション
        validation($_POST['live_name'],'ライブ名','');
        //追加するライブ名を取得
        $live_name = $_POST['live_name'];
        //live_idバリデーション
        validation($_POST['live_id'],'ライブID',7);
        //追加するライブIDを取得
        $live_id = $_POST['live_id'];

        //SQL準備(新規ライブIDとライブ名をliveテーブルに追加)
        $sql = "INSERT INTO live (live_id,live_name) VALUES (:live_id,:live_name)";
        $prepare = $db -> prepare($sql);
        //live_idに挿入する変数と型を指定
        $prepare -> bindValue(':live_id',$live_id,PDO::PARAM_STR);
        //live_nameに挿入する変数と型を指定
        $prepare -> bindValue(':live_name',$live_name,PDO::PARAM_STR);
        //クエリ実行
        $prepare -> execute();

        echo '<p>追加完了</p>';
    }
} catch (PDOException $e) {
    echo 'データベースエラー発生:' . h($e->getMessage());
}
catch (Exception $e){
    echo 'その他エラー発生:' . h($e->getMessage());
}
```

```php:入力フォーム
<form method="POST">
    <fieldset>
        <div class="colomn">
            <label for="liveName">ライブ名</label>
            <input type="text" name="live_name" maxlength="30" id="liveName" placeholder="ライブ名">
            <label for="liveID">ライブID</label>
            <input type="text" name="live_id" maxlength="7" id="liveId" placeholder="(例)201901A"><br>
            <p>半角英数字7文字で入力してください</p>
            <p>入力例：201901A→(2019年1回目のライブのA日程)</p>
            <p>ライブIDが被ると登録できません</p>
            <p>前のページで他ライブのIDを確認してから入力してください</p>
            <input type="submit" name="live_insert" value="add">
        </div>
    </fieldset>
</form>
```

</div>
</details>

#### 5.7 XSS対策サニタイジング

htmlspecialchars関数を用いたサニタイジングを実施。
かさばるので関数化し別ファイルへ。

<details>
<summary>サニタイジング関数</summary>
<div>

```php:サニタイジング関数
function h($var){//htmlでエスケープ処理をする関数
    //引数が配列だった場合はすべての要素にh()関数を実行する
    if(is_array($var)){
        return array_map('h',$var);
    }
    //引数に対しサニタイジング
    else{
        return htmlspecialchars($var,ENT_QUOTES,'UTF-8');
    }
}
```

</div>
</details>

#### 5.8 バリデーション

汎用的なバリデーションをまとめ、関数化し別ファイルへ。
意図しない入力値であれば、どの入力値がダメであったか表示できるよう、
入力値の分類を引数に指定できるようにした。

<details>
<summary>バリデーション関数</summary>
<div>

```php:バリデーション関数
//汎用バリデーション関数
//第一引数:バリデーション対象のデータ(char) 第二引数:データの名前(char) 第三引数:文字列の長さ(int)
function validation($data,$data_name,$data_len){
    //汎用バリデーション
    if(!isset($data) || !is_string($data) || $data === ''){
        //エラーをExceptionクラスに投げる
        throw new Exception($data_name . 'が不正な値です');
    }
    //データの文字数が指定されていれば
    if($data_len !== ''){
        //文字数バリデーション
        if(strlen($data) !== $data_len){
            //エラーをErrorExceptionクラスに投げる
            throw new Exception($data_name . 'の長さが不正です');
        }
    }
}
```

```php:運用例
//live_idバリデーション
validation($_POST['live_id'],'ライブID',7);
//追加するライブIDを取得
$live_id = $_POST['live_id'];
```

</div>
</details>


#### 5.9 CSSフレームワーク実装

超軽量CSSフレームワーク「milligram」を使用し、
フロントエンドは最小の労力での完成を目指した。

```php

<link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
```

### 6.Herokuデプロイ

本番環境としてのプラットホームは
Ruby on Railsチュートリアルで使い方を習得したHerokuを使用。
GitHubとの連携機能があった為、
GitHubにpushしたリポジトリをそこからHerokuにデプロイ。

#課題点

一区切りついたところで完成としましたが、
課題点は多数あるのでリストアップします。
今後の学習の指標とします。

###バリデーションが不十分

現状の汎用的なバリデーションのみだと
nullや文字数相違は網を張れるが、
意図しない入力形式が入力できてしまう。

![スクリーンショット 2019-06-09 22.15.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/43ded239-526b-3000-0f82-842f6cef854e.png)


また、現状はPOST受信時のみにバリデーションを
行なっているが、データベース側でもチェックをした方が良い。

フロント側のチェックは、トラフィックを発生させずにチェックする。
バック側のチェックは、直POSTによる不正データ登録防止。

###Cookieを使用していない

Webアプリケーションのポートフォリオとして、
基本的な技術であるCookieを使用していない。
あくまでポートフォリオとしての欠陥であるが、
著者に使い方が身についていないという事。

###セッションを使用していない

上記と同様の理由。
このWebアプリにはログイン機能が無い為、
必要無かった。
しかしセッションハイジャック等のリスクもあるので
これからバックエンドエンジニアとして成長するために
必ず習得する必要がある。

###CSRF対策が為されていない

セッションを用いたトークンを使用していないので
CSRF対策が出来ていない。

###ボタンが大きい

バンド管理画面はデータ表示領域よりボタンの方が大きく
無駄な配置となっている。
フロントエンドを出来るだけ簡素に作る為
CSSフレームワークに頼ったが、
ユーザビリティを向上させるために
CSSで調整する方法を今後身に付けたい。

###ボタンの位置がおかしい

テーブル内のボタンの位置が上に寄っている。
CSSでの細かい修正が出来ていないため。

![スクリーンショット 2019-06-07 20.35.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360072/3288955a-a79a-7551-072a-e865e9961a9e.png)

###スマホだと見にくい

表示が横長画面を前提として作られている。
このアプリの目的はバンドサークルのライブ運営を円滑にするため、
現場でスマホを用いて操作することが多くなると予測できる。
このままではスマホ上で操作しずらく、かえって不便。

###ユーザーがIDを意識して操作しなければいけない

このWebアプリのデータベースの
各テーブルの一意キーは文字列IDの形式を取っている。
それにより、新規登録で、
ユーザーが普通は意識しないIDを入力したり、
一意キーの為に、重複しないよう気を使う必要がある。

自然キーに拘った理由は、「達人に学ぶDB設計徹底指南書」内で、
まともな理論家ならばサロゲートキーを推奨しない、とあり
それを鵜呑みにした結果である。
ユーザビリティを後回しにし、思考を柔軟に出来なかったことが要因。

# 参考文献

- [達人に学ぶDB設計徹底指南書](https://www.shoeisha.co.jp/book/detail/9784798124704)
- [Ruby on Rails チュートリアル](https://railstutorial.jp)
- [基礎からのMySQL](https://www.sbcr.jp/products/4797393118.html)
- [PHP逆引きレシピ](http://php-recipe.com)

##2019/6/10追記

GitHub晒します。
https://github.com/RinyuDrvo/soul_live_app