- 前提: PHP 5.6以上であること
- 狙い: Webサービスを構築できるまでの前提知識をつけること
基本的なリファレンス。ある程度他の言語での経験を積んでいることを期待する。式、評価、データ構造、関数、クラス、標準入出力などの概念についての解説はしない。PHPによって実際のサービスを作るのに必要な知識を身に付けることを期待している。この資料では使う機会の多い機能にのみ焦点を絞り、その利用例とともに紹介することにした。演習も用意したので、理解を深めるために解くことを勧める。また、あなたが他の言語において習熟しているトピックについては飛ばしても構わない。後学にも役立つものとするため、参考のリンクも随所に配置しておいた。各自参照しなさい。
PHPの学習及び開発においては、 http://php.net を一次情報として必ずあたること。言語仕様について不明瞭な点がある場合には以下を参考にしなさい。
PHP: 言語リファレンス - Manual http://php.net/manual/ja/langref.php
この資料でサポートしないのは以下の範囲だ。学ばなくて良いというわけではない。アプリケーションエンジニア向けのインターンなので内容を絞るという目的だ。個々のトピックについて気になる人は個別に聴いて欲しい。
- Webサーバへのデプロイについて
- エラー出力について
- キャッシュ / OPcacheについて
- ジェネレータ
- ORM
PHPにもインタラクティブシェルがある。 php -a
で起動可能だ。細かな関数の挙動を試すときなどに役立つだろう。 ((ただし --with-readline
オプション付きでPHPがコンパイルされている必要がある。))
$ php -a
Interactive shell
php > echo "s-tanno is god";
s-tanno is god
PHP: 対話シェル - Manual http://php.net/manual/ja/features.commandline.interactive.php
PHPではダブルクォートとシングルクォートで異なる振る舞いになる。
<?php
echo "Hi";
// Hi
$a = "kuke";
echo "hoge fuga $a";
// hoge fuga kuke
// こちらは展開されない。
echo 'hoge fuga $a';
// hoge fuga $a
// ヒアドキュメントも利用できる
$here = <<<EOF
this is
here
document!
$a
EOF;
echo $here;
// this is
// here
// document!
//
// kuke
制御文字を埋め込む場合には ""
で括る必要がある。
PHP: 文字列 - Manual http://php.net/manual/ja/language.types.string.php
<?php
echo "Hello world!";
// Hello world!
配列やオブジェクトのデバッグ用出力には var_dump()
が便利だ。keyとvalue、及び型の情報を出力してくれる。
<?php
$a = [1,2,3,4,5];
var_dump($a);
出力はこうなる。
array(5) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(5)
}
PHPにおいて配列は実際にはmapである。
PHP の配列は、実際には順番付けられたマップです。マップは型の一種で、 値をキーに関連付けます。 この型は、さまざまな使い道にあわせて最適化されます。 配列としてだけでなく、リスト (ベクター)、 ハッシュテーブル (マップの実装の一つ)、辞書、コレクション、スタック、 キュー等として使用することが可能です。
PHP: 配列 - Manual http://php.net/manual/ja/language.types.array.php
<?php
$a = [1,2,3];
print($a[0]);
// 1
print($a[2]);
// 3
// arrayはnestできる
$b = ["a"=>$a, "next"=>"hoge"];
print_r($b);
// Array
// (
// [a] => Array
// (
// [0] => 1
// [1] => 2
// [2] => 3
// )
//
// [next] => hoge
// )
// 上書き
$a[2] = 5;
print($a[2]);
// 5
// 配列から要素を取り除く
unset($a[1]);
print_r($a);
// Array
// (
// [0] => 1
// [2] => 5
// )
// 要素を追加する
$a[] = 3;
print_r($a);
// Array
// (
// [0] => 1
// [2] => 5
// [3] => 3
// )
PHPにおける関数は以下のスタイルになる。
function sum($i, $j) {
return $i + $j;
}
いくつかの特徴があるのであげておく。
- 返り値の型は指定されない
- 引数の型は必須ではない
- 指定することもできる。 http://php.net/manual/ja/language.oop5.typehinting.php
- タイプヒンティングと呼ばれる
- ただし
int
やstring
などのスカラー型を指定することができないなどの制限がある。- これは次期リリースのPHP 7.x以降サポート予定である。
- 指定することもできる。 http://php.net/manual/ja/language.oop5.typehinting.php
関数名の制限については以下のとおり。
関数名は、PHP の他のラベルと同じ規則に従います。関数名として有効な 形式は、まず文字かアンダースコアで始まり、その後に任意の数の文字・ 数字・あるいはアンダースコアが続くものです。正規表現で表すと、
[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
となります。
PHP: ユーザー定義関数 - Manual http://php.net/manual/ja/functions.user-defined.php
タイプヒンティングの代わりといっては何だが、初期値をデフォルト引数として与えることはよく見られる。ただし、引数に対する拘束力は持たないため、デフォルト引数と異なる型の引数を渡してもエラーにはならない。
function sum($ii = [0]) {
$ret = 0;
if (!is_array($ii)) {
return null;
}
foreach ($ii as $i) {
$ret += $i;
}
return $ret;
}
echo sum([0,1,2]);
// 3
echo sum(5);
// null: 実行時エラーにはならない
また、 ...$i
の形式で可変長引数を扱うこともできる。 $i
は配列として受け取ることができる。
function mul(...$integers) {
$acc = 1;
foreach ($integers as $i) {
$acc *= $i;
}
return $acc;
}
echo mul(1,2,3,4);
PHP: 関数の引数 - Manual http://php.net/manual/ja/functions.arguments.php
- フィボナッチ数列における任意のn+1項目の値を返す関数、
fib($n)
を書け。 - フィボナッチ数列を配列として返す
fib_array($n)
を書け。例えばfib_array(5)
は[1,1,2,3,5]
を返すものとする。 - ある自然数nの階乗を求める関数
fact($n)
を書け。 - ある文字列sが回文であるか否かをチェックする関数
isPalindrome($s)
を書け。返り値はbool(trueまたはfalse)とすること。
PHPには配列関連の標準関数が多い。よく利用するものを紹介しておく。
- PHP: array_map - Manual
- PHP: array_filter - Manual
- PHP: array_reduce - Manual
- PHP: array_values - Manual
- PHP: array_keys - Manual
それぞれ以下のように利用できる。
<?php
$a = range(1, 5);
array_map(function($i){
return $i + $i;
}, $a);
// Array
// (
// [0] => 2
// [1] => 4
// [2] => 6
// [3] => 8
// [4] => 10
// )
array_reduce($a, function($carry, $next){
return $carry + $next;
});
// 15 = 1 + 2 + ...
array_reduce($a, function($carry, $next){
return $carry * $next;
}, 1);
// 120 = 1 * 2 * ...
array_filter($a, function($i){
return $i % 2 === 0;
});
// Array
// (
// [1] => 2
// [3] => 4
// )
array_values(["a"=>1, "b"=>2, "c"=>3]);
// [1,2,3]
array_keys(["a"=>1, "b"=>2, "c"=>3]);
// ["a","b","c"]
配列操作に関する全ての関数は以下にまとまっている。必要に応じて参考にすること。
PHP: 配列 関数 - Manual http://php.net/manual/ja/ref.array.php
array_map()
を利用し配列の各項の値を2乗にする関数、array_square
を書け。例えば入力[1,2,3]
に対して[1,4,9]
を出力すること。- あるリストを反転する関数、
reverse()
を書け。例えばreverse([1,2,3])
は[3,2,1]
を返すこと。 - 上で作成した関数と http://php.net/manual/ja/function.array-reverse.php を比較せよ。また
array_reverse()
と同様の振る舞いになるように変更せよ。 - ある配列の全ての要素を足し合わせる関数
my_array_sum()
を書け。 http://php.net/manual/ja/function.array-sum.php と仕様を合わせること。 $a[] = $n;
のシンタックスを利用し、配列に複数要素を追加する関数my_array_push()
を書け。 http://php.net/manual/ja/function.array-push.php と仕様を合わせること。- フィボナッチ数列の各項をその平方根とした数列の各項を出力する関数、
fib_square($n)
を書け。 - ある文字列sが回文であるか否かをチェックする関数
isPalindrome($s)
について、複数の文字列を渡せるようにしたい。文字列の配列ssを渡せるようにし、isPalindrome($ss)
を定義せよ。渡された配列にある文字列が全て回文の場合のみtrueを返し、それ以外の場合はfalseを返すものとする。
tl;dr; ===
を使うこと
<?php
$c = function($r) {
echo $r ? "true\n" : "false\n";
};
$c(1 == 1.0); // true
$c(1 === 1.0); // false
$c("" == 0); // true
$c("" === 0); // false
$c(null == 0); // true
$c(null === 0); // false
$c(true == 1); // true
$c(true === 1); // false
==
は型の相互変換をした上で比較する。この挙動はバグを仕込みやすい。そのため ===
をつかうようにすること。不等号についても同様。
PHP: 比較演算子 - Manual http://php.net/manual/ja/language.operators.comparison.php
ループについては for
, foreach
, while
, do-while
の制御構造でサポートされている。
<?php
$a = range(1, 10);
foreach ($a as $v) {
printf("%s\n", $v);
}
for ($i = 0; $i < count($a); $i++) {
printf("%s\n", $a[$i]);
}
$i = 0;
while ($i < count($a)) {
printf("%s\n", $a[$i]);
$i++;
}
// 全て同様に以下を出力
//
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
foreach
についてはkeyとvalueをそれぞれ取得することができる。
foreach (["a"=>1, "b"=>2, "c"=>3] as $k=>$v) {
printf("key :%s, value: %s\n", $k, $v);
}
// key :a, value: 1
// key :b, value: 2
// key :c, value: 3
再帰をつかうこともできる。
<?php
function sum_of_integers($i) {
if ($i === 1) {
return 1;
}
return $i + sum_of_integers($i - 1);
}
echo sum_of_integers(10);
// 55
- 階乗を計算する関数
fact($i)
を再帰をつかって書け。
配列操作のところで利用していたが、PHPでも無名関数を利用できる。
PHP: 無名関数 - Manual http://php.net/manual/ja/functions.anonymous.php
例えば array_map()
に対してコールバックのためのパラメータとして渡す例などを見てきた。ここではクロージャについて説明する。
<?php
$a = "s-tanno";
$v = function() {
return sprintf("%s is god", $a);
};
echo $v();
// PHP Notice: Undefined variable: a in /private/tmp/c.php on line 5
$s = function() use ($a) {
return sprintf("%s is god", $a);
};
echo $s();
use
を使うことによって、無名関数のスコープの外にある変数を束縛することができる。値はコピーされて渡されることに注意すること。
- フィボナッチ数列のn-1項を出力する
fib($n)
を無名関数を再帰させることによって書き換えなさい。
$fib = function($n) use (&$fib) {
// STUB
}
- 上記の方法による定義と、元々の再帰を利用した
fib($n)
の実装についての違いを述べなさい。
まず実際の例を見て欲しい。
<?php
class Page
{
public $name = "hoge";
public function set($n)
{
$this->name = $n;
}
public function get()
{
return $this->name;
}
}
$a = new Page();
echo $a->get();
// hoge
$a->set("s-tanno is god.");
echo $a->get();
// s-tanno is god.
PHPでもクラスを利用できる。 $name
がプロパティで、 get()
set()
がそれぞれ関数だ。
また、トレイトを利用できる。Scalaのトレイトに近い。
trait Hi
{
public function greed()
{
echo "Hi";
}
}
class B
{
use Hi;
}
$b = new B();
$b->greed();
// Hi
コードを再利用する目的なら、継承ではなくトレイトを利用すべきだ。ただし、トレイトの副作用についても知らなければいけない。
// TODO staticなメンバの利用、トレイトをまたいだ依存関係をつかわないこと、などについて触れておく。
PHP: トレイト - Manual http://php.net/manual/ja/language.oop5.traits.php
PHP: クラスとオブジェクト - Manual http://php.net/manual/ja/language.oop5.php
PHPのクラスでも継承を扱うことができる。例をみてみよう。
<?php
class Base
{
public function display()
{
echo "this is base\n";
}
}
class A extends Base
{
// 親クラスの関数をオーバーライドしている
public function display()
{
echo "extended.\n";
// 親クラスの関数を呼び出すことができる
parent::display();
}
}
$a = new A();
$a->display();
// extended.
// this is base
- トレイトと継承についてそのメリットとデメリットを述べよ
- GroupWorkBase がどのようにオブジェクトの依存関係を管理しているか確認せよ
名前空間はクラス、関数といったファイル内の項目をカプセル化するものだ。例をみるとわかりやすい。
Java / Scala / Goなどの言語を触ったことがある人にはわかりやすいだろう。Rubyでいうとモジュールである。
<?php
namespace training;
class Student{}
function display() {
echo "let's training.";
}
// グローバルな名前空間、sin()と同じ名前の関数を定義
// これは \training\sin となる
function sin() {
echo "this is local sin()";
}
$s = new Student;
$s = new \training\Student;
$d = \training\display();
$d;
// 名前空間宣言されたファイルの中では名前空間無しで呼べる
display();
echo sin(1);
// this is local sin()
// 明示的にグローバルな名前空間から呼び出すには `\` をつける
echo \sin(1);
// 0.8414709848079
名前空間を利用すると、当該ファイルの外に定義されているクラスや関数の呼び出しがわかりやすくなる。名前空間が利用されていないコードでは、 require_once
や include_once
などを利用し、ファイル自体を呼び出す形式がよく利用されていた。大きなプロジェクトになるとファイルベースでの呼び出しは管理がしづらくなる。そのため、今は名前空間を利用するようにしたほうがよい。
ただし、名前空間を利用していたとしても、その定義元のファイルがどこに配置されているかがわからないと関数やクラスの呼び出しを行うことはできない。そのため、名前空間はPSRに準拠させた形式で配置し、composerと合わせて利用されることが一般的になっている。
PHP: 名前空間 - Manual http://php.net/manual/ja/language.namespaces.php
PHP-FIG — PHP Framework Interop Group http://www.php-fig.org/
- GroupWorkBase で名前空間が利用されている箇所を探し、各コンポーネントがどのように呼び出されているかを確認せよ
- PSR-4に準拠すること
// TODO GroupWorkBaseを例にcomposer.jsonについて紹介する
composerはPHPにおける依存関係管理の仕組みだ。bundler, npm, cartonのようなものだ。ここを読んで欲しい。
http://ja.phptherightway.com/#dependency_management
DBへの接続はPDOをつかう。まずは例をみてほしい。
<?php
$dbh = new PDO("sqlite::memory:", "user", "password");
$dbh->exec("create table hoge(id string, name string)");
$stmt = $dbh->prepare("insert into hoge(id, name) values(:id, :name)");
$stmt->bindValue(':id', "abc");
$stmt->bindValue(':name', "suzuken");
$ret = $stmt->execute();
if ($ret) {
echo "successfully inserted.\n";
} else {
echo "insertion failed.\n";
}
foreach ($dbh->query("select * from hoge") as $row) {
printf("id: %s\n", $row['id']);
printf("name: %s\n", $row['name']);
}
$up = $dbh->prepare("update hoge set name = :name where id = :id");
$up->bindValue(':name', "s-tanno");
$up->bindValue(':id', "abc");
if ($up->execute()) {
echo "successfully updated.\n";
}
if ($dbh->exec("insert into hoge(id, name) values('abcd', 'kuke')")) {
echo "successfully inserted.\n";
};
$q = $dbh->query("select * from hoge");
while ($row = $q->fetch()){
printf("id: %s\n", $row[0]);
printf("name: %s\n", $row[1]);
}
if($dbh->exec("delete from hoge where id = 'abc'") === 1) {
echo "successfully deleted.\n";
};
$q = $dbh->query("select count(*) from hoge");
$cnt = $q->fetchAll();
var_dump($cnt);
$dbh->exec("drop table hoge");
// successfully inserted.
// id: abc
// name: suzuken
// successfully updated.
// successfully inserted.
// id: abc
// name: s-tanno
// id: abcd
// name: kuke
// successfully deleted.
// array(1) {
// [0]=>
// array(2) {
// ["count(*)"]=>
// string(1) "1"
// [0]=>
// string(1) "1"
// }
// }
データベースへの接続を取り扱うのがPDOオブジェクトだ。PDOは通常取り扱われるリレーショナルデータベースであれば大抵接続できる。認証のオプションなどについては以下を参考すること。
PHP: 接続、および接続の管理 - Manual http://php.net/manual/ja/pdo.connections.php
PDOでのデータベース操作は他の言語と同様だ。基本的にパラメータを埋め込む場合にはプリペアドステートメントを使うこと。値の束縛を扱いやすくしてくれるだけでなく、SQLインジェクション対策としても有効である。PDOではデータベースがネイティブでプリペアドステートメントをサポートしていなくても、PHP側でエミューレートするようになっている。
PHP: プリペアドステートメントおよびストアドプロシージャ - Manual http://php.net/manual/ja/pdo.prepared-statements.php
各手続きについての戻り値についてはPHPマニュアルを参照すること。特に挿入や削除が失敗していないか、テーブルの管理に関するコマンドが失敗していないかどうかをハンドリングすることは実際のアプリケーションの大きな関心ごととなる。
PHP: PDO - Manual http://php.net/manual/ja/book.pdo.php
例外についても他の言語と似ている。
<?php
function plus($m, $n) {
if (!is_int($m) || !is_int($n)) {
throw new Exception("整数以外は受け付けません!");
}
return $m + $n;
}
plus(1, 1); // -> 2
plus("hoge", 1); // -> Exception
// PHP Fatal error: Uncaught exception 'Exception' with message '整数以外は受け付けません!' in /private/tmp/e.php:4
// Stack trace:
// #0 /private/tmp/e.php(10): plus('hoge', 1)
// #1 {main}
// thrown in /private/tmp/e.php on line 4
try {
plus("hoge", 3);
} catch (Exception $e) {
echo "例外をキャッチしました\n";
echo $e->getMessage();
}
// 例外をキャッチしました
// 整数以外は受け付けません!
throw new Exception()
によって任意の例外を返すことができる。例外を捕捉するには try
catch
を使う。 catch
については例外のクラスごとに catch
できる。
<?php
// 独自に定義した例外クラス
class EmptyException extends Exception {}
function gen($dsn) {
if ($dsn === "") {
throw new EmptyException("oh, it's empty dsn string.");
}
return new PDO($dsn);
}
$pdo;
try {
$pdo = gen("sqlite::memory:");
} catch (PDOException $e) {
echo $e->getMessage();
}
// nothing
try {
$pdo = gen("");
} catch (EmptyException $e) {
echo $e->getMessage();
} catch (PDOException $e) {
echo $e->getMessage();
}
// oh, it's empty dsn string.
try {
$pdo = gen("invaliiiiiiiiiiiiiiiiieddsnnnnnnnnnnn");
} catch (EmptyException $e) {
echo $e->getMessage();
} catch (PDOException $e) {
echo $e->getMessage();
}
// invalid data source name
- フィボナッチ数列における任意のn+1項目の値を返す関数
fib($n)
について、引数に自然数及び0が渡されなかった場合に例外を返すようにせよ。
基本的にビルトインWebサーバを利用するといい。
$ php -S localhost:8000
これによりtcp:8000でlistenするhttp serverを起動することができる。ローカルでの開発でhttpdやnginxなどを準備しなくてもよいので手軽である。 httpd.conf
などは使えないので、rewriteなどが必要な場合には httpd
を利用してテストすること。
http://localhost:8000/
にGETリクエストするとfizzbuzzを順番に返すWebサーバを書け。以下のようにテストされることを期待する。
$ curl http://localhost:8000/
1
$ curl http://localhost:8000/
2
$ curl http://localhost:8000/
fizz
$ curl http://localhost:8000/
4
$ curl http://localhost:8000/
buzz
...
外部、もしくは内部のHTTP APIをサーバから利用したいケースがあるだろう。その場合にはcurlを使うと良い。PHPのcurlモジュールを利用することができる。
// TODO curlでのGET, POSTの利用の仕方
特に固定された要件がないなら、標準の password_hash
及び password_verify
を使うこと。
ソルトを自分で設定することもできるが、デフォルトでランダムなソルトを生成してくれる。ストレッチングの時間は cost
によって変わるので、ベンチマークをとって適切なcostを設定するとよい。
<?php
$hash = password_hash('cool', PASSWORD_DEFAULT);
password_verify('not cool', $hash);
// unmatch
password_verify('cool', $hash);
// match
当然、生のパスワードをDBに保存するようなことはしてはいけない。
参考: https://phpbestpractices.org/#passwords
テストの技法はソフトウェア開発において一般的なものである。PHPにおいてもテストは重要な方法だ。標準ライブラリにはテスト用のパッケージは用意されていない。PHPUnitが広く使われている。
PHPUnit – The PHP Testing Framework https://phpunit.de/
TBD: TODO
- assertSameを使うこと
- assertTrue
- あまり多くのapiを使おうとしなくてよい。読みづらくなるから。
PHPは5.3以降に大きく仕様が変化している。そのため、できるだけ新しいドキュメントをあたることが好ましい。PHP: The Right Wayは現代の開発における必要な事項(パッケージ管理、依存性の注入、テスト、セキュリティ・・・etc.)に関する「今のやり方」がまとまっている。
PHP: The Right Way http://ja.phptherightway.com/
PHP Best Practices: a short, practical guide for common and confusing PHP tasks https://phpbestpractices.org/
ところどころTODOというのがあるのであとで加筆したかも