@blog.justoneplanet.info

日々勉強

PHP Design Pattern(PHPによるデザインパターン)

■シングルトンパターン(Singleton Pattern)

シングルトンパターンとは、ある特定のクラスのインスタンスが特定の数しか生成されない事を保証するパターンである。

<?php
class Db {
    protected static $dbh;
    protected static $instance;
    protected __construct(){
        self::$dbh = new PDO($dsn, DB_USER, DB_PASS);
    }
    public static function getDbh(){
        if(!$dbh){
            self::$instance = new self();
        }
        return self::$dbh;
    }
}
$dbh = Db::getDbh();
?>

但し、上述の例はいささか不完全でもある。何故ならば$dbhがDbクラスのインスタンスでない限り、クローン化はどうしても防げないからだ。

<?php
final class ForbiddenClonePDO extends PDO {
    final public function __clone(){
        throw new RuntimeException();
    }
}
class Db {
    protected static $dbh;
    protected static $instance;
    protected __construct(){
        try{
            $dsn = 'mysql:host=localhost;dbname=dbname';
            self::$dbh = new ForbiddenClonePDO($dsn, DB_USER, DB_PASS);
            self::$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
            self::$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        cacth(PDOException $e){
            //code
        }
    }
    public static function getDbh(){
        if(!$dbh){
            self::$instance = new self();
        }
        return self::$dbh;
    }
}
$dbh = Db::getDbh();
?>

■ファクトリーパターン(Factory Pattern)

似たようなタスクを違った手法で行う

<?php
class Configuration {
    const STORE_INI = 1;
    const STORE_DB = 2;
    const STORE_XML = 3;
    public static function getStore($type = self::STORE_XML){
        switch($type){
            case self::STORE_INI:
                return new Configuration_Ini();
            case self::STORE_DB:
                return new Configuration_Db();
            case self::STORE_XML:
                return new Configuration_Xml();
            default:
                throw new Exception();
        }
    }
}
class Configuration_Ini {
    //code
}
class Configuration_Db {
    //code
}
class Configuration_Xml {
    //code
}
$config = Configuration::getStore(Configuration::STORE_XML);
?>

■レジストリパターン(Registry Pattern)

<?php
class Registry {
    private static $_register;
    public static function add(&$item, $name = null){
        if(is_object($item) && is_null($name)){
            $name = getClass($item);
        }
        elseif(is_null($name)){
            throw new Exception();
        }
        $name = strtolower($name);
        self::$_register[$name] = $item;
    }
    public static function &get($name){
        $name = strtolower($name);
        if(array_key_exists($name, self::$_register)){
            return self::$_register[$name];
        }
        else{
            throw new Exception();
        }
    }
    public static function exists($name){
        $name = strtolower($name);
        if(array_key_exists($name, self::$register)){
            return true;
        }
        else{
            return false;
        }
    }
}

$db = new DB();
Registry::add($db);

if(Registry::exists('DB')){
    $db = Registry::get('DB');
}
else{
    die();
}
?>

Google Chrome

CMをTVでやるらしい。一足先にYouTubeで公開されているものをご紹介。

うーん。シンプルで面白い。

PHP MySQL Improved Extension(mysqli)

mysqli拡張サポートによって、MySQL4.1以降で使える機能を利用できる。但し、個人的には全くこの手の関数を使わない。何故ならばMySQLに特化した関数であり、仮に他のDBに移行する場合、保守のコストがとっても高くなってしまうと考えているからだ。それにラーニングコストもかかるし、PDOのメソッド名とmysqliの関数名がごっちゃになる。

■mysqliを使ったDBへの接続

<?php
$dbh = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if(mysqli_connect_errorno()){
    //code for error
}
mysqli->close();
?>

mysqliは例外をthrowしない。

<?php
$dbh = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if(!$dbh){
    //code for error
}
mysqli_close($dbh);
?>

■mysqliを使ったDBへのクエリ発行

<?php
$name = mysqli->real_escape_string($_POST['name']);
$sql = "SELECT `id`, `name` FROM `person` WHERE `name` = '$name'";
if(!$dbh->real_query($sql)){
    //print($dbh->error);
}
if($result = mysql->store_result()){
    while($row = $result->fetch_assoc()){
        print($row['id'] . ': ' . $row['name']);
    }
    $result->close();
}
?>

$resultもcloseしなきゃならんとは。。。めんどい。。。

<?php
$name = mysqli_real_escape_string($_POST['name']);
$sql = "SELECT `id`, `name` FROM `person` WHERE `name` = '$name'";
if(mysqli_real_query($dbh, $sql)){
    //print(mysqli_error());
}
if($result = mysql_store_result($dbh)){
    while($row = $result_fetch_assoc($result)){
        print($row['id'] . ': ' . $row['name']);
    }
    mysqli_free_result($result);
}
?>

うーむ。実に不愉快だ。やはりmysqliは性に合わん!

■mysqliを使ったプリペアドステートメント

<?php
$sql = "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES(?, ?, ?)";
if($stmt = $dbh->prepare($sql)){
    $stmt->bindParam('s', $id, $_POST['name'], $_POST['country_id']);
    $stmt->execute();
    $stmt->bind_result($result_id, $result_name);
    while($stmt->fetch()){
        print($result_id . ': ' . $result_name);
    }
    $stmt->close();
}
?>

これじゃ、ソースを書きたくならないな。

<?php
$sql = "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES(?, ?, ?)";
if($stmt = mysqli_prepare($dbh, $sql)){
    mysqli_stmt_bind_param($stmt, 's', $id, $_POST['name'], $_POST['country_id']);
    mysqli_stmt_execute()$stmt;
    mysqli_stmt_bind_result($dbh, $result_id, $result_name);
    while(mysqli_stmt_fetch()){
        print($result_id . ': ' . $result_name);
    }
    mysqli_stmt_close($stmt);
}
?>

■mysqliを使ったトランザクション

<?php
$dbh->autocommit(false);
$dbh->query("INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('4', 'Jack', '2')");
$dbh->query("INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('5', 'Emily', '1')");
if(!$dbh->commit()){
    $dbh->rollback();
}
?>
<?php
mysqli_autocommiti($dbh, false);
mysqli_query($dbh, "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('4', 'Jack', '2')");
mysqli_query($dbh, "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('5', 'Emily', '1')");
if(!mysqli_commit($dbh)){
    mysqli_rollback($dbh);
}
?>

mysqliの特徴

「手続き型」と「オブジェクト指向型」の記述が可能である。

サーバによってはインストールされていない事もしばしばあるようだ。ますます使わんぞ、コリャ=3・・・

命名

余計なお世話型関数。

PHP Data Object(PDO)

PDOには様々なDBに対する各々のドライバがある。一旦、インストールすれば(PDOを使用してDBへ接続している場合、)コードを殆ど書き換えることなく、DBを変更することが可能になる。

■PDOを使ったデータベースへの接続

<?php
$dsn = 'mysql:host=localhost;dbname=dbname';
$dbh = new PDO($dsn, DB_USER, DB_PASS);
?>

通常、PDOはエラーが発生しても何も表示しない(PDO::ERRMODE_SILENT)。これはセキュリティ上安全ではあるが、一方でデバッグを非常に困難にする。従って、例外を発生させ(ファイルに書き込みさせ)るようにすることも可能である。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=dbname';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(Exception $e){
    //code
}
?>

