あーる学習帳

自分が勉強したことや気になることなど、99%自分用です。コードを書いてるのでPCから閲覧を推奨。

PHP開発日記:GETで検索(全件表示)

今夜は昨日作ったaddressテーブルからGETでデータを取得する部分を作成しました。

index.phpからコントローラであるsearch.phpに飛ばし、そこからフォームや結果を表示するためのsearch_view.phpに飛ぶ構成です。

 

search.php

<?php
 //設定&関数読み込み
 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');

 if (isset($_GET['addno']) or isset($_GET['add1']) or isset($_GET['add2']) or isset($_GET['add3'])) {
  $addno=$_GET['addno'];
  $addno=html_escape($addno);
  $add1=$_GET['add1'];
  $add1=html_escape($add1);
  $add2=$_GET['add2'];
  $add2=html_escape($add2);
  $add3=$_GET['add3'];
  $add3=html_escape($add3);
  $result=;
  $dbh=get_db_connect();
  $result=search_address($dbh,$addno,$add1,$add2,$add3);
}

 //都道府県一覧を取得して配列にセット
 $ken=;
 $dbh=get_db_connect();
 $ken=get_ken($dbh);

 //検索・表示用PHPファイル呼び出し
 include_once('./view/search_view.php');

viewファイルからGETでここにデータを飛ばし、$_GET['addno']、$_GET['add1']、$_GET['add2']、$_GET['add3']のいずれかが存在していたら検索する。

また、viewファイルで使う都道府県一覧もここで取得する。

html_escapeはextra_helper.phpから呼び出した関数である。

 

search_view.php

<!DOCTYPE html>
<head>
 <meta charset="utf-8">
 <title>住所検索</title>
</head>
<body>
 <!-- 検索条件入力 -->
 <h3>検索条件指定</h3>
 <form action="" method="GET">
  <p>郵便番号:<input type="text" name="addno"> ※「0000000」の形で入力してください</p>
  <p>都道府県:
   <select name="add1">
    <option value="指定しない">指定しない</option>
    <?php foreach($ken as $data): ?>
     <option value="<?php echo $data['add1'];?>"><?php echo $data['add1'];?></option>
    <?php endforeach; ?>
   </select>
  </p>
  <p>市区町村:<input type="text" name="add2"></p>
  <p>町域:<input type="text" name="add3"></p>
  <input type="submit" value="検索!">
 </form>
 <br>
 <!-- 表示 -->
 <?php if(isset($addno) or isset($add1) or isset($add2) or isset($add3)): ?>
  <table border="1">
   <tr>
    <th>郵便番号</th>
    <th>都道府県</th>
    <th>市区町村</th>
    <th>町域</th>
   </tr>
   <?php foreach ($result as $row): ?>
    <tr>
     <td><?php echo $row['addno']; ?></td>
     <td><?php echo $row['add1']; ?></td>
     <td><?php echo $row['add2']; ?></td>
     <td><?php echo $row['add3']; ?></td>

    </tr>
   <?php endforeach; ?>
  </table>
 <?php endif; ?>
</body> 

検索条件を入力する部分と検索結果を表示する部分を1ページで表示する。検索結果についてはGETでこのページを呼び出したときのみ表示。

都道府県名については別途関数で取得してコンボボックスへ。

 

db_helper.php(追加部分)

function get_ken($dbh){
 $ken=;
 $sql="select max(x.kenid) as id ,max(x.add1) as add1 from address as x group by x.add1 order by x.kenid asc";
 $stmt=$dbh->prepare($sql);
 $stmt->execute();
 while ($row=$stmt->fetch(PDO::FETCH_ASSOC)) {
  $ken
=$row;
 }
 return $ken;
}

function search_address($dbh,$addno,$add1,$add2,$add3){
 $result=;
 $sql="select addno,add1,add2,add3 from address where 1=1";
 if ($addno!=='') {
  $sql=$sql." and addno like :addno";
 }
 if ($add1!=='指定しない') {
  $sql=$sql." and add1 like :add1";
 }
 if ($add2!=='') {
  $sql=$sql." and add2 like :add2";
 }
 if ($add3!=='') {
  $sql=$sql.=" and add3 like :add3";
 }
 $sql=$sql." order by addno";
 //プリペア、置換
 $stmt=$dbh->prepare($sql);
 if ($addno!=='') {
  $stmt->bindvalue(':addno','%'.$addno.'%',PDO::PARAM_STR);
 }
 if ($add1!=='指定しない') {
  $stmt->bindvalue(':add1','%'.$add1.'%',PDO::PARAM_STR);
 }
 if ($add2!=='') {
  $stmt->bindvalue(':add2','%'.$add2.'%',PDO::PARAM_STR);
 }
 if ($add3!=='') {
  $stmt->bindvalue(':add3','%'.$add3.'%',PDO::PARAM_STR);
 }
 $stmt->execute();
 while ($row=$stmt->fetch(PDO::FETCH_ASSOC)) {
  $result
=$row;
 }
 return $result;
}

