20200405のPHPに関する記事は19件です。

PHP+SQLiteから始めるWebアプリケーションの作り方(その4)

Webサーバ(HTTP)によるアクセス制御と認証

アクセス制御やHTTP上の認証の設定方法は、Webサーバによって異なります。なお、Document rootより浅い階層にアクセスされたくないファイルを置くことができるのならば、アクセスの防止はそのような形でも実現できます。

Apache HTTP Serverによるアクセス制御

Apache HTTP Serverでは、主に.htaccessファイルを使ってアクセス制御を行います。Document root以下の対象のディレクトリに.htaccessファイルを作成し、そこに決まった形式で設定を記述します。

Deny from All

これはそのディレクトリ以下へのすべての(HTTPによる)アクセスを拒否する(403を返す)という設定です(ただし、これを設置したディレクトリより深い階層に別の.htaccessファイルを置くと上書きされることがあります)。

httpd.confファイルにアクセスできるのであれば、アクセス制御をそちらに記述することもできます。

mod_rewriteを使うとPHP側でルーティング(パスを読み取り、表示内容を変更する)ができます。

nginx

nginx.confまたはこれにincludeされた設定ファイル(/etc/nginx/sites-enabled/*など)にアクセス制御設定を記述します。

root /var/www/html;

location /data/ {
  deny all;
}

これは/data/以下(/var/www/html/data以下)へのアクセスを拒否する設定です。

ベーシック認証

HTTPの認証機能にはベーシック認証とダイジェスト認証というものがありますが、ここではよりシンプルなベーシック認証のみを取り扱います(ダイジェスト認証はパスワードを通信路上でハッシュ化するという違い)。

認証情報はファイルとして管理できます。apache2-utilsのプログラムhtpasswdが便利なので、これを使います。Windowsの方はApache HTTP Serverのバイナリに同梱されています。

htpasswd -c .htpasswd USERNAME

このコマンドで新しい.htpasswdファイルが作成され、USERNAMEユーザとそのパスワードを登録できます(パスワードは対話形式で入力)。

Apache(.htaccess)では次のように設定します。

AuthType Basic
AuthName "Authentication Required"
AuthBasicProvider file
AuthUserFile HTPASSWD_PATH
Require valid-user

nginxでは次のように設定します。

auth_basic "Authentication Required";
auth_basic_user_file HTPASSWD_PATH;

このように、DBを書き換えるPHPスクリプトを認証下に置くことで簡易的な書き込み制限を設けることができます。ただし、この方法にはいくつかの問題点があります。

例えば、ベーシック認証はユーザ別認証をサポートしていますが、この方法でのアクセス制御には
サーバ管理者がユーザの追加削除の度にサーバサイドのファイルを半手動で編集する必要があります。
また、SSL化すれば暗号化されるとはいえ、対象のすべてのHTTPリクエストに平文で認証情報が乗ることになります。ほかにも、グループなどの複雑な制御は難しいなどの欠点があります。これらの問題のいくつかは、TLS化した上でCookieなどを利用したアカウント/セッションによる認証をアプリケーション側で用意することで、ある程度解決できます。

また、LAN内で提供されるサービスではしばしばHTTP通信の暗号化は行われないか、望ましくないとされるオレオレ証明書を使って暗号化することになってしまう場合がある(SSHポートフォワーディングなどを利用すれば別ですが)ことにも注意しておきましょう(積極的に安全な形で暗号化したいところなんですが..)。

デプロイにあたって

デプロイ(アプリケーションのリリース)にあたって、いくつか必要になること、やっておいた方がいいことを書いておきます。

まず、サーバの用意が必要です。一般にサービスを公開したい場合は、レンタルサーバやVPSなどを借りるといいと思います。それほど規模が大きくなく、PHPならば安いレンタルサーバで事足りると思いますが、Webアプリケーションを公開できるプラットフォームとしてHerokuなどのサービスもあります。

特にVPSを借りる場合は、ドメインの取得と設定が必要です。条件によってはFreenomなど無料でドメインを取得できるサービスが利用できるほか、お名前.com、Google Domainsや、さくらインターネットなどのレンタルサーバ・VPSを取り扱っている会社がセットで扱っていたりします。だいたいTLDによって値段が異なり、また初年度の値段だけを強調するところもありますが、安いものなら年間1000円前後でしょうか。

現在はHTTP通信の通信路をTLSで暗号化するHTTPSに対応するのが一般的です。レンタルサーバなどでは管理画面での簡単な操作で設定が完結するほか、近年ではLet's Encryptという無料でTLS証明書を発行してくれるサービスがあります(以前は基本的に有料でした)。Let's Encryptは、簡単な対話形式のコマンドcertbotを使って数回の入力で証明書を自動発行・Webサーバの設定を自動で書き換えてくれるほか、証明書の自動更新も管理してくれます。

アプリケーションのバージョン管理のため、Gitなどのバージョン管理システムとGitHubやGitLabなどのリモートリポジトリサービスを使うことをおすすめします。変更により動かなくなった部分の復元や過去のソースコードの再利用、活動時間の可視化などに利用できます。注意として、秘匿情報をパブリックなリポジトリに載せないようリポジトリの公開範囲や.gitignoreを正しく設定しましょう。

アクセスされてはいけない場所に正しくアクセス制御が効いているか、データの処理方法・保存方法・表示方法に問題はないか、といったセキュリティチェックは重要です。また、本番環境と同じ環境を構築するのにDockerが利用できます。

さらなる改善


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP+SQLiteから始めるWebアプリケーションの作り方(その3)

PHP + SQLite(その2)

さて、ようやく本題です。PHPとSQLiteを組み合わせて、簡単なWebアプリケーション(ブログ)を作ってみましょう。

記事を投稿する

まず、POSTリクエストを送信するフォームを含むHTMLを作成します。あとでPHPスクリプトを書き足すので、ファイル名はedit.phpのように拡張子をphpにしておいてください。

<!DOCTYPE html>
<meta charset="utf-8">

<form method="POST">
  <p>
    <input name="title" placeholder="Title" size="50" required>
  <p>
    <textarea name="body" placeholder="Body" cols="50" rows="20" required></textarea>
  <p>
    <button type="submit">Post</button>
</form>

PHPを先頭に書き足します。メッセージを表示するp要素も追加しました。

<?php
  $method = $_SERVER['REQUEST_METHOD'];
  $msg = "";
  if ($method === 'POST') {
    $title = $_POST['title'];
    $body = $_POST['body'];

    $db = new SQLite3('db.sqlite3');
    $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');

    $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
    $stmt->bindValue(':title', $title, SQLITE3_TEXT);
    $stmt->bindValue(':body', $body, SQLITE3_TEXT);
    $stmt->execute();

    $msg = "Success";
  }
?>
<!DOCTYPE html>
<meta charset="utf-8">

<p>
  <?= $msg ?>

<form method="POST">
  <p>
    <input name="title" placeholder="Title" size="50" required>
  <p>
    <textarea name="body" placeholder="Body" cols="50" rows="20" required></textarea>
  <p>
    <button type="submit">Post</button>
</form>

これで、非常に機能は少ないですがDBにフォームの内容を追加する投稿ページを作ることができました。簡単に説明を書いておきます。

$_SERVERにはサーバ情報・HTTP通信関連のデータ(一部)が入っています。今回はリクエストメソッドを取り出し、このページにGETリクエストが送られたときはフォームのHTMLを送信する動作だけをして、POSTリクエストが送られたときは送信されたデータをDBに書き込む処理をするように分岐するために使っています。

$_POSTにはPOSTリクエストにより送られたapplication/x-www-form-urlencodedまたはmultipart/form-data形式のデータが文字列キーと文字列値からなる連想配列(キーと値からなるマップオブジェクト)として格納されています。ただし、データの名前(name属性)がkey[]のような形式の場合、値には配列として複数値が格納されます(この場合でも取り出すときは$_POST['key'])。また、ファイルの場合は$_FILESに格納されます。

記事一覧

<!DOCTYPE html>
<meta charset="utf-8">

<h2>タイトル1</h2>
<div>
  <p>本文1
</div>

<h2>タイトル2</h2>
<div>
  <p>本文2
</div>

...

このようなHTMLからなる閲覧ページをPHPで作ってみましょう。

<?php
  $db = new SQLite3('db.sqlite3');
  $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');

  $result = $db->query('SELECT * FROM entries');
?>
<!DOCTYPE html>
<meta charset="utf-8">

<?php while ($row = $result->fetchArray()): ?>
<h2><?= htmlspecialchars($row['title']) ?></h2>
<div>
  <p><?= nl2br(htmlspecialchars($row['body'])) ?>
</div>

<?php endwhile; ?>

DBからの結果を取得し、すべての結果についてループするのにwhile-endwhileを使っています。同様の記法はPythonのWebフレームワークであるDjangoやFlask(Jinja2)のテンプレートでも使います。ループの条件式がやや読みにくいですが、$result->fetchArray()は次の結果(行)がないときFALSEを返し、while$rowに格納されたFALSEを評価してループを終了します。ただし、このままでは大量の記事がある際にインデックスページ(一覧ページ)が長く/重くなってしまうため、大抵の場合ページネーションをつけたり、タイトルだけ、またはタイトルと本文の一部だけで表示したりしますが、ここでは省略します。

htmlspecialcharsはHTMLの特殊文字をエスケープする組み込み関数です。限られた人しか編集できないブログのような、編集者が信頼できる場合(それからセッションCookieの切り分けなどを行っている場合)は必要ないかもしれませんが、記事のtitlebodyにスクリプトや外部への呼び出しを行うようなHTMLタグを埋め込まれる、XSS脆弱性を塞ぐためにこうしています。似たような組み込み関数にstrip_tagsがあり、こちらはHTMLタグ/PHPスクリプトを文字列から除去します(許可するタグを指定することもできる)。

nl2brは文字列中の改行を<br>タグに変換する組み込み関数です(改行文字はそのまま保持されます)。

pタグは段落なので、2個以上の改行はpタグでくくる、のような処理をしたいところですが、ここでは省略します(Markdownライブラリなどを使うといいのでは)。

また、複数の箇所にDBへのアクセスやテーブルの初期化を書いているのはナンセンスな気がしますので、あとで修正します。

ファイル分離

database.php

<?php
  $db = new SQLite3('db.sqlite3');
  $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');

投稿ページ(edit.php

<?php
  require_once __DIR__ . '/database.php';

  $method = $_SERVER['REQUEST_METHOD'];
  $msg = "";
  if ($method === 'POST') {
    $title = $_POST['title'];
    $body = $_POST['body'];

    $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
    $stmt->bindValue(':title', $title, SQLITE3_TEXT);
    $stmt->bindValue(':body', $body, SQLITE3_TEXT);
    $stmt->execute();

    $msg = "Success";
  }
?>
<!DOCTYPE html>
<meta charset="utf-8">

<p>
  <?= $msg ?>

<form method="POST">
  <p>
    <input name="title" placeholder="Title" size="50">
  <p>
    <textarea name="body" placeholder="Body" cols="50" rows="20"></textarea>
  <p>
    <button type="submit">Post</button>
</form>

閲覧ページ(index.php

<?php
  require_once __DIR__ . '/database.php';

  $result = $db->query('SELECT * FROM entries');
?>
<!DOCTYPE html>
<meta charset="utf-8">

<?php while ($row = $result->fetchArray()): ?>
<h2><?= htmlspecialchars($row['title']) ?></h2>
<div>
  <p><?= nl2br(htmlspecialchars($row['body'])) ?>
</div>

<?php endwhile; ?>

PHPには他の言語におけるimport/include文に相当するものとして、requirerequire_onceincludeinclude_onceが用意されています。requireincludeの違いはエラー発生時にインクルード元の処理を続行するかどうかで、requireは処理を停止するエラーを発行し、includeは処理を続行します(エラーの発生は戻り値で判別可)。onceは同じスクリプトを二重にインクルードしません。

テンプレート

インクルード元のスコープから参照できる変数/関数/クラスにはインクルードされたスクリプトからもそのまま参照できます。また、出力を含むPHPスクリプトをインクルードしたとき(HTML部分やprintを含むような)、出力はそのまま反映されます。つまり、ループの中などにinclude/requireを含むPHPスクリプトを作成すればテンプレートになります。閲覧ページにおける記事のインデックスをテンプレート化してみましょう。

記事インデックステンプレート(templates/entry_index.php

<h2><?= $entry['title_safe'] ?></h2>
<div>
  <p><?= nl2br($entry['body_safe']) ?>
</div>

閲覧ページ(index.php

<?php
  require_once __DIR__ . '/database.php';

  $result = $db->query('SELECT * FROM entries');
?>
<!DOCTYPE html>
<meta charset="utf-8">

<?php
  while ($entry = $result->fetchArray()) {
    $entry['title_safe'] = htmlspecialchars($entry['title']);
    $entry['body_safe'] = htmlspecialchars($entry['body']);

    include __DIR__ . '/templates/entry_index.php';
  }

インクルードしたファイルの出力を文字列として受け取りたい場合、ob_startob_get_cleanような組み込み関数が利用できます。

そのほか細かな挙動については公式ドキュメントを参照してください。

タグ機能

これを入れると少し長くなってしまうかと思いましたが、RDBのRelationを体感できる題材な気がするのでタグ機能を追加してみます。

タグというのは、短い文字列で関連する記事と記事を結ぶことでアクセスをよくするものです。PHPとかSQLite3のようなキーワードを与えておくことで、簡単に検索性をよくできます。あるWebサイトの記事(すべて)のタグを並べて、タグの付けられた記事リストにアクセスできるようにしたタグリストをタグクラウドといったりします。

まず、データベース構造を更新します。タグのIDと名前を格納するテーブルtagsと、記事とタグを関連付けるためのテーブルentry2tagを追加しました(今回外部キー制約は使っていません)。

<?php
  $db = new SQLite3('db.sqlite3');
  $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');
  $db->exec('CREATE TABLE IF NOT EXISTS tags(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
  $db->exec('CREATE TABLE IF NOT EXISTS entry2tag(entry_id INTEGER, tag_id INTEGER)');

ある記事がN個のタグを持つとき、テーブルentry2tagはフィールドentry_idがその記事のidになっているレコードをN個格納し、このレコードがそれぞれ異なるtag_idを持つようにします。このようにすることでSELECT tag_id FROM entry2tag WHERE entry_id=123のようなSQL文により、ID123の記事に付けられたタグのリストを取り出せます。タグから記事リストを取り出すことも同様にできます。ただし、これをそのまま実装すると個々のタグ/記事の詳細を取り出す際に追加でSQL文を発行する必要が出てきてしまいますが、この部分を効率的にするSQL文内でのテーブルの結合/JOINについては省略します。また、タグの順番についても今回は省略します(順番を表すカラムを増やすなど)。

次に、,(カンマ)区切りでタグを入力するフィールドを投稿フォームに追加してみましょう(タグに,が使えなくなってしまいますが)。

<!DOCTYPE html>
<meta charset="utf-8">

<p>
  <?= $msg ?>

<form method="POST">
  <p>
    <input name="title" placeholder="Title" size="50">
  <p>
    <input name="tags" placeholder="Tags (ex. A, B, C)" size="50">
  <p>
    <textarea name="body" placeholder="Body" cols="50" rows="20"></textarea>
  <p>
    <button type="submit">Post</button>
</form>

ちょっと長くなっていますが、PHPスクリプト部分です。なお、日本語のタグ名に対応させるためマルチバイト用の関数mb_splitを使っています。拡張機能mbstringを有効化しておいてください。

<?php
  require_once __DIR__ . '/database.php';

  $method = $_SERVER['REQUEST_METHOD'];
  $msg = "";
  if ($method === 'POST') {
    $title = $_POST['title'];
    $body = $_POST['body'];
    $tags_comma = $_POST['tags'];

    $tag_names = mb_split(',', $tags_comma);
    $tag_names = array_map(trim, $tag_names);

    $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
    $stmt->bindValue(':title', $title, SQLITE3_TEXT);
    $stmt->bindValue(':body', $body, SQLITE3_TEXT);
    $stmt->execute();

    $rowid = $db->lastInsertRowid();
    $stmt = $db->prepare('SELECT id FROM entries WHERE ROWID=:rowid');
    $stmt->bindValue(':rowid', $rowid, SQLITE3_INTEGER);
    $result = $stmt->execute();

    $entry_id = $result->fetchArray()['id'];

    $stmt_select = $db->prepare('SELECT id FROM tags WHERE name=:name');
    $stmt_insert = $db->prepare('INSERT INTO tags VALUES(NULL, :name)');
    $stmt_select_rowid = $db->prepare('SELECT id FROM tags WHERE ROWID=:rowid');

    $tag_mapping = array();
    foreach ($tag_names as $tag_name) {
      if (count($tag_name) == 0) continue;

      $stmt_select->bindValue(':name', $tag_name);
      $result = $stmt_select->execute();

      if ($tag = $result->fetchArray()) {
        $tag_id = $tag['id'];
      }
      else {
        $stmt_insert->bindValue(':name', $tag_name);
        $stmt_insert->execute();

        $rowid = $db->lastInsertRowid();
        $stmt_select_rowid->bindValue(':rowid', $rowid, SQLITE3_INTEGER);
        $result = $stmt_select_rowid->execute();

        $tag_id = $result->fetchArray()['id'];
      }

      $tag_mapping[$tag_name] = $tag_id;
    }

    $stmt = $db->prepare('INSERT INTO entry2tag VALUES(:entry_id, :tag_id)');
    foreach ($tag_mapping as $tag_name => $tag_id) {
      $stmt->bindValue(':entry_id', $entry_id);
      $stmt->bindValue(':tag_id', $tag_id);
      $stmt->execute();
    }

    $msg = "Success";
  }

mb_split関数はマルチバイト文字列を区切り文字で配列に分割する関数です。array_map関数は第1引数に渡された関数に配列の各要素を1つずつ引数として渡し、その戻り値の配列を返す関数です。trim関数は文字列の(この場合分割された各タグ名の)前後の空白文字を除去する関数です(カンマの前後にスペースがある場合に除去する)。

$db->lastInsertRowid()について触れます。DBのレコードにはフィールドidとは別にROWIDという隠れた値が割り当てられており、この関数は同DBコネクション(new SQLite)内で最後に追加したレコードのROWIDを返します。ROWIDWHERE句の条件にも使えますので、これを利用して最後に追加した記事/タグのidを取得しています。なお、ドキュメントを見ると実はAUTO INCREMENT制約のついたフィールド、つまりidを追加することでROWIDidと一致するような気がしますが、念のためROWIDからidを取得するようにしています。

array()は(連想)配列を作成する組み込み関数です。なお、PHPではリスト(添字配列)とマップ(連想配列)を型として区別しません。そのほか、配列について詳しくは公式ドキュメントを見てください。countは配列や文字列の長さを返す関数です。

foreach文は珍しい書き方をしていますが、配列の各要素についてループするものです。PHPでは$array as $value$array as $key => $valueのような書き方をします。

これ以外では、stmtオブジェクトを使いまわしている以外はこれまでの応用のはずです。

(タグを記事一覧に表示したり、タグを元に記事一覧を表示する部分を書くのを忘れていたので、また時間のあるときに追加します)

トランザクション

前節の長いPHPスクリプトを見てみます。大まかな流れとしては、次のようになっています。

  1. 記事を作成する
  2. タグを作成または取得する
  3. 記事とタグを関連付ける

ところでもし、2番や3番の処理をしている途中で、記述のミスやDBとの切断など何らかの原因でエラーが発生したらどうなるでしょうか? このとき、記事の作成だけがDBに反映され、タグは保存されないか、関連付けられない(または一部だけが関連付けられた)状態になってしまいます。また、現在は操作ごとに毎回DBへの書き込みを行っているため、パフォーマンスの面でもよくありません。

このような事態を防ぐ機能として、トランザクションというものがあります。一部のDBライブラリではこの機能がデフォルトで有効になっていて、明示的にDBへの変更を反映する操作をしなければならない場合があります(Pythonのsqlite3モジュールなど)。

それでは、投稿処理をするスクリプトにトランザクションを追加してみましょう。

<?php
  require_once __DIR__ . '/database.php';

  $method = $_SERVER['REQUEST_METHOD'];
  $msg = "";
  if ($method === 'POST') {
    $title = $_POST['title'];
    $body = $_POST['body'];
    $tags_comma = $_POST['tags'];

    $tag_names = mb_split(',', $tags_comma);
    $tag_names = array_map(trim, $tag_names);

    $db->exec('BEGIN');

    $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
    $stmt->bindValue(':title', $title, SQLITE3_TEXT);
    $stmt->bindValue(':body', $body, SQLITE3_TEXT);
    $stmt->execute();

    throw new Exception();

    $rowid = $db->lastInsertRowid();
    $stmt = $db->prepare('SELECT id FROM entries WHERE ROWID=:rowid');
    $stmt->bindValue(':rowid', $rowid, SQLITE3_INTEGER);
    $result = $stmt->execute();

    $entry_id = $result->fetchArray()['id'];

    $stmt_select = $db->prepare('SELECT id FROM tags WHERE name=:name');
    $stmt_insert = $db->prepare('INSERT INTO tags VALUES(NULL, :name)');
    $stmt_select_rowid = $db->prepare('SELECT id FROM tags WHERE ROWID=:rowid');

    $tag_mapping = array();
    foreach ($tag_names as $tag_name) {
      if (count($tag_name) == 0) continue;

      $stmt_select->bindValue(':name', $tag_name);
      $result = $stmt_select->execute();

      if ($tag = $result->fetchArray()) {
        $tag_id = $tag['id'];
      }
      else {
        $stmt_insert->bindValue(':name', $tag_name);
        $stmt_insert->execute();

        $rowid = $db->lastInsertRowid();
        $stmt_select_rowid->bindValue(':rowid', $rowid, SQLITE3_INTEGER);
        $result = $stmt_select_rowid->execute();

        $tag_id = $result->fetchArray()['id'];
      }

      $tag_mapping[$tag_name] = $tag_id;
    }

    $stmt = $db->prepare('INSERT INTO entry2tag VALUES(:entry_id, :tag_id)');
    foreach ($tag_mapping as $tag_name => $tag_id) {
      $stmt->bindValue(':entry_id', $entry_id);
      $stmt->bindValue(':tag_id', $tag_id);
      $stmt->execute();
    }

    $db->exec('COMMIT');

    $msg = "Success";
  }

次の3行を追加しただけです。

<?php
  $db->exec('BEGIN');

  throw new Exception();

  $db->exec('COMMIT');

残念ながらPHPのSQLite3クラスにはtransaction系の機能が実装されていないようなので、直接SQL文でトランザクションしています。ただし、PDOというDBシステム間の違いを吸収する機能には実装されているようです(SQLite3クラスとほとんど同じように使うことができます)。

BEGINBEGIN TRANSACTION)はトランザクションの開始、COMMITで変更の反映、ROLLBACKでcommitされていない変更の差し戻しができます。COMMITが呼ばれない場合は変更は反映されません。そのほか、トランザクションを入れ子にするための機能があったりします。

セキュリティ(アクセス制御)

ところで、ここまで作ってきたDBですが、実は直接ファイルとしてアクセスできてしまいます。ブラウザでhttp://localhost:8000/db.sqlite3のように直接URLを打ち込んで開いてみましょう。

まだ機能としては追加していませんが、将来的に下書き機能や認証機能を付けたとき、そのデータベースが誰でも自由に閲覧できてしまうというのは問題があります。この記事ではWebサーバのアクセス制御機能について後ほど取り扱います。

また、投稿スクリプトであるedit.phpは誰でも実行する(開く)ことができるため、掲示板のように誰でも投稿することができる状態です。これも今回の目的のブログとしてはふさわしくないでしょう。このようなアクセス制御はWebサーバ(HTTP)の認証機能を利用したり、アカウントとセッションを使った認証機能を追加することで行います。この記事ではWebサーバ(HTTP)の認証機能について後ほど取り扱います。

さらなる機能の追加

ブログとしては、投稿日を確認できたり、記事をあとから編集できたほうが便利でしょう。この記事では取り扱いませんが、これらの機能もここまでの内容を応用して、いくらかの追加の調査をすることである程度実装できるはずです。現在時刻(UTC)の取得にはtime関数(数値)、クエリパラメータの取得には$_GET(記事IDを渡す)が利用できます。また、フォームの初期値はinputタグではvalue属性、textareaタグではタグ内のテキストとして与えます(エスケープを忘れないようにしてください)。表示しないフォームフィールドは<input type="hidden">のように追加できます。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP+SQLiteから始めるWebアプリケーションの作り方(その2)

PHP

PHPは、HTML文書に埋め込み可能なサーバ側(サーバサイド)で実行されるスクリプト言語です。(有料の)レンタルサーバではPHPとCGIが実行できる環境が多いです(最安で年額1000〜2000円前後か、VPSなどに比べて共有サーバだとかなり安い)。CGIというのは、対象のファイルにブラウザから呼び出しがあったとき、そのファイル(プログラム)を都度実行し、出力を応答する仕組みです。印象ですが、CGIのプログラムファイルには(Shebangが1行目に記述された)スクリプトファイルを使うことが多く、CGIの言語にはRubyやPerlが主に使われ(Pythonも使えますが)、一昔前の掲示板やチャット、一部のゲーム(箱庭諸島やブラジャータウンなど)はこのような形式のCGIで動いていました(cgi-binというディレクトリや拡張子cgiが特徴的か)。今だったらDjangoとかWebSocketとかFirebaseとか使うんでしょうか。

PHPには、WebサーバのSAPI経由で動作する方式(主にApache HTTP Serverで動かす場合、モジュールモード)と、CGIとして動作する方式(CGIモード)があります。PHPとApache HTTP Serverはしばしばセットで扱われる気がしますが、nginxでも動きます(Apache HTTP Server、nginxというのはTCP80番、443番への通信=HTTPリクエストを受け付ける代表的なWebサーバです)。今の流行りはnginxであるようです(ドメインベースでもNginxが1位に - 3月Webサーバシェア | マイナビニュース, 2020年3月)。CGIモードでPHPを動かす際には、FastCGIという常駐することでCGIの性能を改善する手法を実装したphp-fpmというプログラムがセットで使われます。

PHPには開発用サーバが組み込まれていて、簡単なコマンドで開発用ディレクトリをDocument root(Webサーバが公開する一番上のディレクトリ)とした、PHPの動作する簡易Webサーバを起動することができます。php -S localhost:8000のようなコマンドです。localhostの部分(ホスト)をPCに割り当てられたIPアドレスや0.0.0.0にするとTCPソケットを外部に公開できますが、あくまで簡易サーバなので、実装されていないHTTPの機能や処理性能の低さ、脆弱性リスクがあることを踏まえるとやめておいた方がいいです。本番環境に投入するときは素直に実績のあるWebサーバを使いましょう。ある程度PHPの文法に慣れてきて、本番環境と同じ環境で開発したくなったときは、Dockerを使うといいのではないかと思います(サードパーティのライブラリを使うときはcomposerも使うとか)。

Webサーバのことはひとまずおいておいて、PHPスクリプトを実行するプログラム、すなわちPHPを導入してみましょう。Windows用のバイナリはこちらから、他の方はパッケージマネージャ等からインストールしてください(最新版ではないかもですが)。必要な場合、binディレクトリにパスを通すなどしておいてください。(特にWindowsの方、環境変数PATHの設定)。

<!DOCTYPE html>
<meta charset="utf-8">
<title>山田太郎のウェブサイト</title>

<h2>Welcome to My Website!</h2>
<p>こんにちは! わたしは山田太郎です

さて、HTMLの節のサンプルをそのまま持ってきました。PHPはHTMLに埋め込み可能なので、これ自体もPHPスクリプトといえなくもないかもしれませんが、ともあれPHPスクリプトを書き足してみます。

<?php
  $name = '池田花子';
?>
<!DOCTYPE html>
<meta charset="utf-8">
<title><?= $name ?>のウェブサイト</title>

<h2>Welcome to My Website!</h2>
<p>こんにちは! わたしは<?= $name ?>です。

WebサーバのDirectoryIndexの設定次第なのですが、index.htmlに相当する(URLにパスを指定しなかった場合に開く)ファイルは通常index.phpとなります。開発用サーバは自動でindex.phpを見に行くので、とりあえずこの名前で保存してください。開発用サーバを起動し、ブラウザで当該URL(http://localhost:8000)を開くとPHPで処理された結果が表示されるはずです。ページのソース(view-source)を見るなどして、サーバ側のスクリプトがブラウザに送信されておらず、サーバ側で処理が行われているらしいことを確認してみてください。なお、PHPの文字列結合は.演算子です。

SQLite / SQL

SQLiteは、軽量な単一ファイルデータベースシステムです。データベースの種類としては、MySQLやPostgresSQLと同じリレーショナルデータベースです。現在はSQLite3というメジャーバージョンであるようです。

印象ですが、SQLiteは単一のファイルとしてDBを扱うことができ、分かりにくいTCPを通じたアクセスやDBシステム上のユーザ管理が不要、シンプルにDBの構造を定義できるという点で、SQLで操作可能で、ある程度実用的な、もっとも手軽なDBシステムであると思っています。DB Browser for SQLiteのようなマルチプラットフォームのGUIツール(表計算ソフトみたいにDBの中身と構造をいじれる)もあります(他のDBシステムにもあります)。

ただ気をつけてほしいのは、SQLiteはいわゆる同時編集に弱かったり、速度の面で他のMySQLやPostgresSQLといったシステムに劣っていたり(印象)、システム自体にユーザや権限といった機能がないという点です。一部のローカルDBとしてSQLiteが使われているのは見ますが、サーバサイドで動作し、複数のユーザが利用する(特に書き込む)ならば、そのプロジェクトでは別のDBシステムに乗り換えてから運用することを考えておいたほうがいいのではないかと思います。またDockerを利用すると他のDBシステムも比較的手軽に扱える、という印象も持っています。

さて、SQLiteのCLIを触ってみます。Windowsの方は上の公式サイトからバイナリを、他の方はパッケージマネージャ等からインストールしてください(最新版ではないかもですが)。sqlite3コマンドになるかと思います。sqlite3 db.sqlite3のようなコマンドを適当なディレクトリで実行してみてください(引数はファイル名です)。

CREATE TABLE entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT);

SQLite3のCLIに入ったら、上のコマンドを実行してみてください。これでentriesという名前の、3つのフィールドを持ったテーブルが生成されます。フィールドは表計算ソフトでいう列のことです、カラムともいいます。テーブルは表計算ソフトでいうシートです。

テーブルの一覧は.tablesと打って実行すると確認できます。Ctrl+Dで一度CLIを抜けて、もう一度sqlite3 db.sqlite3を開いてもこのテーブル(もちろんすべてのデータ、トランザクションしてない限り)は維持されています。最初からやり直したいときは、db.sqlite3ファイルを普通にファイルとして削除すればいいです(くれぐれも本番環境でやらかさないように気をつけてください。全データが消滅します。バックアップと入念な確認が必要です。特にワイルドカード/ディレクトリ削除などに巻き込まないように)。

SELECT * FROM entries;

次に上のコマンドを実行してみてください。何も表示されないはずです。

INSERT INTO entries VALUES(NULL, "はじめまして", "こんにちは!");

これを実行したら、もう一度SELECTしてみましょう。今追加したレコードが表示されるはずです。レコードは表計算ソフトでいう行です。

SELECT * FROM entries;

表示が見にくいと感じたら、.headers onを実行してみてください。出力の1行目にカラム名が表示されます。他にも表示を見やすくするコマンドがあるので調べてみてください。

PRIMARY KEY制約やAUTOINCREMENT制約、フィールドの型やSQL文法についての詳しい説明は専門書/公式ドキュメントに譲ります。ただ、何度かCREATEINSERT文を実行したり、数値型(INTEGER)のフィールドを追加したりすればだいたい分かるかと思います。INTEGERは数値型、TEXTは文字列型です。NULLを使っているのはSQLite側に自動でIDを決めさせるためです。JSONや時間などの細かいデータ型はシステムとしてはサポートされていません(時間は組み込み関数で扱うことができるみたいですが、ホスト側=DBを呼び出す側の言語で処理してもいいと思います)。また、型名/型がかなり柔軟です。INTEGERというと整数のイメージですが、浮動小数点数を格納できたり、型名をFLOATとして定義できたりします(別名扱い)。

既存のレコード(の中身)を更新するUPDATE文、REPLACE文について調べて試してみましょう。テーブルの削除はDROP TABLEです。

レコードの検索、例えばSELECT * FROM entries WHERE id=1;SELECT * FROM entries WHERE title LIKE "%hoge%";を試してみましょう(適当なデータを追加して)。

PHP + SQLite(その1)

DBをプログラミング言語から扱うときは、通常それぞれの言語のライブラリを使用します。

プログラム中のオブジェクトとDB中のオブジェクトとの変換を隠蔽したり、異なるDBシステムの違いを吸収したり、マイグレーション(後からのDBの構造変更)を助けたりするORMという種類のライブラリもありますが、入門ですのでここでは使用しません。

それでは、PHPからSQLiteのデータベースを扱ってみましょう。と、その前に少し準備が必要な場合があります。PHPからSQLiteを使用する際には、PHPのSQLite3に関する拡張機能(Extension)を導入する必要があります。Windowsの方は拡張機能自体は標準で含まれていたかと思いますが、php.iniファイルを自分で編集して必要な拡張機能を有効化する必要があったように思います。調べてやってみてください。それ以外の方は、パッケージマネージャ等からインストールできると思います(Ubuntuの場合、php-sqlite3)。日本語のようなマルチバイト文字を扱う組み込み関数mb_*を利用するときなども拡張機能の導入・有効化が必要な場合があります。レンタルサーバの場合はリストが提供されていたり、またphpinfo関数を呼び出すPHPスクリプトをブラウザから開くことで調べられます(環境情報が流出するおそれがあるのでおすすめはしませんが)。準備ができましたら、次に進みましょう。

<?php
  $title = 'はじめまして';
  $body = 'こんにちは!';

  $db = new SQLite3('db.sqlite3');
  $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');

  $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
  $stmt->bindValue(':title', $title, SQLITE3_TEXT);
  $stmt->bindValue(':body', $body, SQLITE3_TEXT);
  $stmt->execute();

上のようなPHPスクリプトを(拡張子をphpにして)保存し、ブラウザから開いてください。DBが生成され、ページを開くごとにレコードが追加されていきます(CLIやSELECT文で確認してみてください)。下にPHPからSELECT文で(とりあえず)中身を確認するスクリプトを示します。

<?php
  header('Content-Type: text/plain; charset=UTF-8');

  $db = new SQLite3('db.sqlite3');

  $result = $db->query('SELECT * FROM entries');
  while ($row = $result->fetchArray()) {
    print_r($row);
  }

header関数はHTTPヘッダを出力する関数で、ここではボディのMIMEタイプを出力しています。

exec関数とquery関数はシンプルにSQL文を実行する関数で、この2つの違いはSQL文を実行した結果を返すかどうかです。

では、prepare関数を使っている部分は何をしているのでしょう? これはPreparedStatementというPHP-SQLite3の機能を使っています。前節のSQL文の例を思い出してみると、VALUESの中に変数の値を直接文字列結合によって入れ込んでしまえばいいのではないか、と思うかもしれませんが、特にユーザからの入力をSQL文に入れ込もうとする場合、それは実はよくない実装です(変数の値が固定なのだからそのまま書けばいい、というのは置いておいてください..)。そのような実装をした場合、入れ込んだ文字列によってSQL文が意図しない挙動をするおそれがあるだけでなく、悪意のあるSQL文が実行されるように変数の値を細工されることで、見せてはならない情報をDBから抜き取られたり、DBを破壊されたり、事前の検証をスルーして本来入ってはいけない値がDBに入ってしまうなどといったSQLインジェクションという攻撃にさらされる危険があります。実際にはPreparedStatementはほぼ同一のSQL文を連続で実行する場合のパフォーマンス最適化を主目的としているようですが、入力値を自動でエスケープしてくれるため、SQL的に信頼できない値を安全に入れ込むことができます。もちろん、XSSなどの脆弱性には対処できないので、きちんと入れる値、出す値の検証/処理をするようにしましょう(PHPにはそのような機能が豊富です)。

<?php
  error_reporting(E_ALL);
  ini_set('display_errors', '1');

プログラミング中にエラーが発生したとき、開発中は上のような記述をプログラムの先頭に追記することですべてのエラー詳細をWebページ上で確認することができます。ただし、本番に投入する前に消し忘れないように注意しましょう(内部のディレクトリ構造やアルゴリズム、またAPIキーのような秘密にしなければならない変数値などが流出する危険があります)。

HTTP

ブラウザとWebサーバの間の通信に使われるプロトコルは基本的にHTTPです(FTPなどをサポートしている場合もありますが)。HTTPにもバージョンがあり、現在はHTTP/3の標準化が議論されている段階であったように思いますが、現在主流なのはHTTP/1.1です。HTTP/1.1では通常、サーバはTCP80番(HTTPSの場合は443番)でブラウザ/クライアントからの通信を待ち受け、基本的に一定のルールに沿ったテキスト形式でデータをやりとりします(ボディがgzipなどで圧縮される場合もありますが、ヘッダは常にテキスト形式です)。

HTTP通信でやりとりされるテキストデータのフォーマットのイメージは次のようなものです。空行をはさんでヘッダとボディと呼ばれる2部分に分かれています。

ブラウザ:適当なポートA→HTTPサーバ:80(HTTPリクエスト/要求)

Request Header
(空行)
Request Body

HTTPサーバ:80→ブラウザ:適当なポートA(HTTPレスポンス/応答)

Response Header
(空行)
Response Body

やりとりされるデータはだいたいこんな感じです。

ブラウザ→サーバ(ボディはなし)

GET / HTTP/1.1
Host: example.com

サーバ→ブラウザ

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 50

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

サーバ側の動作は、イメージ的にはこんな感じです。

request_data = tcp_socket.recv_http() # Requestを受け取る

response_data = process_request(request_data) # Requestを処理する

tcp_socket.send_http(response_data) # Responseを返す

PythonのHTTP通信ライブラリrequestsでローカルサーバにHTTPリクエストを送ったとき、こんな感じのテキストデータが送信されました。

GET / HTTP/1.1
Host: localhost:8000
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

PHPの開発サーバから返ってきた応答はこんな感じでした。

HTTP/1.1 200 OK
Host: localhost:8000
Date: Thu, 02 Apr 2020 12:00:00 GMT
Connection: close
X-Powered-By: PHP/7.2.24-0ubuntu0.18.04.3
Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

プログラム

import requests
requests.get('http://localhost:8000')
<!DOCTYPE html>
<meta charset="utf-8">
Hello world

まず、リクエストを見てみましょう。リクエストの1行目に書かれているGETというのは、リクエストメソッド(Request Method)の1種です。GETの他にも、POSTやHEAD、PUTなどがあり、WebサーバはGETとHEADのサポートが必須(RFC 7231 #4. Request Methods)とされています(HEADはヘッダのみを返すもの)。GETHTTP/1.1の間に挟まっている/はパス(Request Target、Reqeust URI)です(RFC7230 #5.3. Request Target)。

GET / HTTP/1.1
Host: example.com

通常、Webページにアクセスするときに意識するのはGETとPOSTの2種類かと思います。Webページをふつうに開くときに使われるのがGET、アンケートやログインといったフォームを送信するときによく使われるのがPOSTといった感じです。WebのAPI作法の一種であるREST APIなどでは、PUT(追加、アップロード)やDELETE(削除)なども利用してCRUDを実現しますが、HTMLのフォームでは使用できず、通常のWebサーバは(動作する機能として)実装していないと思います(POSTにPUTやDELETEに相当する機能を持たせる)。

上にフォームを送信するときにPOSTがよく使われると書きましたが、GETでもフォームを送信することがあります。例えば、Google検索のフォームはGETリクエストを送信します。フォームというのはHTTPリクエストにデータを持たせるための部品ですが、GETリクエストではHTTPヘッダの1行目に含まれるRequest Targetの一部としてPATH/?key=value&hoge=fugaのように送信されます。ブラウザでは、アドレスバーにURLの一部として同様に表示されます。GETリクエストにこのような形で含まれるデータをクエリパラメータといいます。

では、POSTの場合はどこにフォームのデータが含まれるのでしょうか? それはリクエストボディです。ボディにデータを含むPOSTリクエストは、ヘッダにボディのmimetypeを表すContent-Typeフィールドを伴って送信されます。通常、HTMLフォームを使って送信されるデータのmimetypeはapplication/x-www-form-urlencodedまたはmultipart/form-dataです。前者はテキストデータのみを送信するようなフォームに用いられ、後者はファイルのアップロードなどの際に用いられます。(REST APIのような)HTMLフォームを前提としないAPIサーバなどでは、application/jsonなどをContent-Typeとして要求することがあります。

次に、レスポンスを見てみましょう。レスポンスの1行目に200 OKという文字列があります。これはHTTPステータスコードといい、HTTPリクエストがサーバにおいてどのように処理されたかを示す記号です。よく見かけるものは、404 Not Found403 Forbidden503 Service Unavailableのようなものでしょうか。リダイレクトを表すステータスコードもあります。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 50

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

ブラウザ上でHTTP通信を使ってやりとりされているデータはブラウザの開発者ツールから確認することができます(ヘッダやボディも直接確認できます)。普段使っているサイトで見てみましょう。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP+SQLiteから始めるWebアプリケーションの作り方(その1)

はじめに

この記事は、PHPとSQLiteを使って非常にシンプルなWebアプリケーション(ブログ)を作り、またそこから他のDBMSへ移行したり、他の言語等でWebアプリケーションを開発する基礎を押さえることを目的にしたものです。ある程度のプログラミング経験と自分で調べながら読み進めることを要求するかもしれません。各項目は割と手抜きだったり、文字が多かったりします。

また、Webの世界は日進月歩ですので、あまり新しいバージョンだと互換性が失われていることもあるかもしれません。しかし、同時に脆弱性の発見と修正も日々行われています。最新の安定版、というようなリリース元がおすすめしているバージョンを使うといいのではないかな、と思います(リリース元の発信する情報には注意するようにしてください。あなたのアプリケーションとそのユーザが影響/被害を受ける致命的な脆弱性が見つかることがあります)。

それから、もし(バージョンの関係や自分の追加したコードで)動かないところがあったら、自分で調べて直してみるのも大切なことかなと思います(誤植はすいません..)。また、英語で調べてみないと情報が得られないこともよくあります。苦手意識を持たずに、英語の勉強も兼ねて試してみましょう。

素人が書いているので、間違いがあったら訂正していただけると助かります。

HTML

PHPから始めると書きましたが、これはタイトル詐欺です。Webアプリケーションを作るからには、HTMLを扱えたほうがいいでしょう(ただし、「Webアプリケーション」というのがWebブラウザ上で動作するものである場合の話ですが)。

この回では静的ファイルとして管理されるようなWebサイトを作成することを目指します。このような形式のWebサイトの場合、GitHub PagesFirebase Hostingなど無料でホスティングしてくれるサービスがあります。主にこのようなサービスで配信することを目的としていると思うのですが、Markdownなどのテキストから静的なHTMLなどを自動で生成し、それをサーバにアップロードする形でWebサイトの構築を支援するアプリケーション(Gatsby.js、Jekyllなど)もあります。

それでは話を戻して、HTMLについて説明します。

試しに、https://example.comを開いてみてください。シンプルなWebページが表示されるかと思います(このページはインターネットに関わる資源を管理する国際組織ICANNのサーバが配信しているものです)。現在は、次のような表示が得られます。

example.com

以下にこのページを構成するHTMLを引用します。なお、Firefox 74、Chrome 80ではF12(ファンクションキーです)を押すことで現在開いているページのソースコードや動作を分析するツール(開発者ツール、デベロッパーツール)が開きます。また、アドレスバーをクリックして、URLの前にview-source:を追加する、つまりview-source:https://example.comのようにして開くとブラウザはページのレンダリングを行わず、生のHTMLを表示してくれます。

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

HTMLはこのように、一定のルールのもとに記述されたテキストデータとして表されます。なお、この例は全体的に記法が古いのですが(互換性のためというのもあるかもしれませんが、10年前のブラウザをそのまま使っているような場合でもない限り影響はないでしょう。また、古いソフトウェアを更新せずに使い続けるのは致命的な脆弱性が明らかになっている場合があるので、セキュリティの面でよくないです)、一方でHTMLの構造がわかりやすくなっています。XMLを知っている方は、似ていると思うかもしれません。これは歴史的にXMLとHTMLが共通の祖先(SGML)を持っているためです。また、HTMLとXMLの融合を目指したXHTMLというものも過去にありましたが、現在は主流ではありません(一部の解説サイトなどでは見かけますが、古い記法です)。HTMLの仕様は数年ごとに更新されていますが、この記事ではHTML5を想定することとします。

bodyタグ

さて、まず注目してほしいのは、<body></body>で囲まれた部分です。<body>のような表記をタグといい、<body>は開始タグ、</body>は閉じタグといいます(<tag />のような表記はXHTMLのものであり、古い記法です)。開始タグ<body>と閉じタグ</body>で囲まれた部分をbodyタグで囲まれた部分、またはbody要素と呼びます。見てもらえれば分かると思いますが、ここがページの本体であり、画面に表示される内容を格納する、Webページの肝となる部分です(HTML5の場合、シンプルなWebページを作るときは、この中身と数行のメタ情報だけで用が足ります)。

body要素の中身を見ていきましょう。この例では、divタグ、h1タグ、pタグ、aタグが階層構造を持って格納されています。ある要素(A)が別の要素(B)の中(ただし、直下)に入っているということを入れ子になっている、とかAはBの子である、といいますが、この例でdivタグはbodyタグの子であり、h1タグと2つあるpタグはdivタグの子であり、aタグは2つ目の方のpタグの子になっています。HTMLを書く上では、このような親子関係と順序が大切になることがあります。

次に、bodyタグの子をそれぞれ見ていきましょう。まず、divタグは複数のタグをひとまとめにするために用いられます。ここでは、ページの中央に表示された白い四角に対応していて、このようなデザインを実現するために使われています。中央に寄せたり、色を付けたりするのはCSSの仕事ですから、ここでは詳しいことはおいておきます(この記事ではあまりCSSの詳しい解説はしないので、各自調べてみてください)。

h1タグは見出し/headingを表すタグです。後ろの番号はh1からh6まで定義されていて、見出しの深さを表すのに使われます(数字が小さいほど浅い見出し)。Webページ(文章)のタイトルにはh1、文章の見出しにはh2、h3、h4を使う感じでしょうか。

pタグは段落/paragraphを表すタグです。文章の1段落に対応します。このように、HTMLのタグはそれぞれの種類で異なる意味を持っています。普通のブラウザが(CSSが適用されていないデフォルトの状態で)文字を太字として表示するタグには、bタグ(bold)やstrongタグなどがありますが、HTML5ではstrongタグは強調を、bタグは強調の意図を含まない太字として扱われます。

aタグはリンクを表すタグです。HTMLのHTはHypertextですが、Hypertextというのは文書間にリンクを持つ文書のことです。Webページが相互にリンクし合うことで、今のWebは成り立っています。基本的に、hrefという属性(属性:タグの持つ付加情報のこと)にURLを記述することで他の文書(Webページ)へのリンクを張ることができます。ブラウザはその情報を解釈し、ユーザがa要素をクリックしたときに指定したページを表示する、という挙動をします。bodyタグについては以上です。文章を構成する細かいタグについて知りたい/知る必要がある場合は、自分で調べてみてください。

headタグ

次に、headタグを見ていきましょう。この例では、titleタグ、metaタグ、styleタグの3種類のタグが格納されています。

titleタグは、文書のタイトルを表すタグです。ブラウザのタイトルバーの表示やブックマークに保存するときの(デフォルトの)名前には、ここに記述された内容が使われます。

metaタグは、文書のメタ情報に関する汎用的なタグです(<meta />のような記述になっていますが、これは古い記法です。HTML5ではmetaタグは閉じタグを省略でき、/は必要ありません)。ここでは、文字コードの指定とviewportというデザインに関する指定が記述されています(2つ目の文字コードの指定http-equiv="Content-Type"はHTML5では必要ありません。charsetだけでOKです。なお、Content-Typeとはmimetypeを意味していて、拡張子に依存しない形でデータの種類を表すのによく用いられます)。

styleタグは、文書のスタイル情報、通常はCSSを格納するタグです(HTML5では埋め込まれたスタイル情報がCSSで記述されている場合type属性を省略できます)。外部にファイル(CSSファイル、*.css)として記述されたスタイル情報は<link rel="stylesheet" href="URL">というタグを埋め込むことで指定できます。

HTML5

headタグとbodyタグ、これらを囲むhtmlタグ、それからHTML文書であることを示す頭の1行がこのHTML文書のすべてです。例の解説については以上にします。ほかに気になるところがあれば自分で調べてみてください。

例では丁寧にタグの構造が記述されていますが、HTML5では大胆な省略が仕様として可能です。(作成者のミスで)構造の壊れたHTMLをブラウザが自動で復元してくれることがありますが、それとは異なりブラウザが仕様に沿って自動で補完してくれます(可読性やメンテナンスの点で有用なので、積極的な省略を推奨します)。以下にHTML5で書かれたシンプルなWebページの例を示します(HTMLにおいて、だいたいの場合大文字小文字に関しては好みの問題です)。これを好きなテキストエディタ(BOMなしUTF-8が保存できるものにしてください。Shift-JISは時代遅れです)で保存して、あなたのブラウザにD&Dしてみてください。シンプルなWebページが開かれるはずです。

<!DOCTYPE html>
<meta charset="utf-8">
<title>山田太郎のウェブサイト</title>

<h2>Welcome to My Website!</h2>
<p>
  こんにちは! わたしは山田太郎です。

HTMLを書くとき発生する面倒な問題の1つがエスケープです。文書の構造を指定するために様々な文字が特別な意味を持っている(例えば、不等号>, <)ため、その意味をキャンセル(エスケープ)する必要がある場合があります。これには、実体参照/entity referenceという記法が使われます。例えば、大なり(greater than, >)は&gt;、小なり(less than, <)は&lt;と記述する必要があります(無視してもブラウザが推測してくれる場合もありますが、ページの表示に不具合が出る場合があるのでよくありません。パンくずリストや数式を書くときなどは気をつけましょう)。

CSS

CSSは、HTML文書のデザインを記述するのに使われる記法です。CSS文書はスタイルシートともいいます。

以下のCSS文書は、body要素の背景色をピンク(#FF00FF)にするという指定です。#FF00FFというのはカラーコードといって、16進数各1バイトで赤、緑、青の光の3原色に基づいて色を指定しています(並び順はRRGGBB、Rは赤、Gは緑、Bは青)。カラーコードを一部省略したり色を名前で指定したりもできますが、ここでは扱いません。#000000は黒(black)、#FFFFFFは白(white)です。

body {
  background-color: #FF00FF;
}

上の例でいうと、bodyの部分をCSSセレクタ、background-colorの部分をプロパティといいます。CSSセレクタはHTML文書の中のどの要素にスタイルを適用するかを表し、プロパティは適用するスタイルを表します。ここではタグ名にもとづく指定しかしていませんが、id属性やclass属性、文書内でのタグの位置など様々な条件で指定することができますし、プロパティも色だけでなくサイズ、配置、余白、フォントの指定やアニメーションを実現するものなど、様々なものが利用できます。詳しくは調べてみてください(widthfont-sizedisplay: flexdisplay: gridptemなど)。

body {
}
div {
}

もちろん、こんな風に複数のブロックを1つのCSS文書に含めることができます(優先度に注意)。ただし、body{ div{ } }のような入れ子にした記述はできません(CSSをブラウザに配信する前に処理して展開する記法で用いられることはあります)。

また、example.comの例に含まれる、@を使った記述をクエリといい、@mediaはメディアクエリといいます。これは指定した条件にマッチする環境でのみそのスタイルを適用する、という記述で、画面の幅などに基づいてスマートフォン用の表示とPC用の表示を切り替えたりするのに使われています(印刷用というのもあります)。

JavaScript

JavaScriptは、ブラウザ側(クライアントサイド)でWebページをプログラムで制御する際に用いられるスクリプト言語です。なお、この記事の目標のPHPもまたスクリプト言語という分類に含まれます(ただし、PHPはサーバ側=サーバサイドで実行され、ユーザのブラウザに配信される前に実行されるものです)。スクリプト言語というのは、C言語やJavaのようにコンパイル(機械語への変換)や中間表現(バイトコード)への事前の変換を必要とせず、インタプリタというプログラムによってテキストとして読み込まれ(一部の言語ではJITという仕組みもありますが)、実行時に解釈されて処理を行うプログラミング言語です(JavaとJavaScriptは名前が似ていますが全くの別物です。名前が似ているけど別物、名前が同じだけど別物、名前が違うけど同じもの、ということはよくあり、しばしば名前だけでは判断できません)。JavaScriptの言語仕様はECMAScriptという名前で定義されています。このあたりは詳しくないのですが、Wikipediaを覗いたところ、ブラウザに実装された、現在表示されているHTMLを操作するDOMの機能やブラウザを操作するAPIをECMAScriptに追加した方言がJavaScriptであるようです(言語仕様自体はECMAScriptに準拠し、実装・アップデートは各ブラウザベンダー/スクリプトエンジン開発者が行っている)。

JavaScriptはHTML文書中に埋め込むこともできますし、外部ファイル(JavaScriptファイル、*.js)として読み込むこともできます。どちらもscriptタグを用います。なお、scriptタグは外部ファイルを読み込む場合でも閉じタグが必須なので、忘れないように注意しましょう。

<script>
var text = 'こんばんは';
document.querySelector('#greeting').innerText = text;
</script>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>

1つ目のタグに書かれたスクリプトは、id属性がgreetingの要素に表示されるテキストをこんばんはに書き換えるものです。querySelector関数の引数にはCSSセレクタを指定します(他にもid属性の値のみで指定するgetElementByIdなどの関数も利用できます)。詳しいDOMの操作については調べてみてください。

2つ目のタグは何やら複雑に見えますが、JavaScriptの有名なライブラリjQueryを読み込むもので、公式サイトからコピーしてきただけのものです。integritycrossoriginはSRIというハッシュ値に基づいてコードの同一性を検証する仕組み(ドメインが乗っ取られてファイルが改ざんされた場合など)のための属性です。このURLの指すサーバのように、スクリプトやフォント、スタイルシートを(専門に)提供するWebサーバ・システムをCDNといい、共通のキャッシュを持つことができるため、複数のサイトで利用されることで高速化が期待でき(印象)、またCDN側でCloudflareなどの負荷分散の仕組み/サービスを使っていたりします。GitHubのraw contentなどはこのような利用を想定したものではないので、(配布元の指定する)CDNを使うか、ダウンロードして利用しましょう。また、他人の作ったライブラリを利用する際はライセンス(利用条件、利用許諾、再配布条件)を確認するようにしましょう。

注意する点として、スクリプトは上(におかれたタグ)から順に実行されるという点、特に外部の文書を読み込みにいく必要がある場合、スクリプトの読み込みが終わるまでページが描画されない点(スタイルシートも同様です)があります。まず前者についていうと、例えば上の例では1つ目のタグ(の中のすぐに評価される式)でjQueryの機能を使うことはできません(関数を定義したり、イベントリスナを登録してあとから呼び出す/呼び出される場合は問題ありませんが、読みにくいです)。後者についていうと、サーバが非常に遠かったり、負荷がかかっているなどの理由でレスポンスが遅いとページ全体のレスポンスが落ちたように見えます(あまり時間がかかるとブラウザは読み込みを諦め、スタイル/スクリプトの適用されていない画面が表示されたりします)。後者はasync属性をつけることで非同期の読み込み(読み込みの完了を待たない)ができますが、この場合実行順の問題が発生するため注意してください。

また、JavaScriptも他のプログラミング言語と同じように計算や文字列処理が可能です(得意とは限りませんが)。

var a = 1234;
a += 3456;

console.log(a);

consoleは開発者ツール(のConsole)に出力を表示します。このスクリプトが埋め込まれたページを開いたとき、4690と(コンソールに)表示されるはずです。logだけでなくwarnやdebugを使ってログの表示のされかた(色、フィルタなど)を変えられます。開発者ツール自体にもWebページをデバッグする上で様々な便利機能があるので、いろいろと試してみてください。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【cakePHP】環境構築 (Mac)メモ

はじめに

・homebrewインストール
・phpインストール
・composerのインストール
・mysqlの接続
このような流れで環境構築していきます。

Homebrewインストール

公式https://brew.sh/index_ja.html
通りにターミナルでコマンド入力

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

PHPインストール

*次のComposerのインストールには、PHPが先にインストールされている必要があります。今回は以下のバージョンのPHPがインストールされている環境にComposerをインストールします。

brew install php@7.2

==>
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-php'...
~~中略~~
To have launchd start homebrew/php/php72 now and restart at login:
  brew services start homebrew/php/php72
==> Summary
?  /usr/local/Cellar/php72/7.2.1_12: 350 files, 46.6MB

Composerとは

Composerとは、PHPのパッケージ管理ツールです。Node.jsのnpmみたいな認識。

コマンドを叩くと、必要なパッケージ類を指定されたバージョンでインストールしてくれます。その際依存関係にあるパッケージも合わせてインストールしてくれる優れものです。
スクリーンショット 2020-04-05 20.04.32.png

Composerを使う必要があってインストールから行ったので、その手順をメモ代わりに残しておきます。

composerのインストール

インストール方法は2つ

Homebrew を使って簡単に Composer をインストール

 % brew search composer
==> Formulae
composer 

インストール

$ brew install composer   
Updating Homebrew...
  ・・・省略(Homebrew のアップデートなど)・・・  
==> Downloading https://getcomposer.org/download/1.9.3/composer.phar
######################################################################## 100.0%
?  /usr/local/Cellar/composer/1.9.3: 3 files, 1.8MB, built in 7 seconds

% composer
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.10.1 2020-03-13 20:34:27

Usage:
  command [options] [arguments]
・・・以下省略・・・

-V オプションでバージョンのみを確認する

composer -V
Composer version 1.10.1 2020-03-13 20:34:27

Homebrew を使ってインストールすると Composer は /usr/local/Cellar/にインストールされる。


# brew でインストールしたパッケージのインストール先
$ ls /usr/local/Cellar/  
composer    libidn2     libunistring    webp
gettext     libpng      openssl@1.1 wget
jpeg        libtiff     tree

# tree コマンドで確認
$ tree /usr/local/Cellar/composer/  
/usr/local/Cellar/composer/
└── 1.9.3
    ├── INSTALL_RECEIPT.json
    └── bin
        └── composer

2 directories, 2 files

Composer 公式サイト インストール

getcomposer.org で Composer をインストールするためのコマンドを取得する

curl -sS https://getcomposer.org/installer | php

cakePHP環境でプロジェクトを作成する

php composer.phar create-project --prefer-dist cakephp/app プロジェクト名

 

作成したプロジェクトを立ち上げる

bin/cake server

引数のないデフォルト状態では、 http://localhost:8765/ であなたのアプリケーションに アクセスできます。

もしあなたの環境で localhost や 8765番ポートが使用済みなら、CakePHP のコンソールから 下記のような引数を使って特定のホスト名やポート番号でウェブサーバーを起動することができます。

bin/cake server -H 192.168.13.37 -p 5673

こうすればアプリケーションは http://192.168.13.37:5673/ でアクセスできます。

これだけです! あなたの CakePHP アプリケーションは ウェブサーバーを設定することなく動きます。

サーバーが他のホストから到達できない場合、

bin/cake server -H 0.0.0.0 

を試してください。

アクセスすると、
スクリーンショット 2020-04-05 20.37.16.png

こんな感じの画面が出れば成功です。

データベースの接続

接続できてないとエラーがでます。
スクリーンショット 2020-04-05 20.52.15.png

CakePHP is NOT able to connect to the database.

Connection to database could not be established: SQLSTATE[HY000] [1045] Access denied for user 'ユーザー名'@'localhost' (using password: YES)

データベースの情報を使って、設定ファイルを書き換えます。

app/configファイルに行ってあげて、app_local.phpを開いください。

*注意

cakePHP4以降は

app.default.phpからapp_local.php という名前に変わってるので注意!!

目印はDatasourceを探します

app_local.phpまたはapp.default.php
 'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            /**
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'non_standard_port_number',
            'username' => 'データベースアクセスユーザー名', <---変更
            'password' => 'パスワード',       <---変更
            'database' => 'データベース名',   <---変更
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'flags' => [],
            'cacheMetadata' => true,
            'log' => false,

これで接続できれば成功です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mockery の shouldReceive() と shouldHaveReceived() ってどう違うの?

PHP のテストで DOC1 をテストダブルに置き換えるのに Mockery というライブラリを重宝しています。
そのダミーオブジェクトのメソッド shouldReceive()shouldHaveReceived() の違いが毎度毎度分からなくなるので、整理してみました。

TL;DR

  • shouldReceive() はモックが持つメソッド。 SUT2 の実行前に記述し、期待する間接出力3を定義する。
  • shouldHaveReceived() はスパイが持つメソッド。 SUT の実行後に記述し、間接出力の内容を検証する。

モックとスパイについて

こちらの記事が大変参考になります。
記事下部の分類方法を引用します。

Test Doubleの分類方法は以下のような感じです。

  • テストの範囲内で本物と同じように動作するTest DoubleはFake Object。
  • 内部のパラメータや状態がなんでもあってもテストに影響を及ぼさない代替オブジェクトなら、Dummy Object
  • 上記以外で、テスト対象の間接出力を受け取り、かつ自身でその検証を行うTest DoubleはMock Object
  • 上記以外で、テスト対象の間接出力を受け取りそれをあとから参照可能にするTest DoubleはTest Spy
  • 上記以外で、テスト対象の間接入力を操作できるTest DoubleはTest Stub

モックもスパイも間接出力を受け取る点は同じですが、テストコード上では期待結果や検証内容を記述する場所が SUT の実行前か実行後かという点が特に異なります。

shouldReceive()shouldHaveReceived() の違い

これを踏まえた上で shouldReceive()shouldHaveReceived() のユースケースを見てみます。
公式ドキュメントのサンプルコードを引用します。

$mock = \Mockery::mock('MyClass');
$spy = \Mockery::spy('MyClass');

$mock->shouldReceive('foo')->andReturn(42);

$mockResult = $mock->foo();
$spyResult = $spy->foo();

$spy->shouldHaveReceived()->foo();

var_dump($mockResult); // int(42)
var_dump($spyResult); // null

ここでダミーオブジェクトによって置き換えられるのは MyClass のメソッド foo() です。
$mock->shouldReceive('foo')foo() の実行前に記述され、 foo() の実行を期待される動作として定義します。
また andReturn() で間接入力も操作しており、スタブとしても振る舞うことが可能です。

$spy->shouldHaveReceived()->foo() はメソッド実行後に記述され、 foo() が実行されたかどうか後から検証します。

このように生成されたダミーオブジェクトの振る舞いと前述のモックとスパイの分類が整合することが分かります。

それぞれの使いどころ

\Mockery::mock() は間接入力を操ることができるため使い勝手は良いですが、期待される全てのメソッド呼び出しについて shouldReceive() を定義する必要があります。

そこでスパイです。
再び公式ドキュメントからスパイに関する記述を引用します。

The third type of test doubles Mockery supports are spies. The main difference between spies and mock objects is that with spies we verify the calls made against our test double after the calls were made. We would use a spy when we don’t necessarily care about all of the calls that are going to be made to an object.

A spy will return null for all method calls it receives. It is not possible to tell a spy what will be the return value of a method call. If we do that, then we would deal with a mock object, and not with a spy.

\Mockery::spy() は後からメソッド呼び出しを検証する性質上あらかじめ期待結果を定義する必要がなく、関心のある間接出力についてのみ shouldHaveReceived() で検証する使い方が可能です。
ただし \Mockery::mock() のように間接入力を操作することはできず、メソッドの返り値は全て null になります。

まとめ

テストダブルの概念と Mockery における shouldReceive()shouldHaveReceived() の関係について整理しました。
より詳しい解説が公式ドキュメントに記載されている4ので、読んでみるとまだ色々と発見がありそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP_CodeSnifferでPSR2ベースの自作ルール適用

静的解析ツールPHP_CodeSnifferをサーバに導入したときのメモです。

自作ルールと既存ルールをカスタマイズして使いたいけど、英語はなるべく読みたくない人へ向けて。
(雑です)

1. phpcsインストール

./vendor配下に、静的解析を行うphpcsと、自動変換を行うphpcbfがインストールされます。

composer require --dev "squizlabs/php_codesniffer=*"

2. phpcs.xmlの作成

ドキュメントルート直下にphpcs.xmlを作成します。
フォーマットは下記にあります
./vendor/squizlabs/php_codesniffer/phpcs.xml.dist

ここではデフォルト値を設定しておくとよいです。
ちなみにdistではPEARがデフォルトのルールに設定されています。

phpcs.xml
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="MyProject coding rule" xsi:noNamespaceSchemaLocation="phpcs.xsd">
    <description>The coding standard for PHP_CodeSniffer itself.</description>
    <file>デフォルト検査対象のパス</file>
    <!-- 除外するファイル・ディレクトリ -->
    <exclude-pattern>vendor/</exclude-pattern>
    <exclude-pattern>*test*</exclude-pattern>
    <exclude-pattern>tmp/</exclude-pattern>
    <exclude-pattern>*.(js|css|json)</exclude-pattern>
    <arg name="basepath" value="."/>
    <arg name="colors"/>
    <arg name="parallel" value="75"/>
    <arg value="np"/>
    <rule ref="MyRule"/>
</ruleset>

3. CodeSniffer.confの作成

phpcsディレクトリ下にCodeSniffer.conf.distがいるのでコピーして利用します
デフォルトルールやレポートの出力方法を変える場合は記載します

CodeSniffer.conf
<?php
 $phpCodeSnifferConfig = array (
  'default_standard' => 'MyRule',
  'report_format' => 'full',
  'show_warnings' => '0',
  'show_progress' => '1',
  'report_width' => '120',
)
?>

4. 自分ルールの作成

自作ルールのディレクトリを用意します。
Strings下にルールファイルを置く場合

mkdir -p ./vendor/squizlabs/php_codesniffer/src/Standards/MyRule/Sniffs/Strings

4.1. ruleset.xmlの作成

ruleset.xmlでは既存ルールの除外・追加、適用ルールのプロパティ変更が設定できます。
プロパティが変更できるルールはwikiに載っています。
他のルール同様にMyRule直下に配置します
./vendor/squizlabs/php_codesniffer/src/Standards/MyRule/ruleset.xml

ruleset.xml
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="MySource" xsi:noNamespaceSchemaLocation="../../../phpcs.xsd">
    <description>The custom coding for PSR-2</description>
    <arg name="tab-width" value="4"/>
    <rule ref="PSR2">
        <!-- PSR2から除外するルール -->
        <exclude name='Generic.Functions.FunctionCallArgumentSpacing'/>
        <exclude name='Squiz.WhiteSpace.SuperfluousWhitespace'/>
        <exclude name='Squiz.WhiteSpace.SuperfluousWhitespace.StartFile'/>
        <exclude name='Squiz.WhiteSpace.SuperfluousWhitespace.EndFile'/>
        <exclude name='Squiz.WhiteSpace.SuperfluousWhitespace.EmptyLines'/>        <exclude name='PSR2.Namespaces.NamespaceDeclaration'/>
        <exclude name='PSR2.Namespaces.UseDeclaration'/>
    </rule>
    <!-- 追加するルール -->
    <rule ref='Squiz.Strings.DoubleQuoteUsage'/>
    <!-- プロパティの変更 -->
    <!-- 1行に80文字以上でwarning、100文字以上でエラー表示 -->
    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="absoluteLineLimit" value="100"/>
        </properties>
    </rule>
</ruleset>

4.2. 自分ルールファイルを作る

先ほど作ったMyRule/Sniffs/Strings下に配置します。
下記はMySource/Strings/JoinStringsSniff.phpをMyRule用にコピーしたものです。

JoinStringSniff.php
<?php
namespace PHP_CodeSniffer\Standards\MyRule\Sniffs\Strings;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;

class JoinStringsSniff implements Sniff
{

    /**
     * A list of tokenizers this sniff supports.
     *
     * @var array
     */
    public $supportedTokenizers = ['PHP'];


    /**
     * Returns an array of tokens this test wants to listen for.
     *
     * @return array
     */
    public function register()
    {
        return [T_STRING];

    }//end register()


    /**
     * Processes this test, when one of its tokens is encountered.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
     * @param integer                     $stackPtr  The position of the current token
     *                                               in the stack passed in $tokens.
     *
     * @return void
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        if ($tokens[$stackPtr]['content'] !== 'join') {
            return;
        }

        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
        if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR) {
            return;
        }

        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
        if ($tokens[$prev]['code'] === T_CLOSE_SQUARE_BRACKET) {
            $opener = $tokens[$prev]['bracket_opener'];
            if ($tokens[($opener - 1)]['code'] !== T_STRING) {
                // This means the array is declared inline, like x = [a,b,c].join()
                // and not elsewhere, like x = y[a].join()
                // The first is not allowed while the second is.
                $error = 'Joining strings using inline arrays is not allowed; use the + operator instead';
                $phpcsFile->addError($error, $stackPtr, 'ArrayNotAllowed');
            }
        }

    }
}

メソッドregisterで検知に用いるPHPパーサトークンを設定します。
検知するとメソッドprocessが呼び出されて、トークンの検証を行います。

5. 動作確認

phpcs 対象ファイル

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel で $_SERVER['QUERY_STRING'] を取得する

$_SERVER['QUERY_STRING'] が欲しかったのです。

たとえば、URL が http://example.com/hoge?piyo=1&piyo=2 であれば、

であれば piyo=1&piyo=2 部分を取得したいのです。

結論以下のように書きました。

$request->getQueryString();

これは、laravel の内部で使っている symfony が用意しているメソッドです。

laravel 該当コード

symfony 該当コード

他には、以下のようにもかけます。

str_replace($request->url(), '', $request->fullUrl());

http_build_query($request->query());

違い

$_SERVER と getQueryString の違いはエンコードされるかどうかです。

$_SERVER['QUERY_STRING']
=> "key=hoge&piyo=;alert(1)"
$query
=> "key=hoge&piyo=%3Balert%281%29"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel で $_SERVER['QUERY_STRING'] を取得するいくつかの方法

$_SERVER['QUERY_STRING'] が欲しかったのです。

たとえば、URL が http://example.com/hoge?piyo=1&piyo=2 であれば、

であれば piyo=1&piyo=2 部分を取得したいのです。

結論以下のように書きました。

$request->getQueryString();

これは、laravel の内部で使っている symfony が用意しているメソッドです。

laravel 該当コード

symfony 該当コード

他には、以下のようにもかけます。

str_replace($request->url(), '', $request->fullUrl());

http_build_query($request->query());

違い

$_SERVER と getQueryString ではエンコードに少し違いがあります。

'?a=A&b="B"&c;alert(1);&d=あ'

request()->getQueryString()
=> "a=A&b=%22B%22&c%3Balert%281%29%3B=&d=%E3%81%82"

$_SERVER['QUERY_STRING'];
=> "a=A&b=%22B%22&c;alert(1);&d=%E3%81%82"

それと、Laravel の feature テストとかだと $_SERVER の方は Undefined index: QUERY_STRING になっちゃう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 使用頻度の高そうなコマンド

目的

  • Laravelで使用頻度の高そうなコマンドをまとめる
  • 自分が必要に感じたものを随時追加してゆく

ローカルサーバの起動

  • アプリ名ディレクトリで下記コマンドを実行する。

    $ php artisan serve
    

マイグレーションファイルを作成する。

  • マイグレーションファイルに命名規則はないが、どんな修正をDBに与えるのかがわかりやすいファイルである必要がある。
  1. テーブルの新規作成

    • アプリ名ディレクトリで下記コマンドを実行する。

      $ php artisan make:migration create_テーブル名_table --create=テーブル名
      
  2. 既存テーブルにカラム追加等の途中からテーブルに修正を加える

    • アプリ名ディレクトリで下記コマンドを実行する。(下記のコマンドは〇〇カラムを追加する時のコマンドの例)

      $ php artisan make:migration add_〇〇_column_テーブル名_table --table=テーブル名
      

マイグレーションファイルのマイグレート

  1. マイグレート

    • アプリ名ディレクトリで下記コマンドを実行する。

      $ php artisan migrate
      
    • ロールバック

    • アプリ名ディレクトリで下記コマンドを実行する。

      $ php artisan migrate:rollback
      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP【Laravel】:POSTの設定とCSRFについて

PHPのフレームワークLaravelでPOSTを使い値を送信する際の注意点について学んだのでメモ。

POSTを使うなんてプログラミングを学び始めの頃、見よう見まねでできたのに、何今頃つまずいているんだと凹みながら色々調べていたら、Laravelでは少し勝手が違うようだった。

まず、以下のコードを書きを実行したら、「419のHTTPステータスエラー」が出た。

ビュー

sample.blade.php 
<h1>タイトル</h1>
      <p class="name">ようこそ{{$msg}}さん</p>

  <form method="POST" action="/sample">
     名前を入力してください<br>
    <input type="text" name="msg">
    <input type="submit">
  </form>

ルーティング

web.php
Route::get('/sample', 'SampleController@test');
Route::post('/sample', 'SampleController@post');

コントローラ

SampleController.php
class SampleController extends Controller
{
  public function test(Request $request){

    $data['msg'] = $request->msg;

    return view('sample.sample', $data);
    //
  }

public function post(Request $request) {

    $data['msg'] = $request->msg;

    return view('sample.sample', $data);
  }

原因はCSRFトークン:

原因はCSRFトークンでした。
Laravelでは、クロスサイトリクエストフォージェリ対策として、CSRFトークンを使用することができます。

CSRFについて

Laravelではミドルウェアにデフォルトで

kernel.php
 \App\Http\Middleware\VerifyCsrfToken::class

という記載があります。

kernel.php
protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

なのでCSRF対策の仕組みを実際に使う必要があります。

修正

ビューに@csrfを追記。

sample.blade.php
<h1>タイトル</h1>
      <p class="name">ようこそ{{$msg}}さん</p>

  <form method="POST" action="/sample">
    @csrf
     名前を入力してください<br>
    <input type="text" name="msg">
    <input type="submit">
  </form>

もしくは、ミドルウェアで無効化(コメントアウト)。

kernel.php
protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,

           // \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP【Laravel】:POSTの設定とCSRF対策について

PHPのフレームワークLaravelでPOSTを使い値を送信する際の注意点について学んだのでメモ。

POSTを使うなんてプログラミングを学び始めの頃、見よう見まねでできたのに、何今頃つまずいているんだと凹みながら色々調べていたら、Laravelでは少し勝手が違うようだった。

まず、以下のコードを書きを実行したら、「419のHTTPステータスエラー」が出た。

ビュー

sample.blade.php 
<h1>タイトル</h1>
      <p class="name">ようこそ{{$msg}}さん</p>

  <form method="POST" action="/sample">
     名前を入力してください<br>
    <input type="text" name="msg">
    <input type="submit">
  </form>

ルーティング

web.php
Route::get('/sample', 'SampleController@test');
Route::post('/sample', 'SampleController@test');

コントローラ

SampleController.php
class SampleController extends Controller
{
  public function test(Request $request){

    $data['msg'] = $request->msg;

    return view('sample.sample', $data);
    //
  }

原因はCSRFトークン:

原因はCSRFトークンでした。
Laravelでは、クロスサイトリクエストフォージェリ対策として、CSRFトークンを使用することができます。

CSRFについて

Laravelではミドルウェアにデフォルトで

kernel.php
 \App\Http\Middleware\VerifyCsrfToken::class

という記載があります。

kernel.php
protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

なのでCSRF対策の仕組みを実際に使う必要があります。

修正

ビューに@csrfを追記。

sample.blade.php
<h1>タイトル</h1>
      <p class="name">ようこそ{{$msg}}さん</p>

  <form method="POST" action="/sample">
    @csrf
     名前を入力してください<br>
    <input type="text" name="msg">
    <input type="submit">
  </form>

もしくは、ミドルウェアで無効化(コメントアウト)。

kernel.php
protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,

           // \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelのartisanコマンドでルートを検索する(オプションまとめ)

概要

業務内で、Laravelで書かれているソースコードを読んでいる時に、ルーティングを効率的に検索する必要に迫られることがあります。ですので、オプションを使って効率的に検索する方法をまとめてみました。

ルート一覧の表示方法

php artisan route:list

ルートをパスから探す

php artisan route:list --path==api/user/like
+--------+--------+---------------+------+----------------------------------------------+------------+
| Domain | Method | URI           | Name | Action                                       | Middleware |
+--------+--------+---------------+------+----------------------------------------------+------------+
|        | POST   | api/user/like |      | App\Http\Controllers\LikeApiController@store | api        |
+--------+--------+---------------+------+----------------------------------------------+------------+

ルートを名前から探す

php artisan route:list --name==user
+--------+----------+------------+-------------+--------------------------------------------+------------+
| Domain | Method   | URI        | Name        | Action                                     | Middleware |
+--------+----------+------------+-------------+--------------------------------------------+------------+
|        | POST     | update     | user.update | App\Http\Controllers\UserController@update | web        |
|        | GET|HEAD | {userName} | user.index  | App\Http\Controllers\UserController@index  | web        |
+--------+----------+------------+-------------+--------------------------------------------+------------+

ルートをHTTPメソッドから探す

php artisan route:list --method=delete
+--------+--------+-------------------------------+-----------------------+------------------------------------------------------+------------------------------------------------------+
| Domain | Method | URI                           | Name                  | Action                                               | Middleware                                           |
+--------+--------+-------------------------------+-----------------------+------------------------------------------------------+------------------------------------------------------+
|        | DELETE | _debugbar/cache/{key}/{tags?} | debugbar.cache.delete | Barryvdh\Debugbar\Controllers\CacheController@delete | Barryvdh\Debugbar\Middleware\DebugbarEnabled,Closure |
|        | DELETE | delete/{delete}               |                       | App\Http\Controllers\PostController@delete           | web                                                  |
+--------+--------+-------------------------------+-----------------------+------------------------------------------------------+------------------------------------------------------+

複合検索も可能

php artisan route:list --method=post --name=like --path=unlike
+--------+--------+---------------------------+--------------+---------------------------------------------+------------+
| Domain | Method | URI                       | Name         | Action                                      | Middleware |
+--------+--------+---------------------------+--------------+---------------------------------------------+------------+
|        | POST   | user/{user}/unlike/{post} | like.destroy | App\Http\Controllers\LikeController@destroy | web        |
+--------+--------+---------------------------+--------------+---------------------------------------------+------------+

逆順で表示

php artisan route:list --method=DELETE -r

ソートの方法

php artisan route:list --method=DELETE --sort=name
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vagantを使ってLaravelを動かす

はじめに

初学者が最初に躓きやすいと言われる環境構築ですが手順をしっかり追い、そこで何が行われているを簡単にイメージすることが大事だと痛感しました。
そこで私的メモ程度に構築手順を書き記します。
あくまで私の環境ではこの手順で動くものであって、全員がこれで動くとは限らないので参考程度でお願いいたします。
また初学者であるがゆえに問題点があるかと思います。修正箇所やご指摘いただけると幸いでございます。

この記事でやること

  1. vagarantを使ってcentOS環境の構築。
  2. その環境下でPHP、Laravel、MySQLを導入。
  3. Nginxを立ち上げ、プロジェクトにログイン機能を実装する。

今回使うもの

  • Vagrant
  • vbguest
  • CentOS7
  • Nginx
  • PHP7.3
  • Laravel6.x
  • MySQL5.7

上記に接続できる環境があることを前提に手順を追っていきます。

環境を構築してゆく

作業ディレクトリの準備

まずは仮想環境に接続できる作業スペースを作成していきます。

ターミナル上でLinuxコマンドで作業ディレクトリを作成します。

今回は仮にvagrant_lessonとします。

mkdir vagrant_lesson cd vagrant_lesson

ディレクトリ移動後に使用するboxを指定してあげましょう。

今回はcentOS7を使用します。

vagrant init centos/7

# 実行後以下のようになれば成功
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

vagrantfileの編集

vagrant_lessonのVagrantfileをエディタで開き、以下記述に変更します。

1 コメントアウトを外す

config.vm.network "forwarded_port", guest: 80, host: 8080
# 今回ip番号は以下を使用します
config.vm.network "private_network", ip: "192.168.33.15"

2 記述を適当な箇所に追加

config.vm.synced_folder "./", "/vagrant", type:"virtualbox"

vagrantの起動

# Vagrantfileがあるディレクトリにて以下コマンドの実行
vagrant up

macbook air だとここあたりからpcがうなりだすかもしれませんがゆっくり見守ってあげましょう。
ちなみに起動や停止などのコマンドはこちらを参考
【まとめ】Vagrant コマンド一覧

仮想環境に接続する

ssh接続で環境に接続します。

今回作業するディレクトリ内で以下コマンドを実行するだけです。

vagrant ssh

# 実行後以下のようになれば成功
[vagrant@localhost ~]$

パッケージの導入

開発を使用する上で必要なパッケージをインストールします。

仮想環境につながった状態で以下コマンドを実行します。

[vagrant@localhost ~]$ sudo yum -y groupinstall "development tools"

これで作業するディレクトリの下準備が完了しました。

続いて実際に環境構築していきます。

環境構築~導入編~

このセクションでは環境構築に必要なものを仮想環境にインストールしていきます。

PHP7.3の導入

centOSのデフォルトのPHPのバージョンは5.4.16です。
一方で今回使用するPHPのバージョン7.3なので、それがインストール出来るようにcentOSの設定を変更します。

変更といってもコマンドを入力するだけです。

# EPELのリポジトリを追加
sudo yum -y install epel-release wget

# インストール先を最新の状態に更新します
sudo wget http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

# REMIのリポジトリを追加
sudo rpm -Uvh remi-release-7.rpm

# php7.3をインストール
sudo yum -y install --enablerepo=remi-php73 php php-pdo php-mysqlnd php-mbstring php-xml php-fpm php-common php-devel

# バージョン確認
php -v
# バージョンが7.3.x であれば成功

composerの導入

次にLaravelとそれに必要なcomposerをインストールしていきます。

まずはcomposerを導入します。

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

php composer-setup.php

php -r "unlink('composer-setup.php');"

# グローバルコマンドを使用するためにfileを移動
sudo mv composer.phar /usr/local/bin/composer

# バージョン確認
composer -v

バージョンが確認できれば成功です。

Laravel6.xの導入

ではLaravelのバージョン6.xを導入していきます。

準備としてLaravelを導入するディレクトリに移動しましょう。

# 移動して
cd /vagrant

# Laravel6.0をインストール
composer create-project laravel/laravel=6.0 --prefer-dist laravel_sample

# 移動して
cd laravel_sample/

# バージョン確認
php artisan --version

# 6.xになっていれば成功です。

Nginxの導入

最新のバージョンをインストールするため以下コマンドでファイルを編集します。

sudo vi /etc/yum.repos.d/nginx.repo

下記内容を追記します。

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/\$releasever/\$basearch/
gpgcheck=0
enabled=1

入力方法は
iでインサートモードにし、編集後escでインサートを終了し:wqで保存&終了します。

間違ってしまったら:q!しましょう。

ではインストールしましょう。

# laravel_sampleでコマンド実行
sudo yum install -y nginx

# バージョン確認
nginx -v

インストール成功したらNginxを起動してみましょう。

起動コマンドは以下のとおりです。

sudo systemctl start nginx

Nginxのwelcome画面が表示されれば成功です。

環境構築~Laravel表示編~

では、今表示されているNginxの画面からLaravelのホーム画面になるように設定していきます。

Nginxの設定ファイルを編集します。

sudo vi /etc/nginx/conf.d/default.conf
server {
  listen       80;
  server_name  192.168.33.15; # Vagranfileでコメントを外した箇所のipアドレスを記述してください
  root /vagrant/laravel_sample/public; # 追記
  index  index.html index.htm index.php; # 追記

  #charset koi8-r;
  #access_log  /var/log/nginx/host.access.log  main;

  location / {
      #root   /usr/share/nginx/html; # コメントアウト
      #index  index.html index.htm;  # コメントアウト
      try_files $uri $uri/ /index.php$is_args$args;  # 追記
  }

  # 省略

  # 以下の該当箇所のコメントアウトを指定の箇所外し、変更する場所もあるので変更を加える
  location ~ \.php$ {
  #    root           html;
      fastcgi_pass   127.0.0.1:9000;
      fastcgi_index  index.php;
      fastcgi_param  SCRIPT_FILENAME  /$document_root/$fastcgi_script_name;  # $fastcgi_script_name以前を /$document_root/に変更
      include        fastcgi_params;
  }

続いてphpインストール時にインストールしたphp-fpmの設定fileを編集していきます。

sudo vi /etc/php-fpm.d/www.conf
;24行目近辺
user = apache
↓ 変更
user = vagrant

group = apache
↓ 変更
group = vagrant

編集が完了したらNginxを再起動してphp-fpmを起動しましょう。

# Nginx再起動
sudo systemctl restart nginx

# php-fpm起動
sudo systemctl start php-fpm

Forbidden という403エラーが出た場合

sudo vi /etc/selinux/config

この記述を変更してくだい。

# 変更前
SELINUX=enforcing

# 変更後
SELINUX=disabled

保存を反映させるためにvagrantを再起動しましょう

exit #ログアウト

vagrant reload #再起動

再起動後、ssh接続しましょう。

vagrant ssh

接続が完了したらNginxを再起動してphp-fpmを起動しましょう。

# Nginx再起動
sudo systemctl restart nginx

# php-fpm起動
sudo systemctl start php-fpm

Laravelのhomeが表示されたらエラ−解決です。

環境構築~DBに接続編~

mysqlの導入

rpmにリポジトリを追加しインストールします。

sudo wget http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm

sudo rpm -Uvh mysql57-community-release-el7-7.noarch.rpm

# インストール
sudo yum install -y mysql-community-server

# バージョン確認
mysql --version

#バージョンが確認できたら成功です

mysqlに接続

sudo systemctl start mysqld

sudo cat /var/log/mysqld.log | grep 'temporary password'

#以下表示される。文末がパスになるのでコピーする
2017-01-01T00:00:00.000000Z 1 [Note] A temporary password is generated for root@localhost: ********

mysql -u root -p

#パスワードが求められるので先程コピーしたものを入力
Enter password: ********

接続後パスワードを変えます。

-- パスワードはダブルクオーテーションで囲む
mysql > set password = "新たなpassword(必ず大文字小文字の英数字 + 記号かつ8文字以上)";

DBを作成

最後に実際に使用するDBを作成しましょう。

mysql > create database DBの名前;

-- 以下表記で成功
Query OK, 1 row affected (0.00 sec)

作成したプロジェクトに登録・ログイン機能を実装してゆく

仮想環境に繋いだまま、作成したlaravel_sampleに移動しましょう。

移動後、以下コマンドを実行するだけでLaravelホーム画面に登録機能とログイン機能が実装できます。

composer require laravel/ui 1.*

php artisan ui vue --auth

# 以下が表示されれば成功
Vue scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.
Authentication scaffolding generated successfully.

先程設定したhttp://192.168.33.15/を開きし、右上にregisterとloginの項目があれば成功です。

さいごに

環境構築と言われ難しいイメージを感じたかもしれませんが、何がどの役割を持っているかイメージできれば割と理解しやすいかと思います。
ただ私も6割程度の理解ですのでこれからこの辺りの知識は深める必要があると感じました。

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクト指向(PHP)

オブジェクト指向とは

オブジェクト指向とは、システムの作り方の一種。
ちなみにシステムとは、個々の要素が相互に影響しあいながら全体として機能する纏まりのことであり、クラス同士を組み合わせて作られていくものである。

例えば、一つのシステムを構築するソースコードを、一つのクラス内に書こうと思えば全てを記載することが可能であるが、コードは冗長且つ複雑になるため可読性が低く、機能の変更や追加の必要性が生じた際に膨大な労力を生み出してしまうことが容易に想像できる。

そこで、役割ごとにクラスを作り分け、『一つ一つのクラスを役割を持った物(オブジェクト)として扱おう』として生み出されたのがオブジェクト指向である。

具体的に言うと、一つのパソコンには、キーボード、マウス、モニターなどの部品が存在する。

この部品にもそれぞれの役割があるように、役割を持った物ごとにクラスを作り分けておけば、キーボードが故障した際はキーボードクラスのソースコードを修正していけば良いし、新しい機能を付けたい時も対象となるクラスだけを変更すれば良いということである。

つまり、オブジェクト指向とは、『ある役割を持った物ごとにクラスを分割し、物と物との関係性を定義していくことでシステムを作り上げようとするシステム構成の考え方』であり、ソースコードの分け方の一種である。

オブジェクト指向がもたらすメリット

オブジェクト指向は、大規模システムを作っていく上でメリットをもたらす。

前記の様に、機能の役割ごとにクラスやファイル等のソースコードを分けていくため、一つのシステムを複数人で作成していくことが可能である。

また、ソースコードを分けているため、短くまとまったコードは可読性が高い上に、処理がシンプルなのでテストも楽に実行出来る。

役割ごとに分けたクラスは、外部で必要になった際など様々な場面で使い回すことが可能である。
例えば、経理システム内で消費税計算のプログラムは様々な場面で使うことが予想出来るため、消費税計算の処理を一纏まりにしておけば、使い回しが可能になる。

そのクラス自体の作成意図が組めるため、情報の管理がしやすい上、仕様の変更が生じても変更箇所の特定がし易い他、影響範囲も小さく、メンテナンス性が高いというメリットがある。

故にオブジェクト指向を取り入れている言語は、大きいシステム構築で採用されるケースが多い。

クラスについて

クラスは設計図とも言える。

例えば、車を作ろうとした時に、どんな機能が車にあるか想像してみると、車には色や持ち主等の固有情報を持つ他、加速・減速、ライトの点灯・消灯などの車として存在するために必要な機能が備わっている。

色や持ち主は変更が不可能ではないが、基本的には車が持つ固有の属性である。
アクセルを踏むと加速、ブレーキを踏めば減速する等の処理を担う機能のことをメソッドと呼ぶ。

こういった物の情報と機能を定義したものがクラスであり、まさにクラスはその物の設計図と言える。
よって、クラス内にはメソッドの定義と属性の宣言が出来る。

インスタンスについて

前記の様に、クラスは紙一枚の設計図と一緒なので、紙一枚のクラス自体に処理を求めても不可能である。
そのため、設計図を元に処理を実行する実体を生む必要がある。

この設計図を元に生まれた実態をインスタンスと言い、実体化することをインスタンス化と言う。

いくら実態とは言え、インスタンス自体は動くことの出来ない、ただの物体に過ぎない。
そのため、実体化した物に対して、メソッドの呼び出し等の命令を与えていく必要がある。
(実体化した際に、自動的にインスタンスが処理を行う記述もクラス内に出来る)

設計図があれば、様々な個性ある車を作れる様に、一つのクラスからは複数のインスタンスを生成できる。
故にどんな車でも共通して持つ内容を設計図に定義する必要がある。

とはいえ、車の設計図のオーナー欄に『持ち主は太郎』と書いてしまうと、全ての車の持ち主が太郎として生成されてしまうため、初めから属性値を決めるような変数の初期化は宜しくない。

よって、属性のタイトルのみをクラスに定義し、各インスタンスに対してそれぞれの値を当てはめていくことが望ましい。

この理論で言えば、インスタンス化の後には、セットとして属性の値を詰めていく記述をする必要がある。
そこで、インスタンスというあやふやな存在を作りつつ、同時に値を詰めて完全な実体化をしていくことが理想であり、これをPHPではコンストラクタと呼ぶ。

コンストラクタを使用すれば、インスタンスの生成時に自動的な処理をさせることが出来る。

カプセル化

オブジェクト指向には3つの重要な機能がある
その一つがカプセル化である。

例えば、人間クラスがあるとして、人間には名前という属性がある。

しかし、この名前は簡単に変更出来るわけではなく、意図して他人が変えれるものでもない上、SNS等に本名を上げてしまえば個人情報が拡散されるために隠したい情報でもある。

変更したい場合は本人が役所で手続きをする必要があり、悪意による属性の書き換えや、うっかりミスや意図せぬ用途で外部クラスからの書き換えが出来ない様にする必要がある。

この目的を実現するためにカプセル化という存在がある。

カプセル化にはアクセス権を定義するprivate(プライベート)とpublic(パブリック)がある。

privateとは自クラス内からのみアクセスを許可するために付ける修飾子であり、プライベート化すると他人が知る由もない見えない存在になるため、外部クラスからは変更はおろか参照も出来なくなる
プロパティは基本的にアクセス権をprivateにすることが望ましい。 

車にはアクセルを踏むと前に進むという車としての揺るぎない機能がある。
これを変更してしまうと車としての存在意義を揺るがしてしまうことになるため、この機能をカプセルの内側に閉じ込めておこうというもの。

その反対で、publicという修飾子を付けると、外部クラスからも参照や書き換えが可能となる。

publicを使用した場合

<?php

  class Human {    //人間クラスを定義
    puclic $name  //氏名属性を定義してパブリック修飾子を付ける
    public function __construct($name) {
      $this->name = $name;
    }
  }
  //クラス外
  $japanese = new Human('taro');  //人間クラスをインスタンス(実体)化

  echo $japanese->name;  //japaneseインスタンス変数の氏名属性値を出力

  //出力結果:taro
?>
?>

privateを使用した場合

<?php

  class Human {    //人間クラスを定義
    private $name  //氏名属性を定義してプライベート修飾子を付ける
    public function __construct($name) {
      $this->name = $name;
    }
  }
  //クラス外
  $japanese = new Human('taro');  //人間クラスをインスタンス(実体)化

  echo $japanese->name;  //japaneseインスタンス変数の氏名属性値を出力

  //出力結果:エラー
?>

ゲッター

上記コードの様に、プロパティのアクセス権をprivateにすると、プロパティの値をクラスの外から取り出すことができなくなる。

そこで、プロパティの値を返すだけのメソッドの一つとして「ゲッター」が存在する。
ゲッターは「getプロパティ名」のように命名するのが一般的である。

<?php
  class Human {   
    private $name  //プライベート修飾子を付けている
    public function __construct($name) {
      $this->name = $name;
    }
    public function getName() { //ゲッターを定義
      return $this->name;
    }
  }
  //クラス外
  $japanese = new Human('taro');   
  echo $japanese->getName();  //ゲッターの呼び出し

  //出力結果:taro

?>

セッター

privateによりプロパティの値を変更出来ない場合に、プロパティの値を変更する「セッター」というメソッドが存在する。
セッターは「setプロパティ名」のように命名するのが一般的である。 

今回はここまで:santa:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP基本集(2)~クラスとは~

自分の学習用です:santa_tone1:

クラスの定義方法

クラスとは、簡単にいうと設計図のようなもの。
オブジェクト指向(簡単に言うと現実世界の物に例える様にプログラムを組む思考)に倣って作成していく。
漫画『ONE PIECE』の世界で例えるとすれば、海賊船のクラス(設計図)があったとする。

<?php
  class PirateShip {  //クラス名の頭文字は大文字にする
    //クラスの内容
  }
?>

インスタンス

インスタンスとは、クラスを元に実際に具現化した実体である。
設計図を元に造船したようなものである。
インスタンスを生成する際は、「$インスタンス名 = new クラス名( )」と記述する。

<?php
  class PirateShip {  //海賊船の設計図

  }

  $goingMerry = new PirateShip();   //ゴーイング・メリー号を造船
  $mobyDick = new PirateShip();     //モビー・ディック号を造船
  $arkMaxim = new PirateShip();     //方舟マキシムを造船
?>

クラスのプロパティ

プロパティとはクラスが持つデータのことである。
キー(データのタイトル)値(データ内容)で組み合わせられている。
クラスにプロパティのキーを定義することで、各インスタンスによってデータ内容を変えていくことが出来る。
プロパティの定義する際は、「public プロパティ名」と記述する。

  <?php
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティの定義。
  }
  //インスタンスの生成
  $goingMerry = new PirateShip();   //ゴーイング・メリー号を造船
  $mobyDick = new PirateShip();     //モビー・ディック号を造船
  $arkMaxim = new PirateShip();     //方舟マキシムを造船
  //プロパティの値を定義
  $goingMerry->captain = 'luffy';   //船長はルフィ
  $mobyDick->captain = 'newgate';   //船長は白ひげ
  $arkMaxim->captain = 'enel';      //船長はエネル

各インスタンスのプロパティ値にアクセスする時は「$インスタンス名->プロパティ名」とする。

<?php echo $goingMerry->captain; //「luffy」が出力される ?>

クラスのメソッド

メソッドは一連の処理を一纏めにしたものである。
メソッドを定義する際は、「public function メソッド名( ) { 処理内容 }」と記述する。
メソッドの中で、そのメドッドのプロパティやメソッドにアクセスするときは「$this」を使用出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function introduction() { //船の船長名を出力するメソッドを定義
      echo 'この海賊船の船長は'.$this->captain.'です'; 
    }
  }
?>

各インスタンスのメソッドを呼び出す時は「$インスタンス名->メソッド名」とする。

<?php $goingMerry->introduction; //出力結果:この海賊船の船長はluffyです ?> 

コンストラクタ

__construct」というメソッドを使用すると、newを用いてインスタンスを生成した時点で自動的にメソッドを呼び出すことが出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct() { 
      //インスタンス生成時に自動的に船の種類を出力する処理が行われる
      echo 'これは海賊船です'; 
    }
  }
?>

例えば、プロパティの値を定義していなくても、上記コンストラクタに引数を渡せば、引数を利用して自動的にプロパティ値をセットすることも出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct($captain) { //仮引数で値(luffy)を受け取る  
      $this->captain = $captain; //インスタンス生成時に自動的にcaptainデータに値をセット
    }
  }

  $goingMerry = new PirateShip('luffy'); //テータにセットする値(luffy)を引数で渡す

?>

HTMLにPHPを埋め込む

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct($captain) { //仮引数で値(luffy)を受け取る  
      $this->captain = $captain; //インスタンス生成時に自動的にcaptainデータに値をセット
    }
  }
  $goingMerry = new PirateShip('luffy'); //テータにセットする値(luffy)を引数で渡す
?>

<p>船長は<?php echo $goingMerry->captain ?>である<p>  

条件分岐や繰り返し処理を埋め込む

foreach文を使用する場合
{ 』の代わりに『 : 』を使用し、『 } 』の代わりに『 endforeach 』と記述する。

<?php 
  $captains = ['luffy', 'newgate', 'enel'] //配列作成
?>

<h1>参加する船長名<h1>

<?php foreach($captains as $captain): ?>
  <p><?php echo $captain ?></p>
<?php endforeach ?>

これでpタグの部分には、配列に入れられた船長3名分の名前が繰り返し表示される。

foreach文の他にも、if文やfor文、while文、switch文でも上記と同様の記述をしていく。

<?php if($age >= 20): ?>
  内容
<?php endif ?>

<?php for($i = 0; $i < 100; $i++): ?>
  内容
<?php endfor ?>

<?php while($i < 50): ?>
  内容
<?php endwhile ?>

ファイルを分割する

ただし、上記の記述のようにPHPとHTMLのコードを織り交ぜていくと複雑で見辛くなるので、
その場合はクラス定義用のファイル、データ定義用のファイル、表示用のファイルに分けるなどして、ファイルを分割していく。
require_once ( ' 読み込みたいファイル名 ' )」を使用して読み込むことで、別ファイルに定義したクラスやメソッド、処理内容を反映することができる。

index.php
<?php require_once('one_piece.php') ?>

<p><?php echo $goingMerry->introduction ?></p>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP基本集(2)~クラス~

自分の学習用です:santa_tone1:

クラスの定義方法

クラスとは、簡単にいうと設計図のようなもの。
オブジェクト指向(簡単に言うと現実世界の物に例える様にプログラムを組む思考)に倣って作成していく。

オブジェクト指向(PHP)

漫画『ONE PIECE』の世界で例えるとすれば、海賊船のクラス(設計図)があったとする。

<?php
  class PirateShip {  //クラス名の頭文字は大文字にする
    //クラスの内容
  }
?>

インスタンス

インスタンスとは、クラスを元に実際に具現化した実体である。
設計図を元に造船したようなものである。
インスタンスを生成する際は、「$インスタンス名 = new クラス名( )」と記述する。

<?php
  class PirateShip {  //海賊船の設計図

  }

  $goingMerry = new PirateShip();   //ゴーイング・メリー号を造船
  $mobyDick = new PirateShip();     //モビー・ディック号を造船
  $arkMaxim = new PirateShip();     //方舟マキシムを造船
?>

クラスのプロパティ

プロパティとはクラスが持つデータのことである。
キー(データのタイトル)値(データ内容)で組み合わせられている。
クラスにプロパティのキーを定義することで、各インスタンスによってデータ内容を変えていくことが出来る。

  <?php
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティの定義。
  }
  //インスタンスの生成
  $goingMerry = new PirateShip();   //ゴーイング・メリー号を造船
  $mobyDick = new PirateShip();     //モビー・ディック号を造船
  $arkMaxim = new PirateShip();     //方舟マキシムを造船
  //プロパティの値を定義
  $goingMerry->captain = 'luffy';   //船長はルフィ
  $mobyDick->captain = 'newgate';   //船長は白ひげ
  $arkMaxim->captain = 'enel';      //船長はエネル

各インスタンスのプロパティ値にアクセスする時は「$インスタンス名->プロパティ名」とする。

<?php echo $goingMerry->captain; //「luffy」が出力される ?>

クラスのメソッド

メソッドは一連の処理を一纏めにしたものである。
同じクラスの中で、プロパティやメソッドにアクセスするときは「$this」を使用出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function introduction() { //船の船長名を出力するメソッドを定義
      echo 'この海賊船の船長は'.$this->captain.'です'; 
    }
  }
?>

各インスタンスのメソッドを呼び出す時は「$インスタンス名->メソッド名」とする。

<?php $goingMerry->introduction; //出力結果:この海賊船の船長はluffyです ?> 

コンストラクタ

__construct」というメソッドを使用すると、newを用いてインスタンスを生成した時点で自動的にメソッドを呼び出すことが出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct() { 
      //インスタンス生成時に自動的に船の種類を出力する処理が行われる
      echo 'これは海賊船です'; 
    }
  }
?>

例えば、プロパティの値を定義していなくても、上記コンストラクタに引数を渡せば、引数を利用して自動的にプロパティ値をセットすることも出来る。

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct($captain) { //仮引数で値(luffy)を受け取る  
      $this->captain = $captain; //インスタンス生成時に自動的にcaptainデータに値をセット
    }
  }

  $goingMerry = new PirateShip('luffy'); //テータにセットする値(luffy)を引数で渡す

?>

HTMLにPHPを埋め込む

<?php 
  class PirateShip {  //海賊船の設計図
    public $captain;  //船長プロパティ。
    public function __construct($captain) { //仮引数で値(luffy)を受け取る  
      $this->captain = $captain; //インスタンス生成時に自動的にcaptainデータに値をセット
    }
  }
  $goingMerry = new PirateShip('luffy'); //テータにセットする値(luffy)を引数で渡す
?>

<p>船長は<?php echo $goingMerry->captain ?>である<p>  

条件分岐や繰り返し処理を埋め込む

foreach文を使用する場合
{ 』の代わりに『 : 』を使用し、『 } 』の代わりに『 endforeach 』と記述する。

<?php 
  $captains = ['luffy', 'newgate', 'enel'] //配列作成
?>

<h1>参加する船長名<h1>

<?php foreach($captains as $captain): ?>
  <p><?php echo $captain ?></p>
<?php endforeach ?>

これでpタグの部分には、配列に入れられた船長3名分の名前が繰り返し表示される。

foreach文の他にも、if文やfor文、while文、switch文でも上記と同様の記述をしていく。

<?php if($age >= 20): ?>
  内容
<?php endif ?>

<?php for($i = 0; $i < 100; $i++): ?>
  内容
<?php endfor ?>

<?php while($i < 50): ?>
  内容
<?php endwhile ?>

ファイルを分割する

ただし、上記の記述のようにPHPとHTMLのコードを織り交ぜていくと複雑で見辛くなるので、
その場合はクラス定義用のファイル、データ定義用のファイル、表示用のファイルに分けるなどして、ファイルを分割していく。
require_once ( ' 読み込みたいファイル名 ' )」を使用して読み込むことで、別ファイルに定義したクラスやメソッド、処理内容を反映することができる。

index.php
<?php require_once('one_piece.php') ?>

<p><?php echo $goingMerry->introduction ?></p>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Eclipse 補完パターン追加方法

はじめに

Eclipseには初期状態でも便利な補完パターンがいくつも登録されております。しかし、使っていると…

  • これもあったりいいな~
  • 他のテキストエディタではこれ補完してくれるのにEclipseではしてくれないのね…

と思うようになりました。
Eclipseでは補完パターンを簡単に追加変更削除をすることができますので、紹介します。

やり方

ウィンドウ > 設定 > PHP > エディター > テンプレート

を開きます。すると補完パターンを見ることできます。ここから補完パターンの追加は勿論、既存の補完パターンをカスタムすることもできます。

私はよくexplodeでカンマ区切りの配列を作るのですが、その場合…

名前
explode

説明
explodeカンマ

コンテキスト
php statemets

パターン
explode(",", $$${});${cursor}

を作成します。補完からexplodeカンマを選択してEnterを押すと…

explode(",", $!);

と出てきます。分割文字","が初めから入力されており、カーソルが分割する変数名を入力する部分に入るため、いきなり変数名を入力できる!
同様の手順でexplode改行などを作っておけば、補完のプルダウンにある程度完成されたステートメントをいくつも置いておくことができそうです。便利便利!!

インポートもできるみたいなので、私が愛用している補完パターンも後日紹介予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHP3でTODOアプリを作ってみる

CakePHP3でTODOアプリを作ってみます

作ったもの: https://github.com/kshiva1126/cakephp3_todo.git

setup

Gitクローンからコンテナの立ち上げまで

$ git clone https://github.com/kshiva1126/cakephp3_docker.git
$ cd cakephp3_docker
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app composer install

Databaseの設定

config/app.php の下記4項目を修正します

    'Datasources' => [
        'default' => [
            ...
            'host' => 'db',
            'username' => 'user',
            'password' => 'password',
            'database' => 'cake_db',
            ...
        ]
    ]

http://localhost:3000 でページが確認できます

migration

下記コマンドでマイグレーションファイルを生成します

config/Migrations/ に出力されます

bin/cake bake migration CreateTasks name:string decription:text done:boolean created modified

ちなみに bin/cake migrations create でも作成できるらしいけど今回はスキップ

<?php
use Migrations\AbstractMigration;

class CreateTasks extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('tasks');
        $table->addColumn('name', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('decription', 'text', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('done', 'boolean', [
            'default' => false, // nullからfalseに変更した
            'null' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->create();
    }
}

マイグレートします

bin/cake migrations migrate
mysql> desc tasks;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | NO   |     | NULL    |                |
| decription | text         | NO   |     | NULL    |                |
| done       | tinyint(1)   | NO   |     | 0       |                |
| created    | datetime     | NO   |     | NULL    |                |
| modified   | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

マイグレーションファイルにはなかった id は自動的に追加されるようです

bake

CakePHPといえば、 bake ですね!!!

bake にはたくさんのサブコマンドがありますが、今回は all を利用してMVCすべてのスケルトンを生成します

先程作成した tasks を指定します

bin/cake bake all tasks

ここで http://localhost:3000/tasks にアクセスしてみます

tasks_top.png

もうそれっぽい画面が表示されました

左カラムの New Task から追加画面にいけます

tasks_add.png

追加後です

tasks_top_added.png

customize

このまま終了だとかなり味気ないので、ちょっとしたカスタマイズを加えます

現状、Done を 更新するためには編集画面で行うしかありません

tasks_edit.png

これを、TOP画面からでも更新できるようにします

まずViewを編集します

Done を Checkbox で表示させます

参考: https://book.cakephp.org/3/ja/views/helpers/form.html#checkbox-radio-select-options

src/Template/Tasks/index.ctp

<tbody>
    <?php foreach ($tasks as $task): ?>
    <tr>
        <td><?= $this->Number->format($task->id) ?></td>
        <td><?= h($task->name) ?></td>
        <!-- <td><?= h($task->done) ?></td> -->
        <!-- Checkboxで表示させるように修正 -->
        <td><?= $this->Form->checkbox('done', [
            'value' => h($task->done),
            'checked' => h($task->done),
            'data-id' => $this->Number->format($task->id),
            'hiddenField' => false,
        ]) ?></td>
        <td><?= h($task->created) ?></td>
        <td><?= h($task->modified) ?></td>
        .. 省略 ..
</tbody>

jQueryのAjaxを使いたいので、CDNで読み込ませます

src/Template/Layout/default.ctp

<head>
    <?= $this->Html->charset() ?>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>
        <?= $cakeDescription ?>:
        <?= $this->fetch('title') ?>
    </title>
    .. 省略 ..

    <!-- jQuery読み込み -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
</head>

Done を更新するためにAjaxでPOSTする処理を追加します

src/Template/Tasks/index.ctp

<script>
$(function () {
    const csrfToken = <?= json_encode($this->request->getParam("_csrfToken")) ?>;
    $("input[name='done']").on("change", function (event) {
        const data = {
            id: $(this).data('id'),
            done: $(this).prop('checked') ? 1 : 0,
        };
        $.ajax({
            type: 'POST',
            dataType: "json",
            url: '/tasks/changeDone',
            headers: {
                'X-CSRF-Token': csrfToken
            },
            data,
        })
        .fail(function (err) {
            console.log(err)
        });
    });
});
</script>

Controllerに更新処理を追加します

TasksController に新たに changeDone() メソッドを追加することで /tasks/changeDone でアクセスできるようになります

src/Controller/TasksController

public function changeDone()
{
    // postのみ許可する
    $this->request->allowMethod('post');
    $id = (int) $this->request->getData('id');
    $done = (int) $this->request->getData('done');

    // idに合致するTaskを取得
    $task = $this->Tasks->get($id);
    $task->done = $done;
    $this->Tasks->save($task);

    exit;
}

(CakePHP的にこの書き方で正しいのかはわからない... (特に最後の exit; ))

これでTOP画面からDone の更新が行えるようになりました:clap:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む