おべんきょうメモ

Web制作の勉強メモ ほぼほぼ自分用

Ajaxメモ

久々にどん詰まったのでメモよ
あけましておめでとうございます(順番が謎)

基本的な構造

$.ajax({
  url: 'ajax.php',
  type: 'POST',
  dataType: 'json',
  data: {
    value: $('.input').val(),
  }
})
.done(function(data){
  // 通信に成功したときの処理
})
.fail(function(data){
  // 通信に失敗したときの処理
})

大まかな流れとしては

通信についての設定を書く(url, type 等)
↓
通信したいファイルに送りたい値を設定する(data)
↓
通信が完了してからの処理を書く

url

そのまま。通信したいファイルを記載する
僕はPHPをどうこうしたいことがほとんどだけど、HTMLとかも使える 外部htmlを丸ごとor一部抜き取って埋め込むみたいな使い方をする

type

通信方式 所謂GET or POST
PHPajaxを使う場合、要するに予め用意していたPHPファイルをJavaScriptから発火させるというだけでやることはPHPと変わらないんですね
ちなみに小文字でも可

dataType

通信先ファイルから返ってくるデータの形式を指定する
一方的な通信というか、発火をさせるだけで通信先からデータを貰ってJavaScriptでそのデータをこねくり回すみたいなことをやらないなら記述はいらないはず
PHPの戻り値として受け取りたいって場合はjsonが一番無難かな……(詳細は後述)

data

通信先ファイルに送信するデータをオブジェクト型で定義する
PHPに送信する場合)送信したデータはPHP側でグローバル変数$_GETor$_POSTに格納される
すなわちこう

JS
// 前略
type: 'POST',
data: {
  'value': $object.val(),
  'text': '文章'
}
PHP
<?php
$value = $_POST['value']; // =$object.val()
$text = $_POST['text']; // ='文章'

PHP側で注意すること

JSの説明してPHPに飛んで(この後)JSに戻ってdone/failの説明ってなんか行ったり来たりって感じでちょっと忙しいが、先にPHPの説明しといた方が後の理解がしやすい(処理の順番がこうなので仕方ない……)と思うのでPHPのこと書きます

基本は普通に書いていいです、がいくつか気をつけておくべき点があります

返り値は「echo」

なんとなくreturnで返したくなる気持ちもありますが呼び出してるのは関数じゃなくてファイルそのものだからね echo 返したい値 で返してあげます

JSON形式に直してからechoする

まぁそもそも言語が違うからそのままデータを送ってもJS側は読めないんですね、僕もヒンディー語読めないし(?)
もしかしたらtext形式とかだと送れるんかもしれないけど(調べてない!)データ型がString一択になっちゃうし1個しか送れないんで不便、そんなわけでたくさんデータ保持できてデータ型もそのままに出来るJSON形式に直しちゃいましょう
JSONJavaScriptで扱える配列とかオブジェクトみたいな見た目のデータ形式です
こんな感じ↓

data: {
  'int': 130, // ちゃんと数値になっている
  'text': '文章'
}

PHPの配列をJSON形式に変換するにはjson_encode()を使います
例えば

<?php
$array1 = [ 'quick', 'buster', 'arts' ];
$array2 = array(
  'green' => '',
  'red' => '',
  'blue' => ''
);