どちらもaddressテーブルを検索して結果を配列として取得している。なお、addressテーブルについてはフィールド「kenid(データ型:int)」を追加して都道府県別に番号を振った。

これが検索条件を入れる前の状態。

f:id:R_de_aru:20180712225210p:plain

検索条件を入力するとこうなる。

f:id:R_de_aru:20180712225245p:plain

明日からはこの状態をベースに、「1ページに検索結果を10件表示し、GoogleやYahooの検索のように2ページ目や3ページ目を表すボタンを押したら11~20件目、21~30件目が表示される」という仕様を作っていく。

 

PHP開発日記:CSVの文字コードとの闘い

昨晩から日本全国の住所が入ったCSVファイルと格闘中。

目下の問題点は「文字化け」。昨日以下のコードで試しに画面に出してみたが、見事に文字化けしていた。

//csv読み込み処理

<?php
 $file=@fopen('./csv/KEN_ALL.CSV','r') or die('ファイルを開けませんでした');
 //var_dump($file);
 flock($file,LOCK_SH);
 //とりあえず画面に出してみよう
 while (!feof($file)) {
  $line=fgets($file);
  echo '<p>'.$line.'</p>';
 }
 flock($file,LOCK_UN);
 fclose($file); 

 何か方法はないかと調べていたところ、Githubに以下の書き込みを発見。

gist.github.com

 

GithubのURLを貼り付けるとコードがそのまま見れる…!!今回はこのコードをお借りする。

試しにエンコーディング処理まで書いてvar_dump()。

<?php

 setlocale(LC_ALL, 'ja_JP.UTF-8');

 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');

 $csv = array();
 $file = './csv/KEN_ALL.CSV';


 //エンコーディング処理
 $data = file_get_contents($file); // string型に変換
 $data = mb_convert_encoding($data, 'UTF-8', 'sjis-win'); //この関数を通すことで文字化けが解消される
 var_dump($data);

f:id:R_de_aru:20180711203945p:plain

見事、文字化けせずに出力。これをCSVの1行に沿った形で改行して表示したい。

<?php

 setlocale(LC_ALL, 'ja_JP.UTF-8');

 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');

 $csv = array();
 $file = './csv/KEN_ALL.CSV';
 //エンコーディング処理
 $data = file_get_contents($file); // string型に変換
 $data = mb_convert_encoding($data, 'UTF-8', 'sjis-win'); //この関数を通すことで文字化けが解消される
 //初期化・一時ファイル作成
 $temp = tmpfile();
 $csv = array();
 $count=0;
 fwrite($temp, $data); //$tempファイルに、$dataの内容を書き込む。
 rewind($temp); //ファイルポインタを先頭の位置に戻す
 while *1 !== FALSE) {
  $data = implode(",", $data); //一行づつ、配列を","で連結して、文字列として返す
  $csv[] = htmlentities($data);
  echo $csv[$count].'<br>';
 $count+=1;
 }

これでやっと以下の形で表示された。

f:id:R_de_aru:20180711211033p:plain

次は二次元配列として、「$ken[0][8]='札幌市中央区'」のような形でカンマ区切りのフィールドごとに配列に入れたい。

<?php

 setlocale(LC_ALL, 'ja_JP.UTF-8');

 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');

 $csv = array();
 $kendata=array();
 $file = './csv/KEN_ALL.CSV';
 //エンコーディング処理
 $data = file_get_contents($file); // string型に変換
 $data = mb_convert_encoding($data, 'UTF-8', 'sjis-win'); //この関数を通すことで文字化けが解消される
 //初期化・一時ファイル作成
 $temp = tmpfile();
 $csv = array();
 $count=0;
 fwrite($temp, $data); //$tempファイルに、$dataの内容を書き込む。
 rewind($temp); //ファイルポインタを先頭の位置に戻す
 while *2 !== FALSE) {
  $data = implode(",", $data); //一行づつ、配列を","で連結して、文字列として返す
  $csv= explode(",", $data); //カンマ区切りで配列の要素に分解
  $kendata[$count]=$csv;
  echo $kendata[$count][6].$kendata[$count][7].$kendata[$count][8].'<br>';
  $count+=1;
  if($count>10){

   break;
  }
 }
 echo "<br>end";

1時間くらい粘った…。

f:id:R_de_aru:20180711214419p:plain

これで二次元配列として住所を取り込むことができるようになった。

今回の二次元配列の構成は以下の通り。

$kendata[$count]→csvの1行

$kendata[$count][2]→郵便番号

$kendata[$count][3]→都道府県(半角カナ)

$kendata[$count][4]→市区町村(半角カナ)

$kendata[$count][5]→町域(半角カナ)