■PDOを使ったデータベースへのクエリ発行

SELECT文

SELECT文のような、結果セットを得ることを主目的としている場合は、以下のようにqueryメソッドを用いる。また、queryメソッドを用いた場合、戻り値はPDOStatementオブジェクトである。

<?php
$sql = "SELECT * FROM `person` WHERE `name` = 'John'";
$result = $dbh->query($sql);
foreach($result as $row){
    print($row['name'] . ': ' . $row['country_id']);
}
?>

但し、以下のように書いた場合、$rowには「数字キー」と「カラム名のキー」が両方含まれているので値が重複する。これはデフォルトのデータ取り出しモードがPDO::FETCH_BOTHであり、「数字キー」と「カラム名のキー」での配列になっているためである。

<?php
$sql = "SELECT * FROM `person` WHERE `name` = 'John'";
$result = $dbh->query($sql);
foreach($result as $row){
    foreach($row as $value){
        print($value . PHP_EOL);
    }
}
/*
1
1
John
John
2
2
*/
?>
データ取り出しモードの変更

データ取り出しモードを変更するには、以下のように結果セットが含まれるPDOStatementオブジェクト$resultに対して、setFetchModeメソッドを実行せねばならない。

<?php
$sql = "SELECT * FROM `person` WHERE `name` = 'John'";
$result = $dbh->query($sql);
$result->setFetchMode(PDO::FETCH_OBJ);
foreach($result as $row){
    print($row->name . ': ' . $row->country_id);
}
?>
ユーザ入力の値をSQL文に組み込むためのメソッド

通常、SQLインジェクションを回避するために、ユーザ入力の値をSQL文に含める際にはサニタイジングしなければならない。以下のように、quoteメソッドを用いることで可能である。

