[PHP初心者]掲示板を作ってみた

[前回の記事](https://qiita.com/qwertyuiopngsdfg/items/597da67387723a5aedad)でログイン機能を作ってみました。
次は何を作ろうかなと思ったので考えた所掲示板を思いついたので作ってみました。
ただ作ると前回とほぼ変わらないので
今回はクラスやメソッド等を使って作ってみましたので、初心者さんが練習で掲示板を作る時の参考になればなと思います。
僕も使うのが初めてなので間違ってたら指摘していただけると助かります。

#完成した物
![circleanimationmuvie](https://raw.githubusercontent.com/qwertyuiopngsdfg/BBS_PHP/master/bbs.gif)

[Github](https://github.com/qwertyuiopngsdfg/BBS_PHP)
GithubのほうにCSSやらjsやら、スタイルを当てたindex.phpも一応載せてますが
gif画像を載せてみたいがために適当に作っただけなのでcssとjsは参考にしないほうが良いかもしれません。
ブラウザ間の差異等の確認もしてません。


#各種設定
```create.sql
create database bbs_users;

create user dbuser@localhost identified by 'yourpassword';

grant all on bbs_users.* to dbuser@localhost identified by 'yourpassword';

create table users (
  id int not null auto_increment primary key,
  name varchar(15) default '名無し', //ユーザーから何も入力が無い場合は名無しで保存
  body varchar(255) not null,
  password varchar(255) not null,
  created timestamp not null default current_timestamp //記事を投稿した日時を自動的に保存
);
```

```config.php
<?php
//ini_set('display_errors', 1);
define('DSN', 'mysql:host=localhost;dbname=bbs_users');
define('DB_USER', 'dbuser');
define('DB_PASS', 'yourpassword');
```

#index.php

```index.php
<?php

require_once('actions.php');

try {//order by id desc で、idの降順(大きい物から小さい順)に記事一覧を取得
  $result = $pdo->query("select * from users order by id desc"); 
  $count = $result->rowCount();
} catch (Exception $e) {
  echo $e->getMessage() . PHP_EOL;
}

$ptm = new PostTheMessage();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
	$message = $_POST;
	$ptm->post($message);
}
?>

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<title>掲示板</title>
	</head>
	<body>
		<div id="header">
			<h1>掲示板</h1>
			<p>現在の投稿<span><?= $count; ?></span>件</p>
		</div><!-- header -->
		<div id="main">
			<div id ="modal" class ="hidden">
				<form action="" method="post">
					<label for="name">名前</label>
					<input type="text" name="name" value="" id="name"><br>
					<label for="password">削除用パスワード</label>
					<input type="password" name="password" value=""><br>
					<textarea name="body" rows="8" cols="40" placeholder="ここにコメントを記入してください。"></textarea><br>
					<button type="submit" name="submit" id="submit">書き込む</button>
				</form>
				<p id ="close_modal">Close</p>
			</div><!-- modal -->
			<div id="mask" class="hidden"></div>
			<div id ="open_modal">
				<h2>投稿する</h2>
			</div>
			<div id="posts">
				<?php if ($count == 0): ?>
				<p>まだ投稿はありません。</p>
				<?php endif; ?>
				<dl>
                    <?php $i = 0; ?>
					<?php foreach ($result as $row) : ?>
					<dt class = "postrow <?php if ($i > 4) { echo 'post_hidden'; } ?>"> <!-- 5件以上は非表示にする。 -->
						<span><?= $count - $i; ?></span><span>名前：<?= h($row["name"]) ?></span>
						<span><?= h($row["created"])?> </span><br>
					</dt>
					<dd class = "postrow <?php if ($i > 4) { echo 'post_hidden'; } ?>"><!-- 5件以上は非表示にする。 -->
						<?=  nl2br(h($row["body"])) ?>
						<a href="delete.php?id=<?= h($row["id"]) ?>">削除</a>
					</dd>
                    <?php $i++ ?>
					<?php endforeach; ?>
				</dl>
				<div id="load_result"></div>
				<button id="load_more">全件表示</button>
			</div><!-- posts -->
		</div><!-- main -->
	</body>
</html>
```

当たり前かもしれませんが
post\(\$\_POST\)はできませんでした。
\$message = \$\_POSTとしてから
post\(\$message\)で、渡した先で\$message['name']など、formで渡した名前でそのまま使えます。


#actions.php

```actions.php
<?php

require_once('config.php');

function h($s){
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

try {
  $pdo = new PDO(DSN, DB_USER, DB_PASS);
  $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);;
} catch (Exception $e) {
  echo $e->getMessage() . PHP_EOL;
}

class PostTheMessage {

  public function post($message){
    try {
      // error check
      $validated_message = $this->_validatePost($message);
      // save
      $this->_save($validated_message);
      // redirect
      header('Location:http://' . $_SERVER['SERVER_NAME']);//リダイレクト処理
      exit; 
    } catch (\Exception $e) {
      echo $e->getMessage() . PHP_EOL;
    }
  }

  private function _validatePost($post) {
    if (mb_strlen($post["password"]) > 7 ) {
      $post["password"] = password_hash($post["password"], PASSWORD_DEFAULT);
    } else {
      $errors[] = '8文字以上のパスワードを設定してね。';
    }
    if(mb_strlen($post["name"]) > 15){
      $errors[] = '名前が長すぎるよ';
    }
    if (empty(trim($post["body"])) || mb_strlen($post["body"]) > 255) {
      $errors[] = '文章長すぎるか入力してないよ。';
    }
    if ($errors) {
      die(implode("<br />\n", $errors));
    }
    return $post;
  }

  private function _save($v_message) {
    try {
      $pdo = new PDO(DSN, DB_USER, DB_PASS);
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      if (empty($v_message['name'])) { //名無しの場合
        $stmt = $pdo->prepare("insert into users(body, password) values(?, ?)");
        $stmt->execute([$v_message["body"], $v_message["password"]]);
      }else{ //名前が入力されている場合
        $stmt = $pdo->prepare("insert into users(name, body, password) values(?, ?, ?)");
        $stmt->execute([$v_message["name"], $v_message["body"], $v_message["password"]]);
      }
    } catch (Exception $e) {
      echo $e->getMessage() . PHP_EOL;
      exit;
    }
  }

}
```

二重登録防止のためにリダイレクト処理を付けてます。
例）\$_SERVER['HTTP_HOST'] == 'localhost:8080'
例）\$_SERVER['SERVER_NAME'] == 'localhost'
みたいな感じでSERVER_NAMEにはポート番号が後ろにつかないので、ローカル開発環境等で勉強してる人は気を付けてください。
リダイレクト機能を付ける時に丁度良いので脆弱性のお勉強をしようと思ったのですが、まだ良く理解できてないので説明は辞めておきます。
興味のある人は「ヘッダーインジェクション」「オープンリダイレクト」なんかで調べてみてください。

ユーザーからの入力チェックで、エラーを$errors[]に入れこんでますが
例外としてまとめて投げる方法もあるみたいなので、良かったら試してみてください。
その場合は古い記事ではありますが
[[PHP] まとめて例外をスローする小技](https://qiita.com/mpyw/items/6bd99ff62571c02feaa1)が大変参考になりました。

#delete.php

```delete.php
<?php

require_once('actions.php');

if (is_numeric($_GET["id"])) {
  $user_id = $_GET["id"];
} else {
  echo '不正な情報が入力がされた可能性があります。';
  exit;
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindParam(':id', $user_id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  if(password_verify($_POST['password'], $row['password'])) {
    $stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");
    $stmt->bindParam(':id', $user_id, PDO::PARAM_INT);
    $stmt->execute();
    echo '削除が成功しました。';
    exit;
  }else{
    echo 'パスワードが間違っています。';
  }
}
?>

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>delete</title>
    <link rel="stylesheet" href="/css/styles.css">
  </head>
  <body>
    <dl>
      <dt>
        <span style="color: #e67e22;">名前：<?= h($row["name"]) ?></span>
        <span style ="font-size: 15px; color: #a0a0a0;"><?= h($row["created"])?></span><br>
      </dt>
      <dd>
        <?=  nl2br(h($row["body"])) ?>
      </dd>
    </dl>
    <p>この記事を削除します。宜しければパスワードを入力してください</p>
    <form action="" method="post">
      <label for="password">パスワードを入力してください:</label>
      <input type="password" name="password">
      <input type="submit" value ="submit" style ="background-color:white">
    </form>
  </body>
</html>
```

index.phpから記事のIDをGETで送っています。
記事の呼び出しや削除をactions.phpの方で行うべきか否かを非常に悩みました。
大した行数も無いのでそのまま書きましたが、気になる人はactions.phpの方で書いてみてください。


#最後に
せっかくqiitaのアカウントを作ったのに全然記事が書けてないので無理やり記事にしてしまいました。
1人でも役にたったなと思う人がいれば幸い

次はCMSでも作ろうかなと思ってます。