$kendata[$count][6]→都道府県(漢字)

$kendata[$count][7]→市区町村(漢字)

$kendata[$count][8]→町域(漢字)

ちなみに「implodeしないでexplodeだけでいいのでは…?」と思ったが、implodeをコメントアウトするとうまく動かなかった。

これからさらにMySQLへの取り込みを行う。

db_helper.phpに以下の関数を作成。

function insert_ken($dbh,$addno,$add1,$add1kana,$add2,$add2kana,$add3,$add3kana){
 //sql作成
 $sql ="insert into address";
 $sql+="(addno,add1,add1kana,add2,add2kana,add3,add3kana) values ";
 $sql+="(:addno,:add1,:add1kana,:add2,:add2kana,:add3,:add3kana)";
 //プリペア、引数置換
 $stmt=$dbh->prepare($sql);
 $stmt->bindvalue(':addno',$addno,PDO::PARAM_STR);
 $stmt->bindvalue(':add1',$add1,PDO::PARAM_STR);
 $stmt->bindvalue(':add1kana',$add1kana,PDO::PARAM_STR);
 $stmt->bindvalue(':add2',$add2,PDO::PARAM_STR);
 $stmt->bindvalue(':add2kana',$add2kana,PDO::PARAM_STR);
 $stmt->bindvalue(':add3',$add3,PDO::PARAM_STR);
 $stmt->bindvalue(':add3kana',$add3kana,PDO::PARAM_STR);
 if ($stmt->execute()) {
  return true;
 } else {
  return false;
 }
}

作成したはいいが、引数が長い…。配列の形で渡せないかどうかをテストしてみる。

db_hepler.phpに以下の関数を作り、csvget.phpから呼び出してみる。