<?php
$name = $dbh->quote($_POST['name']);
$sql = "SELECT * FROM `person` WHERE `name` = '$name'";
$result = $dbh->query($sql);
?>

但し、いくつかのドライバはこのメソッドを実装していないので、後述のプリペアドステートメントを用いたほうが良い。

INSERT文、UPDATE文、DELETE文

INSERT文、UPDATE文、DELETE文には変更(処理)した行数を返すexecメソッドを用いる。

<?php
$sql = "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('4', 'Jack', '2')";
$num = $dbh->exec($sql);
print($num);//1
?>

■プリペアドステートメント(Prepared Statement)を用いたクエリの発行

プリペアドステートメントとは

  • 1回のリクエストに対する処理の中で、SQL文の再利用を可能にする
  • DBがプリペアドステートメントをサポートしてない場合、PDOが内部の関数で可能にする
  • DBがプリペアドステートメントをサポートしている場合、DBの機能を使ってアプリケーションの性能の向上を可能にする
  • (SQLインジェクションを誘発させる)クォート漏れに対するリスク回避を可能にする
<?php
$sql = "INSERT INTO `person`(`id`, `name`, `country_id`) VALUES(?, ?, ?)";
$stmt = $dbh->prepare($sql);
$stmt->execute(array(
    $id,
    $_POST['name'],
    $_POST['country_id']
));
?>

INSERT文、UPDATE文、DELETE文には、ユーザ入力の値をSQL文に含めることが非常に多い。とはいえ、以下のようにSELECT文でも当然ながら、プリペアドステートメントを用いることはある。

<?php
$sql = "SELECT * FROM `person` WHERE `name` = ?";
$stmt = $dbh->prepare($sql);
$stmt->execute(array('John'));
//$stmt->setFetchMode(PDO::FETCH_OBJ);
$result = $stmt->fetchAll();
var_dump($result);
/*
array(1) {
  [0]=>
  array(6) {
    ["id"]=>
    string(1) "1"
    [0]=>
    string(1) "1"
    ["name"]=>
    string(4) "John"
    [1]=>
    string(4) "John"
    ["country_id"]=>
    string(1) "2"
    [2]=>
    string(1) "2"
  }
}
*/
?>

但し、PHPのバージョンによって、(MySQL独自拡張の)LIMIT句に対するプリペアドステートメントにバグがあるため、それ(LIMIT句)以外での使用が望ましいと個人的には思う。また、LIMIT句は数値の値しか許容しないため、PHPで(型変換などを使って)サニタイジングすることは比較的に容易である。さらに、PDOStatementクラスに(結果を1行だけ返す)fetchメソッドが「あるバージョン」と「ないバージョン」があるため、このメソッドの使用はなるべく避けたほうが良い。

■PDOを使ったトランザクションの実行

以下のようにして、トランザクションを利用する。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=dbname';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $dbh->beginTransaction();
    $dbh->exec("INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('4', 'Jack', '2')");
    $dbh->exec("INSERT INTO `person`(`id`, `name`, `country_id`) VALUES('5', 'Emily', '1')");
    $dbh->commit();
}
catch(Exception $e){
    $dbh->rollBack();
}
?>

各メソッドについて

beginTransaction
トランザクションを開始する
commit
確定する
rollBack
破棄して最初の状態に戻す

トランザクションとは

「全て成功する」か「全て失敗する」かのどちらかしか存在しないことを保証する機能。この機能が必要な例として、たいていの参考書では「銀行の振込み」などが扱われる。

SQL Joins(SQLにおけるテーブル結合)

テーブル結合とは、複数のテーブルから一つのレコードセットをつくり出す。多くのアプリケーションは非常に複雑なテーブル結合を使っている。

サンプルテーブル

personテーブル
id name country_id
1 John 2
2 Mike 1
3 Nick 99
countryテーブル
id name
1 USA
2 Japan
3 Canada

■内部結合(inner join)

内部結合とは、一歩のテーブルに存在するcountry.idが、もう一方のテーブルに含まれていないとき、行の結合が失敗し結果セットから除外される結合である。

SELECT
    `person`.`id`,
    `person`.`name`,
    `country`.`name`
FROM
    `person`
INNER JOIN
    `country`
ON
    `person`.`country_id` = `country`.`id`;

ONキーワードで結合条件「`person`.`country_id` = `country`.`id`」と指定するとことができる。上述の場合、「INNER JOIN」でなく「JOIN」でも結果は等しいが、可読性の問題などが生じるため、結合の種類はSQL文に明記することをお勧めする。

実行結果