上の2つの配列があります、これにjson_encode()をかけてやる(json_encode($array1), json_encode($array2)

// array1
{
  "0": "quick",
  "1": "buster",
  "2": "arts",
}

// array2
{
  "green": "",
  "red": "",
  "blue": "",
}

こんな感じのJSONファイルになります
一つ前に書いた返り値はechoという話を踏まえるとPHPからJSに値を戻してやる場合は

<?php
$array = array(
  // JSに渡したい値がいろいろ配列形式で入っている
);

echo json_encode($array); // ←ここ

という風に書いてやると良いということ

(function.phpみたいな)外部phpファイルはここでもrequireの必要がある

これはかなり自戒用が強いので飛ばしてもいいです

どうしても(呼び出し元のHTMLを描画している)PHPで既に呼び出してるから要らんやろうという感覚になっちゃって書かなくてもいいかな?と思ってしまうんだけどもう描画後なのでPHPPHPではなくHTMLなんだよなぁ(これは328個人の感覚の話なのでわからなくてもいいです)
いずれにせよ関数とかを外部PHPに書いている場合はそれをちゃんと呼び出さなきゃいけないし、データベース接続も改めてPHP内で行う必要があります、画面描画用のPHPは処理を終了しているのでPHPとしては動きません

doneとfail

JSの話に戻ります
通信が無事成功したときの処理は.done(function(){、失敗したときの処理は.fail(function(){の中に書きます
通信先からの戻り値は.done(function(data){dataの中に格納されます、なので戻り値をJS側で使う場合必ず関数の中に引数を入れましょう
どうでもいいけどこの文章打ってて今まで意味不明だったコールバック関数の本質が少し見えた気がした

引数に格納されたデータを使うにはdata['key']data.keyのどっちかで取り出せます どっちでもいいと思う

doneとfailはJSON形式を勝手にJavaScriptのオブジェクトに変換してくれる

これ最初「JSON形式を返すから変換が要るのでは!?」と思ってJSON.parse(data)JSONをJSオブジェクトに変換する関数)を書いたのですが実はdone(orfail)のdataJSON.parse(data)を通された状態、すなわち既にオブジェクトに変換された状態で格納されているらしいのです、なのでそこでまたJSON.parse(data)をしようとしたら既に変換されてるやで~とエラーを吐いてきます
処理いらずでそのままオブジェクトとして扱えるのですね、便利

おまけ:$(this)はajax処理の外で変数に格納したほうがいいと思う

まぁ冷静に考えれば当然なのですが、例えば複数あるボタンのどれかがクリックされたときにajax通信をする、みたいな流れだったらクリックイベント発動直後にまず$(this)を変数に格納し、その後からajax通信を開始する、みたいな流れがおそらくベター

$button.on('click',function(){
  $that = $(this); // まずここで$(this)を変数に入れちゃう
  $.ajax({ // その後に通信開始
    url: 'ajax.php',
    type: 'post',
    dataType: 'json',
    data: {
      'value': $that.val(), // クリックされた要素の属性とかを取得できる
    }

そんな感じ ajaxはまだしっかり身についてないのでもう少しいくつか作りたいですね

PHPメモ ページングの話

新しいことを脳に入れようとすると眠気が襲い来る

SELECTで取ってくる個数の上限を決める文

<?php
$sql = 'SELECT * FROM images LIMIT 3 OFFSET 0;

LIMITは取ってくる個数 OFFSETはデータの何番目から取ってくるか
この文の場合は0番目(先頭ですね……)からデータを3つ持ってくる 0番目、1番目、2番目
ページネーションってのは要はこれを元に

<?php
$dataNum = 1Pに表示するデータの数;
$page = ページ数;
$sql = 'SELECT * FROM images LIMIT $dataNum OFFSET ($page - 1) * $dataNum';

で回していく形なんですね

例えば1Pに最大10件表示するとして1P目は
SELECT * FROM images LIMIT 10 OFFSET (1 - 1) * 10
LIMITはまぁ常に10として OFFSETは(1 - 1)が0の時点で何掛けようがゼロ、要は0から10個取ってきてくれる

2P目
SELECT * FROM images LIMIT 10 OFFSET (2 - 1) * 10
(2 - 1) * 10 = 10、1Pは0~9を取っているのでちゃんとその続きですね

3P目
SELECT * FROM images LIMIT 10 OFFSET (3 - 1) * 10
(3 - 1) * 10 = 20、2Pは10~19なので20からで正解

以下続く
そんな理屈

実際にページングを回すときの理屈

今回は最大表示が5のページングを作る仮定でいく
具体的に言うとこういう感じのもの(今イラレで即興で作った)
f:id:mi28xider:20181207025454p:plain

for文で回す
基本形はこう

<?php
$current = 現在のページ数;
$minPage = ページネーションの最小数(一番左に表示される数字)
$maxPage = ページネーションの最大数(一番右に表示される数字)

$minPage = $current - 2;
$maxPage = $current + 2;

?>
<!-- $i を最小数から最大数まで1つずつ書き出す -->
<?php for($i = $minPage ; $i <= $maxPage ; $i++): ?>
<!-- URL(GETパラメータ)とリンクに表示させる文字として書き出し -->
<li><a href="?p=<?php echo $i ;?>"><?php echo $i; ?></a></li>
<?php endfor; ?>

最小と最大の数値の決め方の感覚としては多分この画像がわかりやすい

f:id:mi28xider:20181207025813p:plain

3つなら±1だし7つなら±3だ

これがまぁ基本
ただまぁ例外がいっぱいあるので以下その場合の対応を書く

現在のページが最大ページ数の -1だった場合

例えば最大ページ数が5で現在のページが4P目だった場合、上のままだと存在しない6P目が表示されてしまう ※総ページ数は 全データ数÷ページごとの表示データ数+(あまりが出た場合1)で計算できます

理想としてはこう(以下の画像)なってほしい

f:id:mi28xider:20181207030757p:plain

つまり現在のページ数 == 総ページ数 - 1が成立する場合に $minPage = $current - 3, $maxPage = $current + 1 に変えてやる必要がある

<?php
$totalPage = 総ページ数;
if($current == $totalPage - 1){
  $minPage = $current - 3;
  $maxPage = $current + 1;
}

というかここまで書いて(動画を見ながら書いています)残りの理屈がわかってしまったから以下簡単にまとめるわね

現在のページ == 最大ページ数 の場合

<?php
if($current == $totalPage){
  $minPage = $current - 4; // $total - 1になるはず
  $maxPage = $current;
}

現在のページ == 2 の場合

<?php
if($current == 2 ){
  $minPage = 1; // $current - 1と同義ではあるが
  $maxPage = $current + 3 ;
}

現在のページ == 1 の場合

<?php
if($current == 1 ){
  $minPage = $current; // 1と同義
  $maxPage = $current + 4 ;
}

(※ちょっと重要)最大ページ数がページネーション数より少ない場合

これは自分で気付かなかった点なのですが、総ページ数がページネーション数より少ない場合、上の方法を通すと存在しないページを生成してしまいます
なので上の方法に「総ページ数がページネーション数以上である」という条件を追加しつつ、総ページ数がページネーション未満の場合の分岐も作ってやる
総ページ数がページネーション未満の場合、現在地の位置は関係なく最小は1、最大はページネーション数そのまま、という感じになる

<?php
$pageNum = ページネーション数;
if( $totalPage < $pageNum ){
  $minPage = 1 ;
  $maxPage = $pageNum ;
}

これらを総括して条件分岐を作る

<?php
// 現在のページが最大 - 1
if($current == $total - 1 && $totalPage >= $pageNum){
  $minPage = $current - 3;
  $maxPage = $current + 1;
}
// 現在のページが最大ページ数と等しい
elseif($current == $totalPage && $totalPage >= $pageNum){
  $minPage = $current - 4;
  $maxPage = $current;
}
// 現在のページが最小ページ数の+1=現在のページが2P目
elseif($current == 2 && $totalPage >= $pageNum){
  $minPage = $current - 1;
  $maxPage = $current + 3;
}
// 現在のページが最小ページ=現在のページが1P目
elseif($current == 1 && $totalPage >= $pageNum){
  $minPage = $current;
  $maxPage = $current + 4;
}
// ここまで全て最大ページ数がページネーション数以上の場合の条件
// 最大ページ数がページネーション数未満の場合
elseif($totalPage < $pageNum){
  $minPage = 1;
  $maxPage = $pageNum;
}
// 上記のどれにも該当しない
else{
  $minPage = $current - 2;
  $maxPage = $current + 2;
}

そんなこんなで最小数、最大数を決めた上でさっきの

<!-- $i を最小数から最大数まで1つずつ書き出す -->
<?php for($i = $minPage ; $i <= $maxPage ; $i++): ?>
<!-- URL(GETパラメータ)とリンクに表示させる文字として書き出し -->
<li><a href="?p=<?php echo $i ;?>"><?php echo $i; ?></a></li>
<?php endfor; ?>

を回してやる

結論必要な情報

  • 現在のページ
  • ページの最大数
  • ページネーションで表示されるページ数
  • 1ページに表示されるデータの数

※執筆途中です

PHPメモ 画像のアップロード

前提:form/inputタグの設定

ファイル送信にはformタグ及びinputタグを使いますが それ周りでいくつか注意すべき点があるので書いときます

inputタグでファイルを送信したい場合にenctype="multipart/form-data"の記述が絶対必要という話

POST送信なのでいつものごとくformタグにmethod="post"の記述は必要なんですが(未だによく忘れる……)画像のアップロードをするために必要不可欠な記述が

<form enctype="multipart/form-data">

まずenctypeってなんやねんって話ですがこれはMIMEタイプ、まぁわかりやすく雑に言ってしまうと扱うファイルタイプを指定する項目になります、これ以外にもtext/plainとかimage/pngとか、あとhtmlコーディングしてると見る機会がよくあるtext/csstext/javascriptもそうです
んで、multipart/form-dataはmultipartと名のある通り複数の種類のデータを一度に送信できる「複合データ型」というものになります(正確にはmultipartと名が付くタイプにはmultipart/byterangesというのもあるのですが今回はformからの送信の話なのでform-dataを使うんだと思います)

で、なんでこれの記述をする必要があるのかというと、詳しくはURLを参照してほしいのですが(えぇ……)デフォルトだとファイルを読み込んでもサーバーに送信されるのはファイルがあるという事実とファイル名のみだけになってしまい、実際のファイルの中身まではサーバーに送ってくれないのです!
なのでサーバー側は実際のファイルを扱うことが出来ず、以後の処理が出来なくなってしまいます、気をつけましょう
僕はこれの記述忘れに気が付かず1週間くらい消費しました

参考
mugenup-tech.hatenadiary.com
www.yoheim.net

input type="hidden"とアップロードファイルサイズの話

input typeにいろいろな種類があることは知っていますが、最近になって初めて知ったtype="hidden"、まぁ字面の通り画面に直接描画はせず隠しデータのようなものを一緒に送信することが出来るものです(あくまで画面に表示されないってだけの話なのでソースを見れば普通に見えます)
これを利用してアップロード出来るサイズ上限を指定することが出来ます、必須ではないのですが覚えておくと便利なことがあるかもしれません

具体的なやり方としてはtype:hiddenのinputタグにname:MAX_FILE_SIZEを設定し、valueに実際の値を入れればOKです

<input type="hidden" name="MAX_FILE_SIZE" value="3145728">

3145728 は3MBの意です 1024×1024×3=3145728

ただファイルサイズはこことは別にもう1つ、php.iniでも設定が必要です
ここの制限はクリアしててもphp.ini 側で引っかかって上げられない、という場合も普通にあるので予め設定しておきましょう

php.ini のファイル上限関係のパラメーター

  • upload_max_filesize:アップロード出来る最大サイズ
  • post_max_size:POST送信で扱えるファイルの最大サイズ
  • memory_limit:サーバーのメモリの最大サイズ

ファイル関係のスーパーグローバル変数$_FILES

送信されたファイル情報については連想配列形式のスーパーグローバル変数 $_FILESに格納されます
具体的にどういう状態で格納されているかというと まず$_FILESの中にファイル自体が配列として入っており 更にそのファイルそのものの名前やファイルサイズファイル形式諸々もろが格納された連想配列になっているという寸法

イメージ図

f:id:mi28xider:20181206101335p:plain

いつもの

なので実際にそれを扱う場合には例えば$_FILESの中にimage1というファイルがあったとしてそれの実際のファイル名を取得したいんや!というとき、本来なら$_FILES['image1']['name']という書き方をすることになります

が、キーが2つも連続すると見栄え的に混乱する人もいると思う(ワイやぞ)ので画像そのものを任意の変数に格納して、それの情報がほしいときにキーを添えた書き方をすると混乱しにくいかもという提言(ひとり会議)

$_FILES['img01']['name'];
$_FILES['img02']['name']; 
$_FILES['img03']['name'];  

と本来はなるところを

$img01 = $_FILES['img01'];  
$img02 = $_FILES['img02']; 
$img03 = $_FILES['img03'];

$img01['name']
$img02['name']
$img03['name']

としてやるとわかりやすいんちゃう?という話

$_FILESに格納されるファイル情報

  • name:ファイル名
  • typeMIMEタイプ 上で説明したimage/pngとか
  • size:ファイルサイズ
  • tmp_name:ファイルをアップロードする際、ファイルは実は一度一時ファイルとして保存されているが、そのときの名前
  • error:読んで字の如くエラー エラーに応じた数字が格納 ちなみにエラーがない場合も0として表現される

実際にアップロードさせる流れ

前提

inputタグはこう記述されているのを前提

<input type="file" name="image">

で、上の話に則って

<?php
$file = $_FILES['image'];

として画像を変数に代入しておく

ファイルの有無をチェック

まず何よりファイルが送られていなければお話にならないので$_FILES関数の中にファイルが有るかをチェック
ファイル情報どれかをチェックすれば有無は確認できると思うけどあとでバリデーションに使う関係もあるのでerrorでチェックしてしまおう
上で言ってるけど特にエラーが無くてもerrorの中には0が格納されます

<?php
if (isset($file['error']) && is_int($file['error'])) {

isset()は「変数が設定されていること」「変数がnullでないこと」をチェックしてくれる関数
データが入っていればここは必ずtrueになるはず!
で、is_int()は整数型かどうかを見てくれる関数、errorは整数で見るのでここがちゃんと整数か念の為確認する

バリデーションチェック

画像にもバリデーションチェックがある
上で言ったようにerrorは整数で返されるが、アップロードの際にPHPはその数字を格納した便利な定数を準備してくれる、出来る子や
いくつかのエラーは例外を投げてやり、try-catchで処理を振り分ける
(現時点で)覚えとくといいエラーコード

  • UPLOAD_ERR_OK=0:エラーなし
  • UPLOAD_ERR_INI_SIZE =1:php.iniupload_max_filesizeに設定した値をファイルサイズが超過している
  • UPLOAD_ERR_FORM_SIZE=2:inputフォームで設定されたファイルサイズを超過している
  • UPLOAD_ERR_NO_FILE=4:ファイルがアップされていない

これらの定数と$file['error']の値を比較する、選択肢が多いからswitch文がいいかもね

<?php
try {
  switch ($file['error']) {
    case UPLOAD_ERR_OK:
      break;
    case UPLOAD_ERR_NO_FILE:
      throw new RuntimeException('ファイルが選択されていません');
    // 中略
    default:
      throw new RuntimeException('その他のエラーが発生しました');
  }

エラーが起きたら例外処理に投げてあげる
詳しいことはググろう 過去にもすこし言及したね
328xider.hateblo.jp

ファイル形式のチェック

ファイル形式自体は$_FILES['type']にも格納されているが、ここのMIMEタイプは割と簡単に偽装できちゃうらしい
ということでファイルのexif情報を見て独自に判別してやる

<?php
$type = @exif_imagetype($file['tmp_name']);
  if (!in_array($type, [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF], true)) {
    throw new RuntimeException('画像形式が未対応です');
  }

画像のexif情報の中にある画像形式をexif_imagetype()という関数で取り出せる
@を関数の前につけるとその関数で発生したエラーメッセージは無視されるらしい
in_array()は配列の中に一致するものがあるかを調べる関数

<?php
in_array(比較するもの, 配列[], 型比較をするかどうか);

in_arrayには第三引数がある。型の比較をするとかしないとか表現がわかりにくいのだけれど、実際はfalseのときは == で、trueのときは === で比較されると覚えておけばいい。

とのこと

exif_imagetype()で取り出したファイル形式がIMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIFのうちのどれかに一致しているかを調べている 一致して無ければ例外を投げちゃう

一時ファイルを正式な保存箇所に移動してやる=アップロード

上でチラッと言ったけどファイルをinputで送信した時点で実はそのファイルは一時ファイルとして保存がされていたりする その一時ファイルを僕らが把握できる箇所に移動してやる、ということがアップロードの本質なのである!(たぶん)
まずはファイルの保存箇所を変数に格納

<?php
$path = './img/upload/' . sha1_file($file['tmp_name']) . time() . image_type_to_extension($type);

sha1_file()はファイル名をハッシュ化=暗号化する関数 ファイル名の重複防止
でも自分で試してたときに「全く同じファイル」だと重複して上書きされちゃうことに気がついた だから念の為UNIXタイムスタンプを付けてより重複しないようにしてみたのがこれ
image_type_to_extension()exif情報のファイル形式から拡張子を取得してくれる(という認識でいいはず……)

で、ファイルを実際の保存場所に移動しちゃう
ついでにダメだった場合にエラーも投げちゃう

<?php
if (!move_uploaded_file($file['tmp_name'], $path)) {
  throw new RuntimeException('ファイル保存時にエラーが発生しました');
}

move_uploaded_file()がファイルを(実際のディレクトリに)移動する関数

<?php
move_uploaded_file(元の場所('概ね$_FILES['tmp_name']かな'), 移動先);

move_uploaded_file()はもし関数が正常に実行できた場合に返り値としてtrueを返してくれる
なのでこれを利用して「trueが返ってこなかった場合」にエラーを投げてるんだね

最後にファイルのパーミッションを変更

<?php
chmod($path, 0644);

ファイルのパーミッションを変えてやる
詳細は割愛しますが644は「自分は読み書き可、他人は読み取りだけ可」の意味です

これでアップロードは完了
$pathにURLが格納されているのでアップロードしたファイルを他(DBとか)で使う場合はこの$pathの値を利用すればいいんですね 関数化してるならreturnでこれを返すのもよい

投げられた例外

catch部分の話 今回はだいたい手動(throw new~)で例外を投げているので、投げたときに添えたメッセージが$e ->getMessage()で取り出せる
ユーザーに表示させる部分にもこれが利用できるので楽ですね

RuntimeExceptionってなんぞや

今回はただのExceptionではなくRuntimeExceptionを利用している
これは運用で発生することが発生しうる例外、「使い方によっては普通に起こりうる」ことに使うことが多い、厳密にはバグではないもの
ちなみにこれの対にあるのがLogicException、これは逆に発生が想定されていないもの、「実運用で起こられると困る」ものに使う、これは明確にバグ

PHPメモ ログイン周りいろいろ

眠い

前提:ログイン判定に必要なもの

ログイン情報はセッション変数$_SESSIONに保存しますが、具体的に何を保存するかっていうと以下になります

'login_date'

最終ログイン時間をUNIXタイムスタンプ形式で保存します、概ねページ遷移する度に更新されます
格納する値は常に現在時刻のみなのでどこで格納するにしても

<?php
$_SESSION['login_date'] = time();

と記述しておけばいいです 一番わかりやすいですね
ちなみに僕はtime()data()をものすごい頻度で間違えます UNIXタイムスタンプは前者です

'login_limit'

最終ログイン日時から何秒経ったらセッション切れと見做すか、その秒数を格納します
デフォルト値は大体1~3時間くらいなんじゃないかなと思いますが、ログイン時にそれを延ばす(1週間~1ヶ月くらい?)選択肢をユーザーに選ばせることも可能です、ログイン画面によくある ✔ログイン状態を保存する みたいなアレです

<?php
// デフォルトの方 1時間
$_SESSION['login_limit'] = 60 * 60;
// 長い方 30日
$_SESSION['login_limit'] = 60 * 60 * 24 * 30;

'user_id'

DBのユーザー情報テーブルのidを格納します、厳密にはログインそのものには関わってきませんがページ表示に必要になることが多いのでログインの際に大体一緒に格納します

<?php
// SELECTで取ってきたとき ログイン時とか
$_SESSION['user_id'] = $result['id'];
// INSERTで新規登録した直後に格納するとき 詳細は後述
$_SESSION['user_id'] = $dbh -> lastInsertId();

変数のキーは例なので別に違っててもいいです
まぁただ自分は大体いつもこれ使う~っていうお決まりのパターン決めとく方が作業はしやすいやろうね

大体上の3つを新規登録やらログインのときに更新したりログイン状態を確認するときに見たりします

新規登録のときにセッションに登録すること

新規登録が完了した際、わざわざログインページヘ戻してもう一回ログインさせるのはあんまりユーザビリティ的によろしくないので 新規登録が完了した時点でセッションに必要なデータを格納してそのまま会員ページを利用できるようにするのがいいと思います

<?php
$_SESSION['login_date'] = time();
$_SESSION['login_limit'] = 60 * 60;

この2つはこれと言って特に変わったことはないのですがuser_idのみ

<?php
$_SESSION['user_id'] = $dbh -> lastInsertId();

という他では見ないやり方で取ってくることが出来ます
まぁ読んで字のごとし「最後にINSERTしたデータのidを取ってくる」ってだけなのですが idの判別はオートインクリメントがついている値、という判断のようです
最後にINSERTしたもの、というものなので対象が「データベース全体」になります、なのでfetchした後のデータとかじゃなくて$dbhから取り出していることに気をつけてください

しかしこれタイミングがめちゃくちゃシビアに重なってしまって最後にINSERTしたものが別の人のデータだったみたいなことは起こらんのやろうか

ログイン画面でやること

ログイン画面でやることの流れは大体こんな感じと思われます

  1. POST送信があるかのチェック
  2. バリデーションチェック
  3. DB接続、IDやらメールアドレス等の個人を識別するデータをキーにSELECTで検索かけて該当行を引っ張り出してくる
  4. 引っ張り出した行の中にあるパスワードとPOST送信されたパスワードが等しいかチェック
  5. セッション期限を長くするかどうかのチェック(期限の格納はここでしてもいい)
  6. セッション変数に現在時刻(と期限)とログインしたユーザーのIDを格納
  7. 会員ページ等に遷移

1とか2は他で記事にしてたりそもそも解説するまでもないことだったりなので省略
3も言うていつも通りSELECT文でクエリ実行してfetchで連想配列形式にしてやるだけなんですけども

<?php
$dbh = dbConnect();
$sql = 'SELECT * FROM users WHERE name = :name OR email = :name AND delete_flag = 0';
$data = array(
  ':name' => $name
);
$stmt = queryPost($dbh, $sql, $data);
$result = $stmt->fetch(PDO::FETCH_ASSOC);

この例は$nameの値をnameカラム及びemailカラムの両方で検索をかけ、どちらかに一致してればOKということにしたので
IDでもemailでもログインできるというちょっと便利な感じに

で、4のパスワード一致判定
パスワードはpassword_hash()で暗号化されているのでそのままでは当然ですが一致しません
ハッシュ化したパスワードとの比較にはpassword_verify()を使います

<?php
password_verify(比較する文字列, ハッシュ化済の文字列);

3の手順から引き継ぐとすれば

<?php
if (!empty($result) && password_verify($pass, $result['pass'])) {

みたいな感じでクエリ実行の結果が空ではなく且つパスワードと一致するもの、という条件にしてあげるのが良さそうです
クエリがちゃんと通ってるのかも条件に含めることで要らんエラーが減ると思う

5のセッション期限を長くするかどうかの判定、大体はチェックボックスにチェックが入ってるかの判定になるかと思うのでそれで

<?php
if (!empty($_POST['check'])) {
  debug('パスワードを保存にチェックがあります:セッション期限30日');
  $_SESSION['login_limit'] = 60 * 60 * 24 * 30;
} else {
  debug('パスワードを保存にチェックがありません:セッション期限1時間');
  $_SESSION['login_limit'] = 60 * 60;
}

if文の部分、最初はBoolean判定、if ($_POST['check'])という書き方をしていたのですが、どうもチェックボックス$_POSTの扱いはチェックが入ってる場合のみtrueを返す(チェックが入ってなくてもfalseは返さない!)ということになるみたいでチェックせずにログインしたときに画面上にこそ出てこないけどログで「$_POST['check']なんて変数定義されてないんじゃしばくぞ」とNoticeで怒られたので変数が空かどうかで判断するように変えました、怒られなくなったのでめでたし

6はtime()で現在時刻のUNIXタイムスタンプと3で取得したデータベースのデータのidをセッションに保存すればいいだけです

<?php
$_SESSION['login_date'] = time();
$_SESSION['user_id'] = $result['id'];

場合によっては5で直接セッションに保存するのではなく変数に格納だけしといて、こっちで改めてセッションに保存する場合もあるやも

<?php
<?php
if (!empty($_POST['check'])) {
  $limit = 60 * 60 * 24 * 30;
} else {
  $limit = 60 * 60;
}
$_SESSION['login_limit'] = $limit;

7は普通にheader()で遷移するだけです、省略

ログイン状態によってページを遷移させる機能

会員制サイト、会員じゃないと見れないページ(大半のページはそれですね)や逆にログイン状態では表示させる必要がないページ(新規登録ページやログインページ等)が存在するのでそれを判断する仕組みが必要です

独立したphpファイルにしてrequire()でページ頭に読み込んであげるのが一番楽そう

以下のフローチャートが大体の流れだと思いますが若干勘違いした状態で作った図なのでちょっと文章がおかしいです、でも作り直すのも面倒

f:id:mi28xider:20181203235242p:plain

ちなみにここで作りました
Flowchart Maker & Online Diagram Software

セッション変数に'login_date'の値がある

すなわち最後にサイトを訪問していた時点ではログインしていたということになります
ここが図の勘違いポイントでした、「過去にログインをしたことが」ってなんだよ
ここから更に2つのパターンに分岐します

現在時刻がセッション期限内

ここが唯一「現在進行系でログイン中」にあたるパターンです
こういうの考えるのがすごく苦手なので自分用に書いておきますが、期限内の場合は現在時刻は期限より小さくなります
ちなみに期限の求め方は'login_date'+'login_limit'です

<?php
if (time() < $_SESSION['login_date'] + $_SESSION['login_limit']) {

実際の記述はこうなります

現在進行系でログイン中の場合にやることは

  • 最終ログイン日時を更新すること
  • 非ログイン状態でのみ表示させるページを表示しようとした場合強制的にトップページ等に遷移させること

の2つです

最終ログイン日時の更新は普通に

<?php
$_SESSION['login_date'] = time();

でおわり

で、もう1つのほうがここの肝ですが、単にheader()でマイページに遷移させるだけだと何でもかんでもマイページに遷移する全く使えないゴミサイトが出来上がりますし何よりこれをマイページから読み込んだ場合楽しい無限ループが開催されます
じゃあどうすればいいかというとこういうときに使えるのが$_SERVER['PHP_SELF']basename()です

$_SERVER['PHP_SELF']

有り体に言うと「現在のURL」が格納されている変数です
要はこれを使って特定のURLからアクセスしたときだけページ遷移を発動させればいいやん、という寸法です
ですがこの変数に格納されているのはドメイン以下全てのアドレスなので直下ファイルならともかくディレクトリを挟んでる場合は予想外の挙動をしてしまったり記述がやや冗長で面倒、ということが起こります
そこで使うのがもう1つ

basename()

記述の仕方としてはbasename($address)
アドレスのディレクトリやら何やらを削いで、単純な「ファイル名のみ」に加工します
こうすることでかなり扱いやすくなりますね

これらを使って「signin.phpまたはlogin.phpにアクセスしようとしたときだけトップページに遷移する」ようにしたのが以下です

<?php
if (basename($_SERVER['PHP_SELF']) === 'signin.php' || basename($_SERVER['PHP_SELF']) === 'login.php') {
  debug('ログイン中のため、トップページに遷移します');
  header("Location:index.php");
}

basename($_SERVER['PHP_SELF']、記述としては長めなので何ならどっかしらで変数に代入してそっちを使ってもいいと思います、僕はそうしました
以下は$phpself = basename($_SERVER['PHP_SELF']);という前提でコード記述します

現在時刻がセッション期限外

やることは簡単で、セッションを破棄してログインページに戻す、これだけです
セッションを破棄するので無限ループについても考えなくてOK

<?php
debug('セッション期限切れです');
// セッションを破棄する
session_destroy();
debug('ログインページに遷移します');
header('Location:login.php');

セッション変数に'login_date'の値がない

サイトからログイン済み(或いはそもそもログインをしたことがない)ということになります
やることは「ユーザー登録の必要なページにアクセスしようとした場合にログインページヘ強制的に飛ばす」だけなのですが無限ループのこととか考えるとやや迷ってしまう感じが個人的にはします

敢えて雑にまとめるとすれば「現在のURLとログイン不要なページが等しくないかをひたすらAND(&&)でつなげていく」ということになるかと思います

<?php
debug('未ログインユーザーです');
if ($phpself !== 'login.php' && $phpself !== 'signin.php') {
 debug('ログインページに遷移します');
 header('Location:login.php');
}

あとはログインが必要か不要かに応じてディレクトリを分けてどのディレクトリに属してるかで判定するとか、phpファイルごとにそれを判定するフラグをつけてそれで振り分けるとかも考えたが試してない

ログアウト機能

これは本当にシンプルです、セッションを破棄してログインページヘ遷移するだけです ログイン判定機能の期限切れのとこでやってることと全く同じことをファイル単体でやってるだけや

<?php 
// 共通関数
require('function.php');

// 画面処理開始
debug('');
debug('==================================');
debug(' ログアウト|logout.php');
debug('==================================');

// セッション情報とか
debugLogStart();

// セッションの配列データを削除する→ログアウト
session_destroy();

// ログインページヘ戻す
debug('セッション変数の値を消去しました');
debug('ログインページヘ戻ります');
header("Location:login.php");

ファイル全体記述してこれだけだからな 本当に単純

PHPメモ データベースとMySQL編

首いてえ
データベースに接続したりSQL文でデータベースをあれこれするメモ

この記事を参考にしています
qiita.com

データベースに接続する

データベースに接続するにはPDOオブジェクトを生成する
構文としてはこれ

<?php
$dbh = new PDO($dsn, $user, $password, $options);

$dbh はDataBase Handlerとのこと まぁ慣習に従っとくのがいいでしょう
引数を1番目から順に見ていくわね

$dsn:データベースに接続するために必要な情報

dsnってのはData Source Nameの略だそう
ここの書き方は使うデータベースによって違うらしい(というかPDO関係の関数全体が多分そうだと思う……)のだけど、とりあえずMySQLの話として進めます
書き方としては

<?php
$dsn = 'mysql:dbname=DBの名前;host=DBのアドレス;charset=文字コード';

気をつけることと言えばDBの名前はダブルクオーテーションとかで括る必要はないことと、最近だと文字コードは概ねUTF-8になると思うんだけどここでそれを記述する際にはハイフンが不要で、具体的に言うとcharset=utf8と記述する、僕一回ここ引っかかったんだよ

$user,$password:データベースのユーザー名とパスワード

まぁこれはぶっちゃけそのままですね
MySQLのユーザー名とパスワードを入れます 強いて言うなら使うユーザーの権限がやりたいことと合致してるかを気をつけるくらいか

$options:接続時のオプション

接続時のいろいろなオプションを連想配列形式で設定します
オプションはいろいろあるけど現時点(の知識)で設定する必要があるべきだろうのは以下

PDO::ATTR_ERRMODE

エラーが起こったときの挙動を選択できる
デフォルトはPDO::ERRMODE_SILENT

PDO::ERRMODE_EXCEPTION:エラー・例外をスローしてくれる
PDO::ERRMODE_WARNINGSQLで発生したエラーをPHPのWarningとして報告する
PDO::ERRMODE_SILENT:エラーコードのみを設定してあとは何もしない

下2つについてはPDOStatement::executeメソッドの返り値が false でないかを毎回確認する必要があるとのこと お手本にしてた動画ではPDO::ERRMODE_SILENTを使っていたが 確かにクエリが成功したか失敗したかを毎回確認していたな

PDO::ATTR_DEFAULT_FETCH_MODE

fetch()とかfetchAll()メソッドに結果をかけたときのデータの形式を選択する、という認識でいいんじゃなかろうか デフォルトはPDO::FETCH_BOTH

PDO::FETCH_BOTH:カラム番号とカラム名の両方をキーとする連想配列で取得する
PDO::FETCH_NUM:カラム番号をキーとする配列で取得する
PDO::FETCH_ASSOCカラム名をキーとする連想配列で取得する
PDO::FETCH_OBJカラム名をプロパティとするオブジェクト形式で取得する

まぁよっぽどでなければPDO::FETCH_ASSOCが一番無難で扱いやすいんじゃないかと思う(だから無駄に太字にした)、参考にしているQiitaの記事を書いた人もそう言っています

PDO::MYSQL_ATTR_USE_BUFFERED_QUERY

MySQL専用のオプションらしい
trueのときにバッファクエリを使用します バッファクエリってなんやねん

  • バッファクエリ
    一度に全ての情報をデータベースから予め取得しておいて、詳細はPHPの方で取り出させる
  • 非バッファクエリ
    一件ごとに都度データベースサーバーと通信を行う

よっぽどな特殊ケースじゃない限りは基本的にtrueでいいでしょう、非バッファクエリはサーバーに負担がかかるとか複数クエリの並行処理が出来ないなんかのデメリットがやたら多いそうな(どうもこれはコマンドラインからバッチ処理を実行する用途という意味合いが大きい模様、現状の僕とはあまり関係ない部分ですね……)
デフォルトはどうもバージョンによってころころ変わっているようなので(現時点ではPHP公式では「デフォルトでバッファクエリを使う」という旨の記載はあった)こっちからオプションでちゃんと指定してあげとくのがよさそう

これらを踏まえて、オプションを設定する 例えばこう

<?php
$option = array(
  PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
);

んで最後に冒頭で挙げたPDOオブジェクトを生成
ただまぁデータベースってとにかく使うから逐一これを記述してたら日が暮れるのでこれら処理をまとめて関数にしちゃう

<?php
function dbConnect(){

// ローカルに立てたMySQLのxider_databaseっていうDBに接続するのを想定
$dsn = 'mysql:dbname=xider_database;host=localhost;charset=utf8';
$user = 'mitsuya';
$pass = 'mi3tsu2ya8'; // てきとうな例
$option = array(
  PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
);

$dbh = new PDO($dsn, $user, $pass, $option);
return $dbh;
}

そうしたら後は使うときに

<?php
$dbh = dbConnect();

の1行だけで接続完了できるね
PDO::ERRMODE_EXCEPTIONを使うときはこの関数もtry-catchで囲んであげる必要がありますね

クエリを実行する

実行の仕方は実は何種類かあるみたいだけど現状習ったやり方を整理しとく

基本的な構文としては

<?php
// $dbh に既にPDOオブジェクトが生成されているという前提
$stmt = $dbh->prepare('SQL文');
$stmt->execute([プレースホルダの中身を配列で]);

という感じ
プレースホルダーには疑問符プレースホルダと名前付きプレースホルダーの2種類がある

疑問符プレースホルダ

配列形式、一応数字をキーにした連想配列としても扱えはする

<?php
// $name と $city に既に条件となる値が入っているとする
$stmt = $dbh->prepare('SELECT * FROM users WHERE name = ? AND city = ?');
$stmt->execute([$name, $city]);

プレースホルダの中身の指定は

<?php
$stmt->execute([
  0 => $name, 
  1 => $city
]);

みたいなやり方も出来る(キー指定なので順番が逆でも数字が合ってればオッケー)が、やる意味あるんか?

名前付きプレースホルダ

連想配列形式、やや記述が長くはなるがキーが名前で付く分読みやすいとは思う

<?php
// $name と $city に既に条件となる値が略
$stmt = $dbh->prepare('SELECT * FROM users WHERE name = :name AND city = :city');
$stmt ->execute([
  ':name' => $name,
  ':city' => $city
]);

ちなみにプレースホルダの頭のコロンは省略も出来るらしい

<?php
$stmt ->execute([
  'name' => $name,
  'city' => $city
]);

そんな感じ

流れとしてはprepare()SQL文をプレースホルダという虫食い(なんか文学的な表現だな……)状態で渡してやり、execute()でその虫食いを埋める、という理解でいいと思う
どうせならこれも関数化した方が読みやすそうなのでそうする

<?php
function queryPost($dbh, $sql, $data){
  $stmt = $dbh ->prepare($sql);
  $stmt ->execute($data);
  return $stmt;
}

使うときは事前にPDOオブジェクトの生成とそれぞれ変数にSQL文、プレースホルダの中身を格納しておく

具体例

<?php
// DB接続は上の方で書いた関数を使うとする
$dbh = dbConnect();
$sql = 'SELECT * FROM users WHERE name = :name AND city = :city';
$data = array(
  ':name' => $name,
  ':city' => $city
);
$stmt = queryPost($dbh, $sql, $data);

// データベースの中身を具体的に参照してどうこうする場合はfetchが要る(たぶん)
$result = $stmt->fetch(PDO::FETCH_ASSOC);

PDO::ATTR_ERRMODEPDO::ERRMODE_WARNINGもしくはPDO::ERRMODE_SILENTのときは逐一メソッドの返り値がfalseかどうか確認する必要があるという話だったが、その際に結果に応じて処理を分ける(ログを吐き出すとかね)場合にはわざわざ毎回書いてたら面倒だから関数にその処理を含めてもいいかもね

<?php
function queryPost($dbh, $sql, $data){
  $stmt = $dbh ->prepare($sql);
  // ここでクエリがfalseを返してないかの判定
  if(!$stmt ->execute($data)){
    error_log('クエリ失敗');
    // 他にやる処理(画面にエラーを表示するとか)あれば書く
    return false;
  }else{
    error_log('クエリ成功');
    return $stmt;
  }
}

こんな感じで

例外処理の話

DB周り、僕からはどうにも出来ない/予測できないエラー、要は例外が発生することがある
そんなときにtry-catchで例外をスローしてあげる

<?php
try{
  // 諸々処理、記述省略
}catch(Exception $e){
  error_log('エラー発生:' . $e ->getMessage());
  // もし画面にエラー表示させるとかやるならそれも記述
}

tryの中身に書いていることをやり、その中でもし何らかの例外が発生したらcatchの中身を実行、という理解で凡そ良いはず
Exceptionはまぁ読んで字のごとし例外のこと(クラスです)だが、例外にもいろいろ種類があってもし発生した例外の種類によって処理を振り分けたいならここを変える PDOExceptionとかDOMException とかOutOfRangeExceptionとかいろいろ……
Exceptionは全ての例外が投げ込まれる(というか正しくは全ての例外の親がExceptionであり、それの子にDOMExceptionやらRuntimeExceptionやらがあり、更にRuntimeExceptionの子、Exceptionから見ると孫の位置にPDOExceptionがある 詳細は以下)
PHP: Exception - Manual

$eには例外のいろんなデータが入っている、ここから更に任意のメソッドでデータを取り出せる
多分圧倒的に使うことが多いのはgetMessage()、まぁそのままだが例外が示してるメッセージを取り出す、どういう理由で例外になったか的な……

ちなみに例外を投げる条件を作ってやることも出来る

<?php
throw new Exception('例外のメッセージ');

例外のメッセージ 部分がgetMessage()で取り出せる内容になる
ちなみにこれException部分に任意の名前を付けてcatchを分岐してやることも出来るっぽいね

SQL

まぁしょうみな話逐一調べても良いような気はするけど割と使用頻度が高いであろうSELECT INSERT UPDATEのことはまとめておこう

SELECT

大雑把に言うと【検索】が感覚としては近いと思う
既に登録されてるユーザーかどうか調べるとか、ログインユーザーの情報を表示させるとか投稿を表示させるとかそういう場面で使う

<?php
$sql = 'SELECT 範囲 FROM テーブル名 WHERE 検索条件';

範囲はまぁ*にして全体を対象にするのが一番多そう
検索条件は例えばpriceが5000以上とかあるかもしれないし、セッションにユーザーid保存させといてそれと一致するデータだけ取り出すとかみたいなこともある

ちなみにSELECTcountと言ってデータではなく条件に該当するフィールドの「数」を返すSQL文もある
具体的にはSELECT count(*)という形で使う、括弧の中は多分他の値も入れられるけど現状の僕の知識の範囲内なら全体*でいいと思う
検索件数を表示させるとか、条件に該当するデータが「あるかどうかだけ」知りたいときとかにいいね

INSERT

雑に言うと【新規登録】、データベースのテーブルの最後尾にデータを追加する

<?php
$sql = 'INSERT INTO テーブル名 (項目名1,項目名2,項目名3,...) VALUE (中身1,中身2,中身3,...)';

項目名はそのまま項目名、項目名の中身は大体プレースホルダーを突っ込んどいて後でその中身を埋める

UPDATE

要は【更新】、データベースの値を書き換える

<?php
$sql = 'UPDATE テーブル名 SET 更新内容 WHERE 更新するフィールドの条件';

更新内容はまぁそのままだけど例えばメールアドレスを変更するで~って話ならemail = :emailみたいな書き方になるんじゃない
更新するフィールドの条件は例えばログイン中のユーザーのメールアドレスを更新したいって場合はuser_id = :user_idとしておいてプレースホルダにセッションに保存されたユーザーidを突っ込んでやるとか
SELECTもそうだが条件はもちろん複数指定することも可能、WHERE 条件1 AND 条件2 AND ...みたいに書いてやる

おまけ password_hashの話

データベースやMySQLの機能っていうわけではないが、まぁデータベース周りで使うことがほとんどだと思うのでpassword_hash()のことも書く
パスワードをデータベースに保存する場合、そのままだとパスワードが平文で保存されてしまって非常によろしくない
そういう場合に使うのがpassword_hash()、指定した文字列をなんかすごいアルゴリズムで暗号化してくれる

<?php
password_hash(暗号化したい文字列, 暗号化の形式);

暗号化の形式、いろいろあるが(僕自身の知識もまだ薄いので)基本的にはPASSWORD_DEFAULT一択で良いと思う
PHP公式サイト曰く「使ってるPHPがリリースされた時点で一番安全な形式を選択してる(意訳)」

PHPメモ バリデーションチェック編

ユーザーに何らかのデータを入力させるタイプのWebサイトを作る場合、そのデータがこっちの想定した形式できちんと入力されているかをチェックした上でデータを通さないとDBにエラーを吐かれて想定外の動作をしたりするかもしれない
のでDBにデータを通す前に形式チェックをする 通称バリデーションチェック

大まかなバリデーションチェックの種類

共通

未入力

empty$str===''辺りかな
empty0も空と判定するので数値を扱う場合は空文字と比較してやる方がいいかもね

文字数

mb_strlen($str)は文字列の文字数をカウントする関数、これと数値を比較してやる

<?php
mb_strlen('オムライス専門店'); // 結果:8

// 文字列が30文字以内かどうかを比べたい場合
$str = '期間限定バナナチョコレートパフェ';
if (mb_strlen($str) <= 30) // 結果:true

みたいな感じ

mb_strlenmbはマルチバイトの頭文字、日本語で使う平仮名・片仮名・漢字は基本マルチバイト文字なので日本のサイトの場合はmb_strlenを使う
MySQLはデータ型によっては文字数上限が決まっているものがある(varcharの256とかintの11とか)があるので、それらで格納するデータは文字数チェックをきちんとやってあげる必要がある
あとはパスワードとかは最低文字列を決めてあげたほうがセキュリティ的によろしいよね

正規表現

正規表現で使うのはpreg_match()、返してくる値はint(※boolではない!)

<?php
// 例:半角英数字かチェック
preg_match('/^[a-zA-Z0-9]+$/', 'HappyNewYear'); // 結果:1
preg_match('/^[a-zA-Z0-9]+$/', 'もやし'); // 結果:0

一致すれば1、不一致なら0、エラーが起こった場合にfalseを返す
正規表現パターンはシングルクォート''で囲って記述するけど、PHPの場合その後更にデリミタというもので囲ってあげる必要がある
デリミタは基本的には(上の例でも使ったように)スラッシュ/が多いみたいだけど場合によっては正規表現内に/を使う場合もあると思う(URLとか……)
そういう場合に#とか~を使ってもいい、割といろいろな文字が使えるようなので状況に応じて臨機応変に対応しよう
ちなみにデリミタの後ろにアルファベット1文字のパターン修飾子というものをつけることも出来る、一番良く使うのは文字コードUTF-8を扱う場合のu 日本語を扱う場合は必須っぽい?

大体の場合は使うパターン決まってると思うのでコピペになるでしょうね
今適当に探したサイト
PHP正規表現 英数字変換・バリデーション・漢字カタカナひらがな・メールアドレス判定
PHPでよく使うバリデーションまとめ!! | ecvoo
PHP正規表現はなんかQiitaとかでもいろいろ議論になっているようで初心者はわからん

データベースとの重複

メールアドレスとかIDとかみたいに複数ユーザーでダブっちゃだめな値はデータベースに接続して既にその値がないか確認する必要がある
SQLならSELECT count(*)で該当する条件の数を数えさせるのがいいのかな、この場合0じゃなければ既にデータが存在するという判定になるね

具体的にどの項目でどんなバリデーションチェックをすればいいかを考えてみた

メールアドレス

  • 文字数チェック(DB上限)
  • 重複チェック(メールアドレスをログイン等で使う場合でなければ要らない)
  • 形式チェック(でもメールアドレスの正規表現による完全なバリデーションは不可能らしいですね、HTML5のinputでも出来るのでこれと併用してもいいかも)

パスワード

  • 文字数チェック(DB上限もそうだが、セキュリティ考えて最低文字数もチェックしたほうがいいかもね)
  • 形式チェック(半角英数字場合によっては記号とかも判定する、セキュリティのために大文字小文字両方を必ず含めないといけないとかそういう判定をすることもあるかも)
  • 登録の場合、再入力させてそれと一致してるかのチェック

ID

  • 文字数チェック(DB上限、ユニーク性も考えて最低文字数も決めてもいいかも)
  • 形式チェック(基本的には半角英数字かな)
  • 重複チェック

URL

  • 文字数チェック(DB上限)
  • 形式チェック(正規表現、日本語URLとかどうなるんやろ)

数値系の値

  • 文字数チェック(MySQLでint型の場合文字数上限は11だ)
  • 形式チェック(半角数字かどうか、JSで全角数字を半角に自動変換する仕組み組んでもいいかも)
  • 最大や最小が決まっている値であればそれを満たしているかの判定

基本的に(varcharやintみたいな文字数制限があるデータ型であれば)文字数判定は必ず行い、その後にそれぞれの形式に応じて正規表現使ったりDBに接続して重複してないかを確かめる感じの流れですかね
あと文字列以外にもチェックボックスなんかの判定もありますね(利用規約に同意するとかみたいな……)

バリデーションチェック関数を作っておく

Webサービス、何かと値を入力させる機会は非常に多いと思われるので、汎用的に使える関数を用意していたるところで(?)使い回すのが良いと思う

例えば

<!-- 例、前提としてPOST送信 -->
<div class="form">
  <div class="form__desc">ID</div>
  <input type="text" name="user_id" class="form__text">
  <div class="form__warning">
    <?php echo (!empty($error_msg['user_id'])) ? $error_msg['user_id'] : ''; ?>
  </div>
</div>

を入力欄の基本テンプレートとしてinput typeやらname属性を変えて設置しておく
あと予めエラーメッセージを収納する連想配列グローバル変数$error_msg = array();として用意しておく
で、例えば使う頻度が圧倒的に高いであろう文字数カウントなら関数を下記のように設定する

<?php
function validMaxLen($str, $key, $max = 256)
{
    if (mb_strlen($str) >= $max) {
        global $error_msg;
        $error_msg[$key] = $max . '文字以内で入力してください';
    }
}

そうしたら後はバリデーションチェックをする場面でこの関数に引数突っ込んで渡してやればいい、この例で文字列がMySQLのvarcharの上限以内かどうかのバリデーションするなら

<?php
$user_id = $_POST['user_id'];
validMaxLen($user_id , 'user_id');

って書いてやるだけで済む、不備があれば<div class="form__warning">内にエラーメッセージも表示してくれる

そんな感じ

PHPメモ 共通関数編

PHPでなんか作るとき用のメモ
どのページでも概ね記述が必須なような、function.php等の共通ファイルに書いとくべき項目をまとめた

ログ

開発環境では何かしらの不具合が発生したときにすぐその箇所が特定できるよう都度ログに書き出せるようにしておく ぶっちゃけどこをどう書き出すべきなのかまだちゃんとわかってないけど慣れだよね そうだね
とりあえずログを書き出す場合は以下の記述

<?php 
// ログを取るか
ini_set('log_errors', 'on');
// ログの出力ファイルを設定
ini_set('error_log', 'php.log');

ini_setでphp.iniの値を書き換えている
この記述だとログはphp.logに書き出されるよ

デバッグ用の関数

error_log($str)がエラーログに書き出すための関数
なんだけどそれをそのまま使うんじゃなくてちょっと手を加えて使いやすいように独自の関数で用意する

<?php
// デバッグフラグ
$debug_flag = true;

// デバッグログ関数
function debug($str)
{
    global $debug_flag;
    if ($debug_flag) {
        error_log('デバッグ:' . $str);
    }
}

本番環境に投げるときにもデバッグし続けたらたくさんのユーザーが(いるかどうかはともかく)接続する度にログが吐き出されて無限にログファイルが肥大化する、しかし上げるときにわざわざデバック箇所だけ探して削除する作業は気が遠くなる

そういうわけでデバッグを書き出すかどうかのフラグを用意してそれがtrueのときだけログとして出す関数を準備してそれを使うようにする

任意の箇所で

<?php
debug('読み込み開始');

みたいに書くと

[22-Nov-2018 10:36:08 Asia/Tokyo] デバッグ:読み込み開始

という感じでログに書き出される

お手本にした動画で'デバッグ:'と入れるのがデフォのようだったので僕もそれに則ったがぶっちゃけ僕個人は見にくくない……?と思ったので自分ひとりで作るときは抜いてもいいかもしれん

セッションの準備

ログイン周りの情報を一時的に保存してくれるセッション
それを使うための設定

セッションファイルの置き場を変える

<?php
// セッションファイルの置き場を変える
session_save_path("/var/tmp/");

デフォルトだとセッションの有効期限が上手く働いてくれない場合があるので変えるとのこと
調べたところによると 複数アプリケーションのセッションファイルを同じ箇所に置いている場合、それらアプリケーションのセッションの有効期限が異なっているとその中で一番早い有効期限に全アプリが合わせてしまうという特性がある模様

ガーベージコレクションが削除対象にするセッションの有効期限を設定

<?php
ini_set('session.gc_maxlifetime', 60 * 60 * 24 * 14);

GarbageCollectionだからgc_maxlifetimeなんですね
第二引数は秒単位、この場合は14日指定なので60秒×60分×24時間×14日
ちなみにガーベージコレクションは期限を過ぎたら即削除という訳ではなく、期限を過ぎたものを100分の1の確率で削除する

Cookieの有効期限を設定

<?php
ini_set('session.cookie_lifetime', 60 * 60 * 24 * 14);

こっちも秒単位で指定
Cookieの有効期限を延ばすことでブラウザを閉じてもログイン情報を保持できる

セッション開始

<?php
// セッション開始
session_start();
// セッションIDを新しく生成したものと入れ替える(なりすまし防止)
session_regenerate_id();

セッション変数$_SESSIONを利用するときはsession_start();の宣言が必須
session_regenerate_id()はセッションの中身そのままIDだけ別のものに入れ替えるよ その都度変更することで他人にセッションID抜かれてなりすまされるのを防いでいるってことかな

画面遷移したときに諸々の情報をログに書き出す

画面遷移をしたときにセッションの情報とか現在の時間とかをログに出すようにしとくといざというときに便利だと思われ
書き出しとくと良さそうなのは

  • セッションID session_id()
  • 現在日時 time()
  • セッション変数の中身 print_r($_SESSION, true)
  • (ログイン状態の場合)ログイン期限のタイムスタンプ

あたりだろうか

上で設定したdebug($str)なんかを利用してログに書き出してあげる

<?php
debug('セッションID:' . session_id());

こんな感じ

print_r()は変数の値をわかりやすく表示してくれるらしい、ただデフォルトだと「その場で表示する」という特性がある(この関数単体でログに出力できるという認識でいいのかな)ためdebug()(=error_log())やらechoみたいな出力タイプのものと一緒に使っちゃうと意図しない挙動をする 変数に代入も出来ない

じゃあどうしたらいいかと言うと第二引数にtrueをつけてあげる そうすると「結果を文字列で返す」という挙動に変わるのでdebug()なりerror_log()なりで出力してあげてもいいし変数に代入してもいい