はじめに
今回は「実務4年目のWEBエンジニアが「PHP本格入門(下)」を読んで学んだこと 12選」というテーマでお伝えします。
PHP本格入門(下)(以下、本書)
本書を手に取った経緯などは、下記記事をご参照ください!
それでは、本題です。
ここからは、本書を読んで学んだこと 12選をお伝えしていきます。
結論
私が本書を読んで学んだことは、以下の通りです。
学んだこと 12選
- クラスの関係性を表すキーワード
- 神クラス(アンチパターン)
- なんでもpublicなクラス(アンチパターン)
- クラスに動的にアクセスする方法
- イテレータを使用した繰り返し処理
- ジェネレーター
- メソッドチェーンに対応したクラスの作り方
- HTMLおよびJavaScriptのコメントには注意する
- セキュリティリスクを少なくするためのphp.iniの設定
- SQLインジェクション攻撃とその対策
- オープンリダイレクトの脆弱性
- PHP_CodeSniffer
ここからは1つずつ詳細をお伝えしていきます。
1. クラスの関係性を表すキーワード
本書では、クラスの関係性を表すキーワードについて述べられています。
そのキーワードとは、以下の3つです。
- is-a の関係
- has-a の関係
- 関連
順に説明していきます!
is-a の関係
is-a の関係とは、「●●(サブクラス)は▲▲(スーパークラス)である」という言葉で表せる関係です。
例えば、
- 「デジタル時計(サブクラス)は時計(スーパークラス)である」
- 「アナログ時計(サブクラス)は時計(スーパークラス)である」
と言い表せます。
is-aの関係を図で表すと以下のようになります。
is-a の関係の場合、クラス間の関係性は継承となります。
has-aの関係
has-a の関係とは「あるクラスが別のクラスを持っている」関係です。
has-a の関係は、さらにコンポジションと集約に分けられます。
コンポジション
コンポジションとは「●●は▲▲で構成される」という言葉で表せる関係です。
例えば、自動車をイメージするとわかりやすいです。
「自動車はボディ、タイヤ、ハンドルで構成される」と言い表せます。
上記図中の数字は「多重度」と呼ばれ、紐づくクラスの数を表しています。
コンポジションには「構成物の所有者が消えると構成物も消える」という特徴があります。
自動車が消えると、ボディ、タイヤ、ハンドルも消えるということです。
集約
集約とは「●●は▲▲を持ちえるが、●●と▲▲は独立している」という関係性です。
例えば、コインパーキングと自動車をイメージしてください。
「コインパーキングは自動車を持ちえるが、コインパーキングと自動車は独立している」といえます。
関連
is-a でも has-a でもないが、クラス間が何かしらの関係性を持つことを「関連」といいます。
例えば、自動車とディーラーのような関係性です。
自動車とディーラーはゆるやかな関係性を持っています。
設計を行う上で、is-a と has-a の見極めが難しいケースがあります。
基本的に、「迷ったら has-a を選ぶ」のが安全です!
なぜなら、has-a であればカプセル化が保てるからです。
逆に is-a を選んでしまうと、カプセル化が保てなくなり、クラス間に依存関係が生じてしまいます。
2. 神クラス(アンチパターン)
アンチパターンとして、「神クラス」と呼ばれるクラスがあります。
全知全能、なんでもこなしてしまう(凝集度の低い)クラスのことです。
基本的に、神クラスは作らないようにしましょう!
ありがちな神クラスの例を以下に示します。
POSレジクラスは、なんでもかんでもやりすぎています。
こうしたクラスは、以下のように分割しましょう。
できるだけ機能を分割し、凝集度を高めることを意識しましょう!
3. なんでもpublicなクラス(アンチパターン)
アンチパターンとして「なんでもpublicなクラス」があります。
例えば以下のような「画像ファイル加工クラス」を考えます。
「+」が「public」を意味しています。
このクラスのどこがいけないかというと、「利用者が使いづらい構造になっている」という点です。
仮に、利用者が「リサイズするメソッド」を使いたいケースを考えます。
利用者は、単に**「リサイズする」メソッドをコールするだけで良いのでしょうか?**
もしかしたら、あらかじめ「幅」「高さ」「画素数」などをセットしておかないと、正しくリサイズできないかもしれません。
このように、メソッドやプロパティがなんでもpublicで定義されていると、利用者が迷ってしまいます。
そこで、以下のように適切なアクセス修飾子を与えることを意識しましょう!
「-」が「private」を意味しています。
クラスの使い方としては、以下が想定されていることが予想できます。
- 呼び出し時に対象ファイルパスを渡す
- 画像をリサイズおよび回転したい場合は、それぞれメソッドをコールする
本来privateなものまでpublicにしてしまうと、そのメソッドやプロパティをコールしなければならない、という誤解を与える可能性があるので避けましょう!
4. クラスに動的にアクセスする方法
続いては、条件によってアクセスするクラスを切り替える場合の記述方法です。
例として、消費税の計算クラスを切り替えるプログラムを考えます。
あるお弁当屋では、**「テイクアウト」「イートイン」**のいずれかの注文区分が存在するとします。
具体的な実装イメージは以下の通りです。
$taxClassName = 'TaxCalculator' . $order['type']; // クラス名を作成
$taxInstance = new $taxClassName(); // クラスを動的にインスタンス化
このように、インスタンス化する際のクラス指定部分を変数化すると、動的にクラスをインスタンス化することができます!
5. イテレータを使用した繰り返し処理
イテレータとは、「繰り返し可能であるもの」の総称です。
イテレータを使用すると、クラスを配列と同じように繰り返し処理できるようになります。
本書では、イテレータの一種である「IteratorAggregateインターフェース」を使った実装例が紹介されています。
<?php
class ShoppingCart implements IteratorAggregate
{
private $item = [];
public function addItem($item): boid
{
// 商品を追加する処理
}
// foreachループの時に内部的にコールされるメソッド。
// IteratorAggregateインターフェース内に定義されている。
// 戻り値はTraversable型出なければならない。
public function getIterator(): Traversable
{
return new ArrayIterator($this->item);
}
}
(インスタンス化)
<?php $cart = new ShoppingCart(); ?>
(IteratorAggregateを実装していることで、インスタンスを繰り返し処理可能)
<?php foreach ($cart as $itemNumber => $item): ?>
<p><?= $itemNumber ?>:<?= $item ?></p>
<?php endforeach; ?>
上記のように、IteratorAggregateインターフェースを使用すると、クラスのインスタンスを連想配列として扱えるようになります!
6.ジェネレーター
ジェネレーターとは、複雑な規則性を持つ配列の要素を、1要素ずつ順々に返す特殊な関数です。
以下のメリットがあります。
ジェネレーターのメリット
- 配列処理のメモリ消費を抑えられる
- 複雑な配列生成処理を隠蔽できる(配列を扱う時は配列生成について気にしなくて良い)
ジェネレーター関数は、returnの代わりにyieldを使用して順々に値を返します。
yield 値;
省メモリ化の例として、以下の処理を見てみましょう。
処理内容は、「AからZまでを返却したあと、AAからZZまでを返却する」というものです。
<?php
// ジェネレーター
function yieldColumnName()
{
// AからZまでを返却
foreach (range('A', 'Z') as $columnName) {
yield $columnName;
}
// AAからZZまでを返却
foreach (range('A', 'Z') as $columnName1) {
foreach (range('A', 'Z') as $columnName2) {
yield $columnName1 . $columnName2;
}
}
}
// メインルーチン
foreach (yieldColumnName() as $columnName) {
echo $columnName, PHP_EOL;
}
上記処理を実行すると以下の通りの結果となります。
A
B
C
〜 中略 〜
X
Y
Z
AA
AB
AC
〜 中略 〜
ZX
ZY
ZZ
ジェネレーターでは1カラム分のメモリ使用で済みますが、returnで実装すると配列の大きさ分のメモリが必要となります。
このようにジェネレーターを使用することで、メモリの節約ができます!
7. メソッドチェーンに対応したクラスの作り方
インスタンスに対するメソッド呼び出しをまとめて書く記法を「メソッドチェーン」といいます。
メソッドチェーンを使用することで、コンパクトに処理を書けるだけでなく、関連性もわかりやすくなるというメリットがあります!
このメソッドチェーンは、自作のクラスでも使用することができます。
メソッドチェーンに対応したクラスを作る方法は以下の通りです。
- メソッドの戻り値を$thisにする
- 結果を得るためのメソッドを作る
- コンストラクタをprivateにし、代わりにpublicメソッドを作る
サンプルコードは以下の通りです。
1. メソッドの戻り値を$thisにする
例として、四則演算をするクラスを作成します。
Class Calculator
{
private $result; // 計算結果
public function add($value)
{
$this->result += $value;
// 戻り値を$thisにする
return $this;
}
public function subtract($value)
{
$this->result -= $value;
// 戻り値を$thisにする
return $this;
}
2. 結果を得るためのメソッドを作る
public function getResult()
{
// 計算結果を返す
return $this->result;
}
3. コンストラクタをprivateにし、代わりにpublicメソッドを作る
インスタンス生成も含めて1ステップで書けるようにするために、コンストラクタをprivateにし、代わりにpublicかつstaticなメソッドを作ります。
// Calculateインスタンスを取得する。
// 引数$startValueには演算前の初期値を入れる。
public static function newInstance(int $startValue): Calculator
{
return new Calculator($startValue);
}
// コンストラクタはprivateとし、newInstanceメソッドからのみ呼び出せるようにする。
private function __construct(int $startValue)
{
(省略)
これで、呼び出し側は下記のように1ステップで全ての計算処理を書けるようになります!
$result = Calculate::newInstance(0)->add(10)->substract(7)->getResult();
// $resultは、3となります。
// 0 + 10 - 7 = 3
8. HTMLおよびJavaScriptのコメントには注意する
HTMLおよびJavaScriptのコメントは、そっくりそのままユーザーに開示されているので取り扱いに注意しましょう!
避けるべきコメントをご紹介します。
下記のHTMLコメントでは、デバッグ用画面のURLが記載されてしまっています。
<a href="mypage.php">マイページ</a>
<!-- a href="mypage-debug.php">マイページ(デバッグ用)</a -->
下記のJavaScriptコメントでは、「debugを1に書き換えるとユーザーの情報を抜き取れそう」という情報をユーザーに与えてしまいます。
$.ajax({
data: {
'debug': 0, // 1にするとユーザーの全データを表示
'userId': userId,
}
コメントを残す際は、情報漏洩リスクを意識するように気をつけましょう!
9. セキュリティリスクを少なくするためのphp.iniの設定
php.iniは、PHPの設定を行うファイルです。
このphp.iniについて、本書では以下の通り設定することを推奨しています。
オプション名 | デフォルト値 | おすすめの設定値 |
---|---|---|
expose_php | On | Off |
allow_url_fopen | On | Off |
display_errors | On | 本番環境ではOff、開発環境ではOn |
open_basedir | 空欄 | /var/files/ など |
ここから、各項目について説明していきます。
expose_php
Onになっていると、HTTPレスポンスヘッダにPHPのバージョン情報が含まれるようになります。
PHPのバージョン情報は、外部に漏れないようにしましょう!
なぜかというと、PHPの特定バージョンに脆弱性があった場合に、悪意のあるユーザーにヒントを与えてしまうことになるからです。
特に不都合はないので、Offにしておきましょう。
allow_url_fopen
Onになっていると、ファイルを読み書きする関数(file_get_content関数など)にURLを指定できるようになります。
外部のWebサイトのファイルを読み込ませることができる、ということになってしまいます。
外部サーバーとのやりとりが必要な場合は、cURL関数などで実装しましょう。
原則として、allow_url_fopenはOffにしておきましょう。
display_errors
エラーメッセージにはさまざまな攻撃のヒントが含まれているので、本番環境ではOffにしましょう!
逆に、開発環境ではOnにした上で、error_reportingもE_ALLに設定しましょう。
本番環境ではエラーメッセージを表示せず、開発環境ではNoticeレベルでも積極的に表示すると良いでしょう。
open_basedir
このオプション値にディレクトリを指定することで、そのディレクトリ以下のファイルしかオープンできないようになります。
サーバー内の重要ファイルにアクセスされないよう、値を設定しておくと良いでしょう。
10. SQLインジェクション攻撃とその対策
SQLインジェクションとは、プログラマーがPHPに埋め込んだSQLを不正に使用し、データベース操作を行う攻撃方法です。
例えば、以下のプログラムがあったとします。
$sql = "SELECT
*
FROM
users
WHERE
login_id = '{$loginId}'
AND password = '{$password}'";
このプログラムは、誰でも簡単に他人のアカウントでログインできるという脆弱性があります。
どういうことでしょうか?
ログインIDとパスワードに、以下の値を設定してみましょう。
- ログインID :
dummy
- パスワード :
' or '1' = '1
生成されるSQLは以下の通りです。
SELECT
*
FROM
users
WHERE
login_id = 'dummy'
AND password = '' or '1' = '1'
注目すべきはWHERE句です。
or '1' = '1'の判定は常に正となります。
よってこのSQLは、「usersテーブルからすべてのレコードを取得する」という結果となってしまいます!
悪意のあるユーザーは、上記SQLを実行することで、1レコード目に見つかったユーザーになりすましてログインすることが可能です。
SQLインジェクションに対応するために、下記のようにプリペアードステートメントを使用しましょう!
$sql = "SELECT
*
FROM
users
WHERE
login_id = :login_id
AND password = :password";
$statement = $pdo->prepare($sql);
$statement->bindValue(':login_id', $loginId, PDO::PARAM_STR);
$statement->bindValue(':password', $password, PDO::PARAM_STR);
$statement->execute();
プリペアードステートメントを使用すると、あらかじめSQLをプリコンパイルしておき、後ほど値をバインドさせる処理が行われます。
プリコンパイルのおかげで、バインド値にどのような記号類が含まれていても、SQL構文を書き換えることはできなくなります!
11. オープンリダイレクトの脆弱性
オープンリダイレクトとは、どんなURLでもリダイレクトできてしまうプログラムのことです。
例えば下記のようなプログラムです。
// https:expample.com/login.php内の処理
if (ログイン成功したら) {
if ($_GET['page']が指定されていたら) {
header('Location: ' . $_GET['page']); // オープンリダイレクト
}
}
攻撃者がオープンリダイレクトを用いることで、罠サイトへと自動遷移させることができます
下記のリンクをスパムメールなどに埋め込み、ユーザーにクリックさせると、ログイン済みのユーザーを罠サイトへ自動遷移させることができます。
https://example.com/login.php?page=http://crack.com
上記の「http://crack.com
」が罠サイトです。
では、オープンリダイレクトにならないようにするにはどうしたらいいでしょうか?
答えは「GETパラメーターの値を厳密にバリデーションチェックする」ことです!
バリデーションチェックの例として、ホワイトリストを用意することが挙げられます。
$allowedUrls = [
'start-page.php',
'mypage.php',
];
if ($_GET['page']が指定されていたら) {
if (in_array($_GET['page'], $allowedUrls)) {
header('Location: ' . $_GET['page']); // 安全なページにのみ遷移させる
} else {
header('Location: start-page.php'); // 安全でない場合はデフォルトページに遷移させる
}
}
オープンリダイレクトにならないよう注意しましょう!
12. PHP_CodeSniffer
PHP_CodeSnifferは、PHPの外部ライブラリの一種です。
コードがコーディング規約に沿っているか、自動でチェック・整形してくれます!
実際に試してみました。
まずはPHP_CodeSnifferをインストールします。
今回は以下の記事を参考にさせていただきました。
(実行環境はM1 MacBook Proです)
% composer global require squizlabs/php_codesniffer
PHP_CodeSnifferをインストールしたら、下記コマンドでパスを通します。
% echo 'export PATH=$HOME/.composer/vendor/bin:$PATH' >> ~/.zshrc
続いて下記コマンドで設定を反映させます。
% source ~/.zshrc
では動作確認です。バージョンを表示してみましょう。
% phpcs --version
無事に動作確認できました!
PHP_CodeSniffer version 3.6.2 (stable) by Squiz (http://www.squiz.net)
下記のPHPファイルを使用して、挙動を試してみます。
<?php
// テストの関数
function echoStringWithEOL(string $string)
{
echo $string , PHP_EOL;
}
(わざと気持ち悪い書き方をしてみました)
では、コーディング規約(今回はPSR-12)に即しているかをチェックしてみます!
% phpcs --standard=PSR12 sample.php
結果は以下の通りです。
FILE: /path/to/sample.php
------------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 2 LINES
------------------------------------------------------------------------
1 | ERROR | [x] Header blocks must be separated by a single blank line
6 | ERROR | [x] Expected 1 blank line at end of file; 2 found
------------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
------------------------------------------------------------------------
Time: 29ms; Memory: 6MB
下記の2点でエラーが発生しました。
エラー内容
1行目: PHPタグの開始部分に空行を入れるべき(今回は0行)
6行目: ファイルの終了部分に1行の空行を入れるべき(今回は2行)
それでは、自動整形も試してみます。
phpcsコマンドと同時にインストールされるphpcbfコマンドを使用することで、自動整形ができます!
% phpcbf --standard=PSR12 sample.php
実行結果は以下の通りです。
PHPCBF RESULT SUMMARY
----------------------------------------------------------------------
FILE FIXED REMAINING
----------------------------------------------------------------------
/path/to/sample.php 2 0
----------------------------------------------------------------------
A TOTAL OF 2 ERRORS WERE FIXED IN 1 FILE
----------------------------------------------------------------------
Time: 31ms; Memory: 6MB
正常に整形が終了しました。
<?php
// テストの関数
function echoStringWithEOL(string $string)
{
echo $string , PHP_EOL;
}
ちゃんとコーディング規約通りになっていますね!
下記画像を参照いただくと、どういう風に自動整形されるのかお分かりいただけると思います。
これは便利そうですね!
もし機会があれば、実務で使用しているコードに対して自動整形をやってみたいところです。
おわりに
今回は、「実務4年目のWEBエンジニアが「PHP本格入門(下)」を読んで学んだこと 12選」というテーマでお伝えしました。
12選の内容は以下の通りです。
学んだこと 12選
- クラスの関係性を表すキーワード
- 神クラス(アンチパターン)
- なんでもpublicなクラス(アンチパターン)
- クラスに動的にアクセスする方法
- イテレータを使用した繰り返し処理
- ジェネレーター
- メソッドチェーンに対応したクラスの作り方
- HTMLおよびJavaScriptのコメントには注意する
- セキュリティリスクを少なくするためのphp.iniの設定
- SQLインジェクション攻撃とその対策
- オープンリダイレクトの脆弱性
- PHP_CodeSniffer
本書を読んだことで、PHPに関する理解が深まったと感じました!
もしも基礎からPHPを学びたいという方がいらっしゃいましたら、ぜひ本書を読んでみることをおすすめします!
ここまで読んでいただきありがとうございました。
それでは。