id name name
1 John Japan
2 Mike USA

■左外部結合(left outer join)

外部結合とは、どちらか一方のテーブルに存在する行が、結果セットに含まれる結合である。「left outer join」はFROM節の左側のテーブル(person)に合わせて結合する。以下のようにすると左外部結合ができる。

SELECT
    `person`.`id`,
    `person`.`name`,
    `country`.`name`
FROM
    `person`
LEFT OUTER JOIN
    `country`
ON
    `person`.`country_id` = `country`.`id`;

実行結果

id name name
1 John Japan
2 Mike USA
3 Nick NULL

■右外部結合(right outer join)

「right outer join」はテーブル(country)に合わせて結合する。以下のようにすると右外部結合ができる。

SELECT
    `person`.`id`,
    `person`.`name`,
    `country`.`name`
FROM
    `person`
RIGHT OUTER JOIN
    `country`
ON
    `person`.`country_id` = `country`.`id`;

実行結果

id name name
2 Mike USA
1 John Japan
NULL NULL Canada

LEFTキーワードとRIGHTキーワードはどちらのデータに空白部分が含まれていても良いかを決めるワードである。

Relational Database and SQL(RDBとSQL)

■データベースの作成

以下のように、「CREATE DATABASE」文や「CREATE SCHEMA」文を使用する。

CREATE DATABASE <dbname>;
CREATE SCHEMA <dbname>;

■テーブルの作成

以下のように、「CREATE TABLE」文を使ってテーブルを作成する。

CREATE TABLE <tablename> (
    <colname> <coltype> [<colattributes>],
    [...
    <colname> <coltype> [<colattributes>]]
);

例えば以下のようになる。

CREATE TABLE book (
    id INT NOT NULL PRIMARY KEY,
    isbn VARCHAR(13),
    title VARCHAR(255),
    author VARCHAR(255),
    publisher VARCHAR(255)
)

■インデックスの作成

CREATE INDEX <indexname> ON <tablename> (<column>[, ... <column>]);

例えば以下のようになる。

CREATE INDEX book_isbn ON book (isbn);

但し、一般的には以下のようにテーブルの作成と同時に行うことが多い。

CREATE TABLE `book` (
    `id` int(11) NOT NULL auto_increment,
    `isbn` varchar(13) default NULL,
    `title` varchar(255) default NULL,
    `author` varchar(255) default NULL,
    `publisher` varchar(255) default NULL,
    PRIMARY KEY  (`id`),
    KEY `book_isbn` (`isbn`)
);

■データベースの削除

DROP SCHEMA <dbname>;

■テーブルの削除

DROP TABLE <tablename>;

■データの操作

追加

データを追加するには、以下のようにINSERT( INTO)文を使用する。テーブルのカラム名は運用上のメリットが非常に大きいため、記述することを強くお勧めする(カラムの追加に対するSQL文の耐性)。

INSERT INTO <tablename>(<colname> [, ... , <colname>]) VALUES(<value> [, ... , <value>]);

更新

データを更新するためには、以下のようにUPDATE(~SET~)文を使用する。但し、以下のコードは全レコードが特定の値に上書きされてしまう。

UPDATE <tablename> SET <colname> = <value> [, ... , <colname> = <value>];

通常は、以下のようにWHERE節などと組み合わせて使用し、特定のレコードのみを更新する。

UPDATE `address_book` SET `name` = 'Mike' WHERE `id` = '1';

削除

データを削除するには、以下のようにDELETE文を使う。但し、以下のコードは全レコードが削除される。

DELETE  FROM <tablename>;

通常は以下のようにWHERE節などと組み合わせて使用する。

DELETE  FROM `address_book` WHERE `name` = 'John';

■データの表示

データを表示するには、以下のようにSELECT文を使用する。

SELECT * FROM `address_book`;

通常、アプリケーションで全件のデータが必要なときというのは稀である。そして全件をSQLから抽出すると、サーバのメモリを圧迫することになる。従って、以下のようにWHERE節などと組み合わせて使うことが多い。

SELECT `tel` FROM `address_book` WHERE `name` = 'John';

WHERE節について

WHERE節では、条件をANDやORで複数指定できる。また、LIKE演算子を使用し大文字/小文字を区別しないようにしたり、「%(任意の複数文字)」や「_(任意の一文字)」の記号を使用して検索できる。

SELECT `tel` FROM `address_book` WHERE (`name` = 'John') OR (`name` LIKE '%Joh%' AND `age` = '27')';

正規表現と記法が異なるのが、個人的にはシックリこない。

インデックス