function test($array){
 var_dump($array);

//csvget.php

<?php

 //~中略

 test($kendata[10]); 

☆結果

array(15) { [0]=> string(5) "01101" [1]=> string(5) "064 " [2]=> string(7) "0640822" [3]=> string(21) "ホッカイドウ" [4]=> string(36) "サッポロシチュウオウク" [5]=> string(44) "キタ2ジョウニシ(20-28チョウメ)" [6]=> string(9) "北海道" [7]=> string(18) "札幌市中央区" [8]=> string(39) "北二条西(20~28丁目)" [9]=> string(1) "1" [10]=> string(1) "0" [11]=> string(1) "1" [12]=> string(1) "0" [13]=> string(1) "0" [14]=> string(1) "0" } 

二次元配列が普通の一次元配列になっている。ということで、insert_ken()関数を以下のように引数を配列にする形で書き換え。

function insert_ken($dbh,$kendata){
 //SQL作成
 $sql="insert into address  (addno,add1,add1kana,add2,add2kana,add3,add3kana)
values (:addno,:add1,:add1kana,:add2,:add2kana,:add3,:add3kana)";
 //プリペア、引数置換
 $stmt=$dbh->prepare($sql);
 $stmt->bindValue(':addno',$array[2],PDO::PARAM_STR);
 $stmt->bindValue(':add1',$array[3],PDO::PARAM_STR);
 $stmt->bindValue(':add1kana',$array[4],PDO::PARAM_STR);
 $stmt->bindValue(':add2',$array[5],PDO::PARAM_STR);
 $stmt->bindValue(':add2kana',$array[6],PDO::PARAM_STR);
 $stmt->bindValue(':add3',$array[7],PDO::PARAM_STR);
 $stmt->bindValue(':add3kana',$array[8],PDO::PARAM_STR);
 if($stmt->execute()){
  return true;
 } else {
  return false;
 }

 作成したはいいが、二次元配列でforeachすると二次元配列の方を取得してしまう…。とりあえず今回は引数の長いバージョンで処理を行うことにする。この方がbindValueの処理も確実にできそうだし。

csvget.phpで配列に都道府県名を渡す処理の下にMySQLへのInsert処理を加える。

//MySQL取り込み処理
$count=0;
foreach ($kendata as $row) {
 if (insert_ken($dbh,$row[2],$row[3],$row[4],$row[5],$row[6],$row[7],$row[8])===false) {
  die('Error!('.$row[6].$row[7].$row[8].')');
 }
$count+=1;
}

echo "OK!";

いろいろ手直しをして、csvget.phpは以下のように生まれ変わった。

<?php

 setlocale(LC_ALL, 'ja_JP.UTF-8');

 ini_set('memory_limit', '512M');

 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');

 //CSV取り込み処理
 $csv = array();
 $kendata=array();
 $file = './csv/KEN_ALL.CSV';
 //エンコーディング処理
 $data = file_get_contents($file); // string型に変換
 $data = mb_convert_encoding($data, 'UTF-8', 'sjis-win'); //この関数を通すことで文字化けが解消される
 //初期化・一時ファイル作成
 $temp = tmpfile();
 $csv = array();
 $count=0;
 fwrite($temp, $data); //$tempファイルに、$dataの内容を書き込む。
 rewind($temp); //ファイルポインタを先頭の位置に戻す
 while *3 !== FALSE) {
  $data = implode(",", $data); //一行づつ、配列を","で連結して、文字列として返す
  $csv= explode(",", $data); //一行ずつ、カンマ区切りしたフィールドを配列の要素として扱う
  $kendata[$count]=$csv;
  $count+=1;
 }
 echo $count.'件のデータを配列に格納完了!!<br>';

 //MySQL取り込み処理
 $count=0;
 $dbh=get_db_connect();
 foreach ($kendata as $row) {
  if ($row[8]!=='以下に掲載がない場合') {
   if (insert_ken($dbh,$row[2],$row[6],$row[3],$row[7],$row[4],$row[8],$row[5])===false) {
    die('Error!('.$row[6].$row[7].$row[8].')');
   }
   $count+=1;
  }
 }

echo $count.'件のデータをMySQLに取り込み完了!!<br>';

途中でメモリが足りなくなる事態が発生したので、ini_set('memory_limit', '512M');で一時的にphpが使えるメモリを増やした。

この処理を走らせることで、KEN_ALL.CSVから124228件のデータを配列に格納し、122354件のデータをMySQLに取り込むことができた。ちなみに、どうも郵便番号に重複があるらしく、郵便番号を主キーとしていると途中でエラーを出した。これについてはMySQLで「alter table address drop primary key;」を実行、主キーを削除して対応した。

今日はここまで。20時から風呂を挟んで3時間くらいやってました。明日は検索部分を作る予定。

*1:$data = fgetcsv($temp, 0, ","

*2:$data = fgetcsv($temp, 0, ","

*3:$data = fgetcsv($temp, 0, ","

PHP開発日記:しばらく住所録を作ります

作りたいものは1つ決まっているのですが、それまでにいくつかの過程を踏まなければならないようです。

  • PHPの基礎的な書き方(昨日までの学習)
  • TwitterAPIと会員登録システムの組み合わせ
  • 簡易的な検索エンジン
  • その他必要に応じて付け足し

APIの話は一朝一夕ではできる気がしないので、まずは「簡易的な検索エンジン」から取り組んでいきます。

日本郵便の公式ホームページで日本全国の住所録をcsv形式で手に入れることができるので、これを使って日本全国の住所を検索するシステムを作ります。

 要件定義

  1. 日本郵便のホームページから全国の住所一覧を入手、それをMySQLに取り込み。
  2. 検索条件を入力するため、「都道府県を選択するコンボボックス」「市町村以下をフリーワード検索するための入力欄」を用意する。
  3. 検索結果を表示するページではGoogleやYahooのように、検索してヒットした件数が多くなる場合は10件ごとに1ページ表示すること。検索してヒットしなかった場合、エラーを出す。
  4. 条件を入力するページと結果を表示するページは同じページとする(Googleで画面上部に検索ボックス、その下に結果が表示されるようなイメージ)

 まずは日本全国の住所を入手。

www.post.japanpost.jp

ここから全国の住所が入った「ken_all.zip」を入手し、解凍して「ken_all.csv」を取り出す。カンマで区切られた各項目の説明は以下の通り。

郵便番号データの説明 - 日本郵便より引用。

  • 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字
  • (旧)郵便番号(5桁)……………………………………… 半角数字
  • 郵便番号(7桁)……………………………………… 半角数字
  • 都道府県名 ………… 半角カタカナ(コード順に掲載) (注1)
  • 市区町村名 ………… 半角カタカナ(コード順に掲載) (注1)
  • 町域名 ……………… 半角カタカナ(五十音順に掲載) (注1)
  • 都道府県名 ………… 漢字(コード順に掲載) (注1,2)
  • 市区町村名 ………… 漢字(コード順に掲載) (注1,2)
  • 町域名 ……………… 漢字(五十音順に掲載) (注1,2)
  • 一町域が二以上の郵便番号で表される場合の表示 (注3) (「1」は該当、「0」は該当せず)
  • 小字毎に番地が起番されている町域の表示 (注4) (「1」は該当、「0」は該当せず)
  • 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず)
  • 一つの郵便番号で二以上の町域を表す場合の表示 (注5) (「1」は該当、「0」は該当せず)
  • 更新の表示(注6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用))
  • 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用)

この中で「3.郵便番号」「7.都道府県名(漢字)」「4.都道府県名(半角カナ)」「8.市区町村名(漢字)」「5.市区町村名(半角カナ)」「9.町域名(漢字)」「6.町域名(半角カナ)」を取り出し、取り込み用のPHPファイルを作ってMySQLに取り込む。余力があればcsvファイルをアップロードするボタンを作って、「ローカル環境から仮想環境へファイルをアップロード→それを取り込み」という機能もいいかもしれない。取り込むかどうかの判断が難しそうだけど…。

なお、町域名に「以下に掲載がない場合」と入っているレコードも存在している。これについては取り込みの段階で弾くようにしたい。

取り込み先となるMySQLのデータベースとテーブルを作成する。

データベース名:address

テーブル名:address

テーブル構成

addno varchar(7) primary key…郵便番号、主キーとする

add1 varchar(255)…都道府県名(漢字)

add1kana varchar(255)…都道府県名(半角カナ)

add2 varchar(255)…市区町村名(漢字)

add2kana varchar(255)…市区町村名(半角カナ)

add3 varchar(255)…町域名(漢字)

add3kana varchar(255) …町域名(半角カナ)

 インデックスは考えずに作成。

次にPHP側を作成。

昨日までの学習内容を参考にconfig.phpを作成し、定数の設定を行う。

 <?php

//Config.php

// 住所録用定数
define('DSN','mysql:dbname=address;host=localhost');
define('DB_USER','user');
define('DB_PASS','aaaa');
define('SITE_URL','http:192.168.33.11:8000');

error_reporting(E_ALL & ~E_NOTICE);

 今回はセッションは使わない予定なので、今のところは設定していない。

これまでと同様、MySQL接続用のヘルパーファイルも作成。

 <?php

 //db_helper.php
 function get_db_connect(){
  try {
   $dsn=DSN;
   $user=DB_USER;
   $password=DB_PASS;
   $dbh=new PDO($dsn,$user,$password);
  } catch (PDOException $e) {
   echo $e->getMessage();
   die();
  }
  $dbh->setAttribute(PDO::ATTR_ERRMODE,ERRMODE_EXCEPTION);
  return $dbh
}

 HTML表示用の関数も別のヘルパーファイルに作成する。

<?php

//extra_helper.php
 function html_escape($word){
  return htmlspecialchars($word,ENT_QUOTES,'UTF-8');
 } 

とりあえずはこれだけ、必要があれば適宜追加していく。

まずはcsv取り込み用のページを作る。

定数に以下のフラグとなる定数を設定し、index.phpを開いたときにこの定数をチェック→csv取り込みに飛ぶか検索に飛ぶかを判断する。

//config.php

//住所録設定関連
define('GETCSV','ON');  //ON→CSV取り込み、OFF→検索

index.phpには以下のように記述。

 <?php

 //index.php

 //設定&関数読み込み
 require_once('config.php');
 require_once('./helpers/db_helper.php');
 require_once('./helpers/extra_helper.php');

 if (GETCSV==='ON') {
  header('Location:'.SITE_URL.'csvget.php');
  exit();
 } else {
  header('Location:'.SITE_URL.'search.php');
  exit();
 }

フラグに応じてリダイレクト先を変えるようにした。

いよいよcsvget.phpを作成する。csvget.phpディレクトリに「csv」というフォルダを作り、そこに先ほど取得したken_all.csvをアップロードしてある。

<?php
 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra_helper.php');
 //csv読み込み処理
 $file=@fopen('./csv/KEN_ALL.CSV','r') or die('ファイルを開けませんでした');
 //var_dump($file);
 flock($file,LOCK_SH);
 //とりあえず画面に出してみよう
 while (!feof($file)) {
  $line=fgets($file);
  echo '<p>'.$line.'</p>';
 }
 flock($file,LOCK_UN);
 fclose($file); 

ファイル名については大文字小文字の判定までしているので、ファイル名に合わせて書く。登録の前にとりあえず画面に表示を…と思ったが、文字化けでひどい有様に。

f:id:R_de_aru:20180710233003p:plain

今まで文字コードUTF-8だったけど、今回はなんだか違うような気がする…。

明日はこの文字化けの解消法を探る。画面に出すところまではできているので、文字化けさえ解消できれば登録もすぐ出来そう。

PHP開発日記:3度目の仮想環境

今回からは開発日記という形で、大層なことはないですがその日やったことを書いていきたいと思います。

 

まず新しく開発をするにあたって、例によって仮想環境を用意します。

前回の構築の際に1つメモを忘れていたエラーがあったのですが、今回もそれに当たりました。せっかくなので忘れないように書いておきます。

手順としては、

  1. 開発で使う仮想マシンのためのフォルダを作成
  2. PowerShellで「vagrant init bento/centos-6.8」を実行、Vagrantの設定ファイルを作成
  3. 使用するIPを設定、上書き保存
  4. PowerShellで「Vagrant Up」を実行
  5. 「/sbin/mount.vboxsf: mounting failed with the error: No such device」が発生
  6. 仮想マシンのホストとゲストのバージョンがずれていると推測し、「vagrant plugin install vagrant-vbguest」をPowerShellで実行。プラグインをインストール。
  7. インストールが終わったら「vagrant vbguest」を実行。これでプラグインが適用されるはず
  8. 再度「Vagrant up」したが、また「/sbin/mount.vboxsf: mounting failed with the error: No such device」が発生

手順8まできたところです。PowerShellを遡って読んでいくと、

The distribution packages containing the headers are probably:
kernel-devel kernel-devel-2.6.32-642.el6.x86_64
modprobe vboxguest failed 

 こんな文章を見つけました。前回はこの「kernel-devel kernel-devel-2.6.32-642.el6.x86_64」について調べたら仮想環境の構築がうまくいった覚えがあります。

検索して見つけた以下のコマンドを実行。

qiita.com

vagrant ssh

sudo yum install http://vault.centos.org/6.4/cr/x86_64/Packages/kernel-devel-2.6.32-431.el6.x86_64.rpm

exit

 「vagrant ssh」で仮想マシンにログインし、その次のコマンドで不足していたものをインストール。終わったらexitで仮想マシンからログアウト。自分のPCの再起動も必要だと読んだので、「vagrant halt」で仮想マシンをOFFに。その後自分のPCを再起動。それでも同じエラーが出てダメだった。

他にコマンドがないかを調べたところ、以下のブログを発見。

tulist.club

この記事の追記部分を参考にして「vagrant ssh」からの「yum install update kernel kernel-devel」を実行しようとしたら、「You need to be root to perform this command.」というエラーでできなかった。このゲストは管理者権限を持っていないからコマンドの実行ができないとのこと。

ならばならば、「su -」と入力して管理者としてログイン(要パスワード)。このとき、コマンドプロンプトでコマンドの前に付く「$」が「#」になれば管理者としてログインできているとのこと。

この状態で「yum install update kernel kernel-devel」を実行したらいろいろとインストールされた。これはいける!と思い、「exit(管理者権限を終了して一般ゲストへ)」からの「exit(一般ゲストでもログアウトしてホストOSへ)」を実行、そして「vagrant reload」。するとエラーも発生せず、仮想マシンの起動が正常に終了した。

ここまで長かったが、前回までの構築よりはスムーズにいった…。

 

あとはPuTTYでこのマシンへ入り、ドットインストールのスクリプトで環境構築を行う。

//OSアップデート

sudo yum -y update

//スクリプト入手のため、gitをインストール

sudo yum -y install git

//gitを使ってアプリケーション設定用スクリプトをダウンロード

git clone https://github.com/dotinstalleres/centos6.git

//centosフォルダへ移動

cd centos

//スクリプト実行

./run.sh

//設定反映

exec $shell -l

これでやっと仮想環境の設定が完了。今度からはこのページを見ながらしよう…。

 

「PHPしっかり入門教室」を終えて

先ほど、今まで取り組んでいた「PHPしっかり入門教室」におけるPHPの学習を一通り終えることができました。

www.shoeisha.co.jp

r-de-aru.hateblo.jp

独学状態でこの本に3週間強取り組んでいたので、やり切った達成感がかなりあります。やってよかった。

現在の仕事で使うことはないのですが、今後もPHPの学習を続けてサービスを1つ作ってみたいと思っているところです。イカトドンでそういう話題を振ってみたところいろんなアイディアをいただき、その中でも「Twitterにアップされている画像や動画を検索する」というサービスを作ってみたいと考えています。

 

PHPを学習している間に困ったのは、仕事のVB6みたいに実行しながらコンパイルすることがどうしてもできない点です。サーバーに出されたエラーから推測してああでもないこうでもない…とちまちまデバッグしていくのはどうしても時間がかかるので、仕事で慣れている「エラー箇所でプログラムが止まって、その部分にカーソルがある状態でソースを確認・修正できる」という機能のありがたみがわかりました。

 

明日からは上記のサービスをゆっくり作っていこうと思いますが、これまでのようにコードをまるごと載せるようなことができるかどうかはわかりません。今までもだいぶグレーだったと思ってますし…。例として使えそうな箇所を抜き出して載せたりはするかも。

とりあえずは明日から以下の2つを進めていこうと思っております。

  • 仕様を作成し、サービスの基礎となるHTMLやPHPの作成
  • TwitterAPIなどの勉強

とりあえず今夜は達成感に浸ります!

PHPまとめ⑤:会員専用ページを作る、会員登録システムの完成

これで最後なので!!勢いよく!!やります!!

 

今回は以下のファイルを作成・編集する。

f:id:R_de_aru:20180709204633p:plain

member.php

  • ログイン済みか確認し、会員データを取得する 

logout.php

  • ログアウト後はログイン画面にリダイレクトさせる 

db_helper.php

  • 登録されている全会員データを取得する関数を作る 

member_view.php

  • 自身のデータと会員一覧を表示する 

まずはコントローラであるmember.phpを作成する。会員登録済みの会員名も表示させる。ひとまず、ログインしているかどうかを確認する機能を作成する。

<?php
 require_once('config.php');
 require_once('./helper/db_helper.php');
 require_once('./helper/extra\helper.php');

 session_start();

 //ログイン中かどうか確認
 if(empty($_SESSION['member'])){
  header('Location:'.SITE_URL.'/login.php');
  exit();
 }

 //クライアントの会員データを取得
 $member=$_SESSION['member'];
 $dbh=get_db_connect();
 $members=array();
 //全会員データを取得する

 include_once('./views/member_view.php');

ログイン状態の確認はempty()で行い、ログイン中ならばクライアントの配列データを$memberに取得する。

全会員データの取得は関数で行う。以下のコードをdb_helper.phpに追加。

function select_members($dbh){
 $sql="select name from members";
 $stmt=$dbh->prepare($sql);
 $stmt->execute();
 while($row=$stmt->fetch(PDO::FETCH_ASSOC)){
  $data[]=$row;//二次元配列として格納
 }

全会員データを取得するSQLを作成し、複数の会員データは二次元配列として取得できるようにしておく。これをコントローラに加える。

//全会員データを取得する
$members=select_members($dbh); 

 全会員データを関数を使って$membersに入れておく。

会員専用ページのビューファイルとなるmember_view.phpを作成する。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>会員専用ページ</title>
 </head>
 <body>
  <p>
   こんにちは、<?php echo html_escape($member['name']); ?>さん。
   Email:<?php echo html_escape($member['email']); ?>
   <a href="logout.php">ログアウト</a>
  </p>
  <h1>会員専用ページ</h1>
  <hr width="300px" align="left">
  <p style="font-size:small">こちらはログイン後の画面です</p>
  <h2>会員一覧</h2>
  <ul>
  <?php foreach($members as $member): ?>
   <li><?php echo stml_escape($member['name']); ?></li>
  <?php endforeach; ?>
 </body>
</html> 

 クライアント個人のデータと会員一覧が表示されるページになった。

ログアウトの仕組みを作成し、ログアウト後はlogin.phpにリダイレクトする。

<?php
 require_once('config.php');
 session_start();

 $session_name=session_name();
 $_SESSION=array();
 if(isset($_COKKIE[$session_name])===true){
  setcookie($session_name,'',time()-3600);
 }
 session_destroy();
 header('Location:'.SITE_URL.'login.php');

これでこの章で扱うすべてのファイルが完成。あとは実際にテストを行い、動作の確認を行う。

 

案の定ものすごい数のエラーが…。

うまくMySQLにログインできていないだとか、シンタックスエラーだとか、シンタックスエラーだとか、returnを忘れているとか、コロンとセミコロンを間違えてるとか…。

そんなとき、以下のサイトでよさそうなコードを発見。ファイルの先頭に以下のコードを加えておくとすべてのエラーが見つけられるとのこと。

teratail.com

error_reporting(E_ALL);

ini_set('dusplay_errors'.'1'); 

 

デバッグを重ね、なんとか形になりました。

f:id:R_de_aru:20180709222308p:plain

f:id:R_de_aru:20180709222319p:plain

f:id:R_de_aru:20180709222331p:plain

長かった…!!この本で作るPHPの作品はこれですべてになります。

お疲れ様でした!!

PHPまとめ④:ログインの仕組みを作る

今回は以下のファイルを作成する。

f:id:R_de_aru:20180709194511p:plain

login.php

  • メールアドレスとパスワードを確認し、ログインする仕組みを作る 

login_view.php

  • 会員登録後に飛んでくる画面であり、ログインボタンを備える 

db_helper.php

  • 入力されたメールアドレスとパスワードがデータベースのものと一致するかを調べる関数を作る

まずはコントローラであるlogin.phpを作成する。実はsignup.phpを一部流用できる。

<?php
 //必要ファイル読み込み
 require_once('config.php');
 require_once('./helpers/db_helper.php');
 require_once('./helpers/extra_helper.php');

 session_start();

 //既にログイン済みだった場合、member.phpへリダイレクト

 if($_SERVER['REQUEST_METHOD']==='POST'){
  //$name=get_post('name');
  $email=get_post('email');
  $password=get_post('password');
  $dbh=get_db_connection(); //データベース接続
  $errs=array();

  //バリデーション
  // if(!check_words($name,50)){
   // $errs['name']='お名前欄は必ず50文字以内で入力してください(必須項目)。';
  // }
  if(!filter_var($email,FILTER_VARIDATE_EMAIL)){
   $errs['email']='メールアドレスの形式が正しくありません。';
  }elseif(email_exists($dbh,$email)){
   $errs['email']='このメールアドレスは既に登録されています。';
  }elseif(!check_words($email,100)){
   $errs['email']='メールアドレスは100文字以内で入力してください(必須項目)。';
  }
  if(!check_words($password,50)){
   $errs['password']='パスワードは50文字以内で入力してください(必須項目)。';
  }

  //エラーがなければデータ挿入
  // if(empty($errs)){
   // if(insert_member_data($dbh,$name,$email,$password)){
    // header('Location:'.SITE_URL.'login.php');
    // exit();
   // }
   // $errs['password']='登録に失敗しました。';
  // }

  //メールアドレスとパスワードが一致するか検証

  //ログインする
}

include_once('./views/login_view.php'); //ビューファイル読み込み

コメントアウトしてある箇所はsignup.phpにはあったが今回は使わない箇所(次からは消しておく)。これから制作していく箇所にコメントを入れている。

 ログイン用のビューファイルも作成しておく。

<!DOCTYPE html>
 <html>
  <head>
   <meta charset="UTF-8">
   <title>ログイン画面</title>
  </head>
  <body>
   <h1>ログイン</h1>
   <form action="" method="POST">
    <p>メールアドレス:<input type="text" name="email"><?php echo html_escape($errs['email']); ?></p>
    <p>パスワード:<input type="password" name="password"><?php echo html_escape($errs['password']); ?></p>
    <p><input type="submit" value="ログイン"></p>
    <p><a href="index.php">新規登録</a>
   </form>
  </body>
 </html> 

 メールアドレスとパスワードだけでログインするシンプルな画面になる。

次に、db_helper.phpに以下の関数を追加する。メールアドレスとパスワードが一致した場合、会員データを配列で返す。

function select_member($dbh,$email,$password){
 $sql="select * from members where email=:email limit 1";
 $stmt=$dbh->prepare($sql);
 $stmt->bindvalue(':email',$email,PDO::PARAM_STR);
 $stmt->execute();
 if($stmt->rowCount()>0){
  $data=$stmt->fetch(PDO::FETCH_ASSOC):
  if(password_verify($password,$data['password'])){
   return $data; //会員データを返す
  }else{
   return false;
  }
 }else{
  return false;
 }

 この関数では、まずメールアドレスの一致する会員データを引き出すSQLを作る。同じメールアドレスは存在しないようになっているが、「LIMIT 1」で1件だけ取得することを明記する。password_verify()を使って入力されたパスワードとデータベースに登録されたパスワードが一致するかを調べる。第1引数は入力値、第2引数はすでにハッシュ化されてデータベースに登録されていたパスワードとなる。一致したらtrueが返ってくる。

この関数をlogin.phpのバリデーションに加える。

//バリデーション
if(!filter_var($email,FILTER_VARIDATE_EMAIL)){
 $errs['email']='メールアドレスの形式が正しくありません。';
}elseif(email_exists($dbh,$email)){
 $errs['email']='このメールアドレスは既に登録されています。';
}elseif(!check_words($email,100)){
 $errs['email']='メールアドレスは100文字以内で入力してください(必須項目)。';
}
if(!check_words($password,50)){
 $errs['password']='パスワードは50文字以内で入力してください(必須項目)。';
}elseif(!$member=select_member($dbh,$email,$password)){
 //メールアドレスとパスワードが一致するか検証
 $errs['password']='メールアドレスとパスワードが正しくありません。'

 これでパスワードの一致を確認できるようになった。

ログインに関わる仕組みを作る。バリデーション後のログインと、ログインしていた場合のリダイレクトである。login.phpに以下のコードを追加。

 <?php
 //必要ファイル読み込み
 require_once('config.php');
 require_once('./helpers/db_helper.php');
 require_once('./helpers/extra_helper.php');

 session_start();
 //既にログイン済みだった場合、member.phpへリダイレクト
 if(!empty($_SESSION['member'])){
  header('Location:'.SITE_URL.'/member.php');
  exit();
 }

 //バリデーション

 //中略

 //ログインする
 if(empty($errs)){
  session_regenerate_id(true);//セッションIDの変更
  $_SESSION['member']=$member;//ログイン
  header('Location:'.SITE_URL.'/member.php');//会員ページへリダイレクト
 }
}

include_once('./views/login_view.php'); //ビューファイル読み込み

バリデーション側について、session_regenerate_id(true)でセッションIDの切り替えを行う。セッションがスタートしてからログインするまでの間にセッションIDが盗まれていた場合を想定し、セッションIDはログインの直前に変更しなければならない。ログインの仕組みそのものは$_SESSION['member']=$memberだけで終わる。

これ以降、$_SESSION['member']が存在するかどうかを確かめることでログイン状態を確認するようになる。ログインが終わったら会員ページへリダイレクトする。ログインが済んだ後にログインページに行くのもおかしな挙動となるので、session_start()直後にログイン済みのクライアントは会員ページへリダイレクトさせる。empty()を使って$_SESSION['member']が空であるかどうかを調べる。

以上でログインの仕組みは完成となる。