日本語に直訳すると「索引」。リソース内の特定の項目を検索するためのメカニズム。一般的にはアプリケーションがWHERE節などで検索に使用するカラムに対して指定される。

  • 「インデックス」はデータベースサーバがテーブルの行を特定するために使われる
  • 「インデックス」は特殊なテーブルである
  • 「インデックス」は行を特定するために使用される列と、行が物理的に配置されている場所を示す情報のみを含んでいる

主キー(primary key)

一組のデータレコードを一意に識別するための特殊なインデックス

テーブルの関係性

テーブルの関係性として「1:1」と「1:複数」の場合は簡単に処理できるが、「複数:複数」の場合はintermediate tableなどを使用しなくてはならない。

1:1
子テーブルの1レコードが、親テーブルの1レコードと関連する
1:複数
子テーブルの複数レコードが、親テーブルの1レコードと関連する
複数:複数
子テーブルの複数レコードが、親テーブルの複数レコードと関連する

関係性を意識したテーブルの作成

1:1

以下のように、primary keyを共通にしてみると良い。

CREATE TABLE `book` (
    `id` int(11) NOT NULL,
    `isbn` varchar(13) default NULL,
    `title` varchar(255) default NULL,
    `author` varchar(255) default NULL,
    `publisher` varchar(255) default NULL,
    PRIMARY KEY  (`id`),
    KEY `book_isbn` (`isbn`)
);
CREATE TABLE `book_author` (
    `id` int(11) NOT NULL PRIMARY KEY,
    `author_id` int(11) NOT NULL
);
1:多

以下のように、REFERENCESを使用してbookテーブルを参照するようにする。

CREATE TABLE `book` (
    `id` int(11) NOT NULL auto_increment,
    `isbn` varchar(13) default NULL,
    `title` varchar(255) default NULL,
    `author` varchar(255) default NULL,
    `publisher` varchar(255) default NULL,
    PRIMARY KEY  (`id`),
    KEY `book_isbn` (`isbn`)
);
CREATE TABLE `book_chapter` (
    `id` int(11) NOT NULL PRIMARY KEY auto_increment,
    `isbn` varchar(13) REFERENCES `book`(`isbn`),
    `chapter_number` INT NOT NULL,
    `chapter_title` varchar(255)
);

PHP Reflection API

PHP5には完全なリフレクションAPIが付属していて、クラス、インターフェイス、関数、メソッドなどについて、リバースエンジニアリングを行うことができる。

■定義済みクラスについて調べる

定義済みクラスについて調べるには、以下のようにReflectionClassクラスを使う。

<?php
/**
 * Dog
 */
class Dog {
    private $name;
    public function __construct($name){
        $this->name = $name;
    }
    /**
     * bark
     * @param
     * @return
     */
    public function bark(){
        print('Bow!');
    }
}
$class = new ReflectionClass('Dog');
print($class->getName());//Dog
print($class->getDocComment());
/**
 * Dog
 */
print($class->getFileName());
print($class->getStartLine());

$methods = $class->getMethods();
var_dump($methods);
/*
array(2) {
  [0]=>
  &object(ReflectionMethod)#2 (2) {
    ["name"]=>
    string(11) "__construct"
    ["class"]=>
    string(3) "Dog"
  }
  [1]=>
  &object(ReflectionMethod)#3 (2) {
    ["name"]=>
    string(4) "bark"
    ["class"]=>
    string(3) "Dog"
  }
}
*/
$tmpAry = array();
foreach($methods as $method){
    $tmpAry[] = array(
        'name' => $method->name,
        'param' => $method->getParameters()
    );
}
/*
array(2) {
  [0]=>
  array(2) {
    ["name"]=>
    string(11) "__construct"
    ["param"]=>
    array(1) {
      [0]=>
      &object(ReflectionParameter)#4 (1) {
        ["name"]=>
        string(4) "name"
      }
    }
  }
  [1]=>
  array(2) {
    ["name"]=>
    string(4) "bark"
    ["param"]=>
    array(0) {
    }
  }
}
*/
?>

ReflectionClassのメソッドについて

string ReflectionClass::getName(void)
クラス名を返す。
string ReflectionClass::getDocComment(void)
ドキュメントのコメントを返す。
string ReflectionClass::getFileName
ファイル名を返す。
int ReflectionClass::getStartLine
クラスを宣言してる行番号を返す。
array ReflectionClass::getProperties
プロパティをReflectionPropertyクラスのオブジェクトの配列として返す。
array ReflectionClass::getMethods
メソッドをReflectionMethodクラスのオブジェクトの配列として返す。
string ReflectionClass::getParentClass
親クラスの名前を返す。

■定義済みクラスを調べる

以下のようにget_declared_classes関数を使って、定義済みクラス名を列挙することができる。但し、この関数は後述のget_defined_functions関数と異なり、システム定義とユーザ定義を明確に区別しない。

<?php
class Dog {
	//code
}
var_dump(get_declared_classes());
/*
array(101) {
  [0]=>
  string(8) "stdClass"
  [1]=>
  string(9) "Exception"
  [2]=>
  string(14) "ErrorException"
・・・
  [44]=>
  string(10) "Reflection"
  [45]=>
  string(18) "ReflectionFunction"
  [46]=>
  string(19) "ReflectionParameter"
  [47]=>
  string(16) "ReflectionMethod"
  [48]=>
  string(15) "ReflectionClass"
  [49]=>
  string(16) "ReflectionObject"
  [50]=>
  string(18) "ReflectionProperty"
  [51]=>
  string(19) "ReflectionExtension"
・・・
  [89]=>
  string(12) "PDOException"
  [90]=>
  string(3) "PDO"
  [91]=>
  string(12) "PDOStatement"
・・・
  [100]=>
  string(3) "Dog"
}
*/
?>
array get_declared_classes(void)
宣言済みのクラス名の配列を返す。

■定義済み関数について調べる

定義済み関数を調べるには、以下のようにReflectionFunctionクラスを使う。

<?php
function sample($to = 'world'){
    print('hello, {$to}');
}
$funcs = get_defined_functions();
foreach($funcs['user'] as $key => $value){
	try{
		$func = new ReflectionFunction($value);
	}
	catch(ReflectionException $e){
		//code
	}
	var_dump($func->name);
	var_dump($func->getParameters());
}
/*
string(6) "sample"
array(1) {
  [0]=>
  &object(ReflectionParameter)#2 (1) {
    ["name"]=>
    string(2) "to"
  }
}
*/
?>

また、ReflectionFunctionクラスには以下のように様々なメソッドが用意されている。getParametersメソッドで引数を返し、getFileNameメソッドでファイル名を返し、getStartLineメソッドで関数が記述されている行番号を返すといった感じだ。

<?php
function sample($to = 'world'){
    print('hello, {$to}');
}
function hoge($a, $b = array()){}
$funcs = get_defined_functions();
$tmpAry = array();
foreach($funcs['user'] as $key => $value){
	try{
		$func = new ReflectionFunction($value);
	}
	catch(ReflectionException $e){
		//code
	}
	$args = array();
	foreach($func->getParameters() as $param){
		$arg = '';
		if($param->isPassedByReference()){
			$arg = '&';
		}
		if($param->isOptional()){
			$arg = '[' . $param->getName() . ' = ' .$param->getDefaultValue() . ']';
		}
		else{
			$arg = $param->getName();
		}
		$args[] = $arg;
	}
	$tmpAry[] = array(
		'prototype' => $func->name,
		'file'      => $func->getFileName(),
		'line'      => $func->getStartLine(),
		'arg'       => $args
	);
}
var_dump($tmpAry);
/*
array(2) {
  [0]=>
  array(4) {
    ["prototype"]=>
    string(6) "sample"
    ["file"]=>
    string(57) "/var/www/sample.com/httpdocs/sample.php"
    ["line"]=>
    int(5)
    ["arg"]=>
    array(1) {
      [0]=>
      string(12) "[to = world]"
    }
  }
  [1]=>
  array(4) {
    ["prototype"]=>
    string(4) "hoge"
    ["file"]=>
    string(57) "/var/www/sample.com/httpdocs/sample.php"
    ["line"]=>
    int(8)
    ["arg"]=>
    array(2) {
      [0]=>
      string(1) "a"
      [1]=>
      string(11) "[b = Array]"
    }
  }
}
*/
?>

ReflectionFunctionクラスのメソッドについて

string ReflectionFunction::getName(void)
関数名を返す
string ReflectionFunction::getDocComment(void)
ドキュメントのコメントを返す
string ReflectionFunction::getFileName(void)
ファイル名を返す
int ReflectionFunction::getStartLine(void)
関数を宣言してる行番号を返す
array ReflectionFunction::getParameters(void)
関数の引数をReflectionParameterクラスのオブジェクトの配列として返す

定義済み関数の列挙

PHPでは以下のようにget_defined_functions関数を用いて、定義済み関数を列挙することができる。

<?php
function sample(){
    print('hello!');
}
var_dump(get_defined_functions());
/*
array(2) {
  ["internal"]=>
  array(1510) {
    [0]=>
    string(12) "zend_version"
    [1]=>
    string(13) "func_num_args"
    [2]=>
    string(12) "func_get_arg"
    [3]=>
    string(13) "func_get_args"
    [4]=>
    string(6) "strlen"
    [5]=>
    string(6) "strcmp"
・・・
    [1508]=>
    string(22) "ioncube_loader_version"
    [1509]=>
    string(23) "ioncube_loader_iversion"
  }
  ["user"]=>
  array(1) {
    [0]=>
    string(6) "sample"
  }
}
*/
?>

各関数について

array get_defined_functions(void)
定義済みの全ての関数の名前を配列で返す。

■まとめ

クラスについてはReflectionClassクラスにクラス名を引数として与えると、各メソッドで情報を取得できる。但し、getMethodsメソッドなどは(ReflectionMethodクラスの)オブジェクトの配列を返す。そのオブジェクトに対するメソッドgetParametersは、(ReflectionParameterクラスの)オブジェクトの配列を返す。

関数についてはReflectionFunctionクラスに関数名を引数として与えると、書くメソッドで情報を取得できる。但し、getParametersメソッドなどは(ReflectionParameterクラスの)オブジェクトの配列を返す。

PHP Lazy Loading(クラスファイルの自動読込)

PHP5以上では、いわゆる「遅延読込」を可能にする自動読込の機能が用意されている。

<?php
function __autoload($class){
    require_once(str_replace('_', '/', $class));
}
$obj = new Some_Class();
?>

■Standard PHP Library(SPL)による自動読み込み

__autoload関数は単一のディレクトリ構成ルールの元でコーディングする際には非常に役立つが、異なったライブラリを使うなどした場合に、非常に重く遅くなる。従って、以下のように記述する。

<?php
spl_autoload_register('spl_autoload');
if(function_exists('__autoload')){
    spl_autoload_register('__autoload');
}
?>

もしもユーザーが定義した__autoload関数がある場合は、条件分岐し登録してあげる必要がある。

spl_autoload関数

全てのインクルードパスから、クラス名を小文字にし「.php」や「.ini」を付加し検索する。付加する拡張子を加えたい場合は、spl_autoload_extensions関数を使用する。

spl_autoload_register

指定した関数を__autoloadのデフォルトの実装とする。

個人的には、プログラムの最初にインクルードファイルを明記するやり方が好きなのでイマイチ使ったことがないが、大規模なアプリケーションでの有用性は垣間見える。

PHP Exceptions(PHPの例外処理)

■PHPにおける「エラー」と「例外」の違い

  • 例外はオブジェクトであり、エラーが起こったときにthrowされる
  • 例外はハンドリングすることが可能、(継承させた)例外のタイプで条件分けできる
  • 例外はハンドリングされていない場合、全てfatalである
  • 例外は発生時にインスタンス化され、コンストラクタが実行される
  • 例外はコードの処理の流れを変える

■Exceptionクラス

以下のような実装になっている。但し、ビルトインクラスであり、ユーザーが記述する必要はない。

<?php
class Exception {
    protected $message = 'Unknown Exception';
    protected $code = 0;
    protected $fill;
    protected $line;
    function __construct($message = null, $code = 0);
    final function getMessage();
    final function getCode();
    final function getFile();
    final function getLine();
    final function getTrace();
    final function getTranceAsString();
    function __toString();
}
?>

Exceptionクラスは継承して効果的にユーザー仕様の例外を実装することができる。以下のようにPDOにおいても垣間見ることができる。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=dbname';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(PDOException $e){
    print('Exception: ' . $e->getMessage());
}
?>

class PDOException extends Exceptionしてる部分と、new PDOExceptionしてるコードがあるってことだな。。。

■例外の発生

通常、throw文によるエラーが起こったときに、例外はインスタンス化され発生する。

<?php
throw new Exception('an error occurs');//Fatal error: Uncaught exception 'Exception' with message 'an error occurs'
?>

例外はユーザー定義した例外や通常の例外まで伝播する。例外が発生する可能性がある場合、一般的には以下のようにtry~catch…ブロックが用いて例外をハンドリングする。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=dbname';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(PDOException $e){
    print('PDOException: ' . $e->getMessage());
}
?>

また、伝播する例外を分別してキャッチするには以下のようなコードを使う。tryをネストすることも可能だが見にくくなるので以下のような形が望ましい。

<?php
class MyException extends Exception {}
try {
    $dsn = 'mysql:host=localhost;dbname=dbname';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    //throw new MyException();
}
catch(MyException $e){
    print('MyException: ' . $e->getMessage());
}
catch(PDOException $e){
    print('PDOException: ' . $e->getMessage());
}
catch(Exception $e){
    print('Exception: ' . $e->getMessage());
}
?>

例外はcatchできればスクリプトの実行は止まらない。しかし、キャッチしきれないとFatal errorが起こりスクリプトの実行は止まってしまう。キャッチしきれない(ハンドリングされていない例外が発生した)ときは、以下のようにして挙動を定義することができる。但し、この場合でもスクリプトの実行が止まることに変わりはない。

<?php
function handleUncaughtException($e){
    print($e->getMessage());
}
set_exception_handler('handleUncaughtException');
throw new Exception('my error');//my error
?>

なんらかの事情で一括してハンドリングされていない例外の挙動を変更したい場合に有用なのかもしれない。

PHP Interfaces and Abstract Classes(インターフェースと抽象クラス)

抽象クラス(やインターフェース)というものは、「ここにコンナものがある」というような抽象的な雛形を用意することである。抽象的であるがゆえ、実際のモノとして生成できず(インスタンス化不可)、「コンナもの」を詳しく継承先で記述してあげる必要がある。

■インターフェース

  • サブクラスに実装を強制することができる
  • 実装を強制させるメソッド以外は何も記述できない(抽象クラスとは異なる点)
  • インスタンスを生成することはできない
  • 複数のインターフェースを実装できる(抽象クラスとは異なる点)
<?php
interface Dog {
    public function bark();
    public function appeal($thing);
}
class Pochi implements Dog {
    public function bark(){
        print('Bow!');
    }
    public function appeal($thing){
        print($thing);
    }
}
$pochi = new Pochi();
$pochi->bark();//Bow!
$pochi->appeal('Give me feeds!!');//Give me feeds!!
?>

インターフェース間での継承

以下のようにインターフェースAnimalをインターフェースDogが継承することができる。その際、以下のようにキーワードextendsを用いる。

<?php
interface Animal {
    public function appeal($thing);
}
interface Dog extends Animal {
    public function bark();
}
class Pochi implements Dog {
    public function bark(){
        print('Bow!');
    }
    public function appeal($thing){
        print($thing);
    }
}
$pochi = new Pochi();
$pochi->bark();//Bow!
$pochi->appeal('Give me feeds!!');
?>

複数のインターフェースの実装

以下のようにカンマで区切って複数のインターフェースを記述する。但し、メソッド名に重複があるとエラーになる。

<?php
interface Human {
    public function walk();
}
interface Dog {
    public function bark();
}
class Pochi implements Human,Dog {
    public function bark(){
        print('Bow!');
    }
    public function walk(){
        print('Walking!');
    }
}
$pochi = new Pochi();
$pochi->bark();//Bow!
$pochi->walk();//Walking!
?>

■抽象クラス

  • サブクラスに実装を強制することができる
  • 抽象メソッドを1つ以上持つクラスは、抽象クラスとしてabstractをつけて宣言しなくてはならない
  • インスタンスを生成することはできない
  • 仕様上、抽象メソッドにデフォルトの処理を記述することはできない
  • 抽象クラス内の通常メソッドに、継承させるべく処理を記述することができる(インターフェースとは異なる点)

以下のように、キーワード「abstract」を用いて、抽象クラス、メソッドを記述できる。

<?php
abstract class Dog {
    protected $name;
    abstract public function bark();
    public function __construct($name){
        $this->name = $name;
    }
}
class Pochi extends Dog {
    public function bark(){
        print("My name is {$this->name}. Bow!");
    }
}
$pochi = new Pochi('pochi');
$pochi->bark();//My name is pochi. Bow!
?>

但し、以下のように記述するとシンタックスエラーとなる。つまり、抽象プロパティというものは存在できない。

<?php
abstract class Dog {
    abstract protected $name;//syntax error!!
    abstract public function bark();
    public function __construct($name){
        $this->name = $name;
    }
}
?>

「抽象クラス」と「インターフェース」の同時利用

また、以下のように「抽象クラスの継承」と「インターフェースの実装」を、1つのクラスで同時に行うことができる。

<?php
abstract class Animal {
    abstract public function walk();
}
interface Dog {
    public function bark();
}
class Pochi extends Animal implements Dog {
    public function bark(){
        print('Bow!');
    }
    public function walk(){
        print('Walking!');
    }
}
$pochi = new Pochi();
$pochi->bark();//Bow!
$pochi->walk();//Walking!
?>

予備知識

PHPでは多重継承はサポートされていない。多重継承がサポートされている言語は、PythonやC++などだけである。