PHPのクラスとオブジェクト指向

ここではPHPを用いた大規模開発で重要な考え方であるオブジェクト指向を学習します。
PHPのクラスがどのようなものなのかをしっかりと押さえましょう。

クラス

クラスは下記のようにして作成することができます。クラス名はグローバルに定義されるので衝突しないようにしましょう。
クラス内にはメソッド(関数)とプロパティ(変数)以外のものを置くことはできません。
オブジェクトをインスタンス化するときは「$object = new ClassName(param);」のようにインスタンス化したあとで「$object->MethodName();」のようにして、クラス内のメソッドを利用します。
コンストラクタもメソッドの一種ですが、「__construct」という名前に特別な意味があり、インスタンス化(new)するときに自動的に読み込まれます。
また、インスタンス化するときに与えてあげた引数もコンストラクタに与えてあげるのと同じ意味になります。
クラス内のメソッドでは$thisという変数に特別な意味があり、自分自身のプロパティやメソッドを呼び出すときに使います。
class クラス名
{
    アクセス修飾子 $プロパティ名;
    アクセス修飾子 $プロパティ名 = 'プロパティの初期値';
    public function __construct(引数)
    {
        [コンストラクタの実装]
    }
    アクセス修飾子 function メソッド名()
    {
        $this->[このインスタンスのプロパティ];
        [メソッドの実装]
    }
}

$オブジェクト名 = new クラス名(引数);//この処理をインスタンス化という
$オブジェクト名->プロパティ名;//クラス内のPublic修飾子がついたプロパティ。
$オブジェクト名->メソッド名;//クラス内のPublic修飾子がついたメソッド。
クラスは個別の何かではありません。例えば、ツイッターのアカウントの振る舞いを定義したTwitterAccountクラスがあったとします。
この場合、@uhero_PRというアカウントはTwitterAccountクラスをインスタンス化したオブジェクトです。
ではTwitterAccountにはどのような振る舞いが必要か考えてみましょう。
TwitterAccountが持つべきプロパティは、ユーザーID(userId)、プロフィール(profile)、場所(location)、URL(url)です。
TwitterAccountが持つべきメソッドはそれらを登録したり取得したりする機能です。
初期値を登録するときはコンストラクタでできるようにしましょう。それを実装してみるとこのようになります。
<?php
class TwitterAccount
{
    private $name;
    private $profile;
    private $location;
    private $url;
    /**
    * これがコンストラクタです。
    * 引数で受け取ったものを、プロパティにセットしています。
    */
    public function __construct($name, $profile, $location, $url)
    {
        $this->name = $name;
        $this->profile = $profile;
        $this->location = $location;
        $this->url = $url;
    }
    /**
    * ユーザー名を取得するためのメソッドです。
    */
    public function getName()
    {
        return $this->name;
    }
    /**
    * プロフィールを取得するためのメソッドです。
    */
    public function getProfile()
    {
        return $this->profile;
    }
    /**
    * 場所を取得するためのメソッドです。
    */
    public function getLocation()
    {
        return $this->location;
    }
    /**
    * URLを取得するためのメソッドです。
    */
    public function getUrl()
    {
        return $this->url;
    }
}

$uhero_PR = new TwitterAccount('株式会社ユヒーロ', '株式会社ユヒーロ公式アカウントです。サイトの更新情報や、社内の取り組みなどツイートしてまいります。', '日本橋', 'uhero.co.jp'); //インスタンス化

echo $uhero_PR->getName(); //uhero_PRはTwitterAccountなので、getName()することができる。

クラスの継承と抽象クラス

クラスの継承

継承はクラスのメソッドやプロパティを引き継いで、新しいクラスを作ることです。
class クラス名 extends 親クラス名
{
}
例えば、Userというクラスがあったとします。その中で特にAdminUserというクラスを別に作ることになりました。
しかし、ほとんどの機能はUserと共通だった場合は同じことを書くのは面倒ですし、メンテナンス時に書き換える場所を増やしてしまうことになります。
このような場合はUserクラスを継承し、異なる部分のみAdminUserクラスに実装します。
そうするとAdminUserクラスでは、Userのメソッドをあたかも自分自身のメソッドのように呼び出すことができます。
また、親クラスと同じ名前を子クラスで定義するとオーバーライドといって上書きされます。
ただし、メソッドの場合、上書き元と引数の数が違う場合はエラーになります。
その場合はオーバーライドしたメソッドにしか存在しない引数にデフォルト値を設定することで動作するようになります。
<?php
class User
{
    private $userId;
    private $password;
    /*
    * 新規登録のメソッドです。
    */
    public function __construct($userId, $password)
    {
        $this->userId = $userId;
        $this->password = $password;
    }
    /*
    * 記事を投稿するメソッドです。
    */
    public function submit($text)
    {
        [処理];
    }
}

/*
* 管理者ユーザーを管理するクラス
*/
class AdminUser extends User
{
    /*
    * 管理者は削除することができます。
    */
    public function delete($id)
    {
        [処理];
    }
}
AdminUserクラスにはsubmitメソッドは存在しませんが、Userクラスを継承しているため、呼び出すことができます。
<?php
//上記の続き
$adminUser = new AdminUser('admin', 'pass');
$adminUser->submit('AdminUserも投稿することができます');

抽象クラス

抽象クラスは、親クラスを直接newするような使い方をしない場合に使います。
例えば、学部生の情報を管理するstudentクラスを作ったとします。また各学部ごとにそれぞれクラスを作りました。
学部生は全員、学部に所属しているため、全員が所属学部のクラスをインスタンス化することになります。
その結果、studentクラスを直接インスタンス化することは起きません。このようなクラスを抽象クラスといいます。
abstract クラス名
{
    [クラスの中身]
}
抽象クラスはこのように作成します。
また抽象クラスには抽象的なメソッドを作成することもできます。
抽象的なメソッドとはメソッドの存在を宣言するだけで、中身を書かないメソッドのことです。
メソッドの中身は継承したそれぞれのクラスで書かなくてはいけません。子クラスにそのメソッドが存在しない場合はエラーになります。
<?php
abstract class Student
{
    private $grade;//学年
    private $studentId;//学籍番号
    /*
    * コンストラクタでセットする
    */
    public function __construct($studentId, $grade)
    {
        $this->studentId = $studentId;
        $this->grade = $grade;
    }
    /*
    * 学年を取得するメソッドは全学部共通
    */
    public function getGrade()
    {
        return $this->Grade;
    }
    /*
    * キャンパス名を取得する抽象メソッド
    * 学部によって取得方法が異なるので中身を書くことは出来ないが、
    * どの学部生もどこかしらのキャンパスに在籍しているので、共通のメソッドである。
    * このクラスを継承した子クラスでも必ず中身を書く必要があることを宣言。
    */
    abstract public function getCampus();
}

/*
* 環境情報学部の学生を管理するクラス
*/
class EnvironmentalInformationStudent extends Student
{
    /*
    * 抽象メソッドの中身
    */
    public function getCampus()
    {
        return '湘南藤沢';
    }
}

/*
 * 経済学部の学生を管理するクラス
 */
class EconomicsStudent extends Student
{
    /*
    * 抽象メソッドの中身
    */
    public function getCampus()
    {
        if ($this->getGrade() <= 2) {
            return '日吉';
        }
        return '三田';
    }
}

$economicsStudent = new EconomicsStudent(21204723, 2);
$environmentalInformationStudent = new EnvironmentalInformationStudent(81271483, 4);
echo $economicsStudent->getCampus() . PHP_EOL;
echo $environmentalInformationStudent->getCampus() . PHP_EOL;
上記の例では抽象クラスであるstudentに、getCampusという抽象メソッドを実装し、継承した子クラスで中身を実装してます。
また、EconomicsStudentクラスでは、$this->getGrade()のように$thisで継承元の親クラスのメソッドを呼び出しています。

インターフェイス

インターフェイスは規格統一のための仕組みです。
例えば、あるオンラインショップでは本、DVD、CDの3種類のものを扱っています。
これらを同じように管理できるようにするために、統一された方法で商品番号を取得できるようにします。
現実世界で例えるなら統一されたバーコードを必ず商品の右下に配置するような感じでしょうか。
オブジェクト指向プログラミングの世界では「商品番号を取得するメソッド」が必ずどの商品にも存在するということを規格することで、統一された方法で管理することが可能になります。
これがインターフェイスです。インターフェイス自体はプログラムには影響は与えませんが、インターフェイスで定義したメソッドが実装されていないとエラーになります。今回の例で言うと、「この店ではここにバーコードを貼るようにしてください」というルールがインターフェイスです。
このルール自体が消滅しても、全ての商品にバーコードが貼られていれば影響はありませんが、将来のことを考えればあった方がいいですよね。
また、インターフェイスを使うメリットとしては「特定のインターフェイスを実装しているもの以外を引数として受け付けない」という書き方が可能です。
先ほどの例えで言うなら「バーコードを右下に貼るというルール」に従わずに搬入された商品を受け付けないという書き方が可能になります。
インターフェイスはこのように作成します。
interface インターフェイス名
{
    [インターフェイスの定義];
}
インターフェイスにはPublicのメソッドの定義のみが可能です。
実装はこのように行います。
class クラス名 implents インターフェイス名[, インターフェイス名(複数指定も可), ...]
{
    [クラスの中身];
}
先ほどのオンラインショップの例ではこうなります。
<?php
interface Products
{
    public function getProductNumber();
}

class Book implements Products
{
    public function getProductNumber()
    {
        [本の商品番号取得するロジックの実装];
    }
}

class DVD implements Products
{
    public function getProductNumber()
    {
        [DVDの商品番号取得するロジックの実装];
    }
}

class CD implements Products
{
    public function getProductNumber()
    {
        [CDの商品番号取得するロジックの実装];
    }
}
次にこの店の各クラスをインスタンス化したものを取り扱うメソッドを書いてみましょう。
引数の$productには本、CD、DVDのどれが入っても大丈夫なようにすると共通で利用出来て便利ですよね。
現実世界で例えるなら本、CD、DVDのどれも同じレジで同じように取り扱えるようにするといった感じでしょうか。
<?php
//続き
function getProductDetial(Products $product) {
    $product->getProductNumber();
    [以下省略];
}
引数の$productには各商品クラスをインスタンス化したオブジェクトが入っています。
引数に「Products $product」と表記することで、Productsというインターフェイスを実装したクラスをインスタンス化したオブジェクトのみを引数として受け付けるようにすることができます。
つまり、引数として受け取った$productには必ず商品番号を取得するメソッドが存在することが保証されています。
だから「$product->getProductNumber();」という書き方が可能になります。
こうして、複数のクラスのオブジェクトをまとめて取り扱い処理することが可能になります。

例外


例外は、何かしらの異常が発生したときに、別の処理を行うときに使います。

throw new 例外クラス名([メッセージ]);

このようにして、例外を発生させることが出来ます。

try {
        [処理]
} catch (例外クラス 変数名) {
        [例外処理]
}

例外が発生する可能性がある場所をtryで囲むことによって、捕捉することが出来ます。
try文で囲まれてない場所でエラーが発生した場合は、致命的なエラーとなります。

catchは例外が発生した場合に実行される内容です。
例外クラスとは、ExceptionなどPHPに予め定義されているクラスを利用するか、それを継承して作成したクラスです。

定義済みの例外クラス

PHPには定義済みの例外がいくつかあります。
例外 解説(php.netより引用)
Exception Exception は、すべての例外の基底クラスです。
ErrorException エラー例外です。
BadFunctionCallException 未定義の関数をコールバックが参照したり、引数を指定しなかったりした場合にスローされる例外です。
BadMethodCallException 未定義のメソッドをコールバックが参照したり、引数を指定しなかったりした場合にスローされる例外です。
DomainException 定義したデータドメインに値が従わないときにスローされる例外です。
InvalidArgumentException 引数の型が期待する型と一致しなかった場合にスローされる例外です。
LengthException 長さが無効な場合にスローされる例外です。
LogicException プログラムのロジック内でのエラーを表す例外です。
OutOfBoundsException 値が有効なキーでなかった場合にスローされる例外です。
OutOfRangeException 無効なインデックスを要求した場合にスローされる例外です。
OverflowException いっぱいになっているコンテナに要素を追加した場合にスローされる例外です。
RangeException プログラムの実行時に範囲エラーが発生したことを示すときにスローされる例外です。
RuntimeException 実行時にだけ発生するようなエラーの際にスローされます。
UnderflowException 空のコンテナ上で無効な操作 (要素の削除など) を試みたときにスローされる例外です。
UnexpectedValueException いくつかの値のセットに一致しない値であった際にスローされる例外です。
これらの例外の中から適切なものをthrowするようにしましょう。

例外の拡張

上記の中に適切な例外な無い場合は、自分で例外クラスを作成することも出来ます。
例外クラスを作成する場合は、必ずExceptionかそれを継承したクラスを継承する必要があります。

例外の活用方法


大規模開発では、同じメソッドを複数の場所から呼び出すことがしばしばあります。
呼び出した先のメソッドで何か想定外のことが発生した場合、メソッド内のif文などで対処してしまうと
どこから呼び出されても同じ対処しかできません。
メソッドで失敗した場合はfalseを返す場合もありますが、
これではどのような失敗をしたかによって呼び出し元で処理をわかることができません。
失敗した場合の対処が呼び出し元によって異なる処理を書きたい時に役立つのが例外です。

例えば、txtファイルを読み込むメソッドがあったとします。
このメソッドで下記の場合例外を発生させることにします
・ファイルが存在しなかった場合
・ファイルの拡張子がtxtではなかった場合
ファイルが存在しなかった場合の例外はFileNotFoundExceptionは自作することにします。
<?php
//例外クラスの拡張。
//中身はexceptionと同一だが、catchするときに名前で識別するために新しいクラスを作る。
class FileNotFoundException extends exception
{
}
/**
 * テキストファイルから中身を読み込みます。
 * @param string $filePath ファイルパス
 * @return string ファイルの中身
 */
function getText($filePath)
{
        if(!'txt'==strtolower(substr(strrchr($filePath, '.'), 1))){
                throw new InvalidArgumentException('拡張子がtxtではありません');
        }
        if (!file_exists($filePath)) {
                throw new FileNotFoundException('ファイルが存在しません');
        }
        return file_get_contents($filePath);
}
このメソッドを利用するプログラムではこのように書きます。
<?php//(つづき)
try {
        getText($filePath);
} catch (FileNotFoundException $e) {
        echo 'ファイルが存在しなかったため処理を終了します。';
        exit;
} catch (Exception $e){
        echo '想定外のデータが渡されました。';
}
このようにすることで、tryの中でFileNotFoundExceptionが発生した場合は、
処理を中断し「ファイルが存在しなかったため処理を終了します。」と出力します。
また、catch (Exception $e)と表記することで、全ての例外クラスをcatchすることが出来ます。
今回の場合は拡張子が違った場合は「想定外のデータが渡されました。」が出力されます。

ノート

catch文の中で$e->getMessage()と記述すると、その例外をthrowしたときに添えたメッセージを取得することができます。 上の場合だと’拡張子がtxtではありません’もしくは’ファイルが存在しません’にあたります。

課題

それでは引き続き課題に取り組んでみましょう!

■第5問

PrimePrinterという名前のクラスを作成し、その中で第4問と同じことをしなさい。
クラスの外側ではオブジェクトの作成とメソッドの呼び出し以外は記述しないこと。
狙い : クラスの入門。外側から呼び出す。外側から呼び出すメソッドをパブリックそれ以外をプライベートにすること。

■第6問

Smartphoneクラスを作成し、下記のプロパティをプライベートで作成しなさい。
・電話番号
・キャリア名
・機種名
・OS
これらのプロパティをコンストラクタでセットできるようにしなさい。
また、これらのプロパティを返り値とするパブリックメソッドをそれぞれ実装しなさい。
電話番号を取得するメソッド名はpublic function getNumberとすること。

次に
Featurephoneクラスを作成し、下記のプロパティをプライベートで作成しなさい。
・電話番号
・キャリア名
・機種名
・実行できるアプリ名
これらのプロパティをコンストラクタでセットできるようにしなさい。
また、これらのプロパティを返り値とするパブリックメソッドをそれぞれ実装しなさい。
電話番号を取得するメソッド名はpublic function getNumberとすること。

最後に下記のオブジェクトを作りなさい。
・電話番号(080-2222-2222)、キャリア名(docomo)、機種名(iPhone5)、OS(iOS6)のSmartphoneオブジェクト
・電話番号(090-7777-7777)、キャリア名(au)、機種名(W61SH)、実行できるアプリ(EZアプリ)のFeaturephoneオブジェクト
狙い : コンストラクタの利用、プロパティの使用。

■第7問

6問目で定義した2つのクラスの共通部分を実装した抽象クラス Mobilephone を作成しなさい。
また、6問目で作成した各クラスはMobilephoneクラスを継承し、異なる部分のみを実装するように修正しなさい。
狙い : abstractの利用、継承の利用。

■第8問

(1)
AppleProductというインタフェースを作成し、シリアル番号を取得するメソッドを宣言しなさい。
次に
AppleProductをimplementsしたMacクラスを作成し、下記のプロパティを実装しなさい。
ーシリアル番号
ー機種名
ーOS
ーCPUメーカー
これらのプロパティをコンストラクタでセットできるようにしなさい。
また、これらのプロパティを返り値とするパブリックメソッドをそれぞれ実装しなさい。

(2)
AppleProductをimplementsしたIPodTouchクラスを作成し、下記のプロパティを実装しなさい。
ーシリアル番号
ー機種名
ーOS
ー容量
これらのプロパティをコンストラクタでセットできるようにしなさい。
また、これらのプロパティを返り値とするパブリックメソッドをそれぞれ実装しなさい。

(3)
Smartphoneを継承してIphoneクラスを作り、AppleProductをimplementsしなさい。また下記のプロパティを実装しなさい。
ーシリアル番号
このプロパティと継承元のプロパティをコンストラクタでセットできるようにしなさい。
また、これらのプロパティを返り値とするパブリックメソッドをそれぞれ実装しなさい。

(4)
下記のオブジェクトを作りなさい。
・シリアル番号(X001QWERTYUIOP)、電話番号(080-2222-2222)、キャリア(docomo)、機種名(iPhone5)、OS(iOS6) のIphoneオブジェクト
・シリアル番号(X09878LKJHGFDS)、機種名(MacBook Pro Retina 13.3)、OS(MacOS X 10.9)、CPUメーカー(Intel) のMacオブジェクト
・シリアル番号(C456787TYFTY)、機種名(iPodTouch 4g)、OS(iOS6)、容量(64GB) のIpodTouchオブジェクト
狙い : インターフェイスの利用

■第9問

AppleStoreクラスを作成し、下記のプロパティをプライベートで作成しなさい。
・在庫(配列形式)
在庫プロパティは、AppleProductインターフェイスをimplementsしたオブジェクトのみが追加できる配列である。

次に下記のパブリックメソッドを追加しなさい。
・在庫内のシリアル番号を全て配列形式で取得できる。
・在庫を末尾に追加することができる。引数はAppleProductとする。
狙い : インターフェイスの理解、複数クラスの利用。

■第10問

(1)

整数の割り算をする関数divideNumberを作成しなさい。2つ数字の引数を持ち、返り値を1番目の引数を2番目の引数で割った数としなさい。
ただし、下記の場合は「不正な値です」という例外メッセージを持つ InvalidArgumentException をスローしなさい。

・数字以外がどちらかの引数に与えられた場合。
・2番目の引数に0が与えられた場合。


(2)
<?php
$values = array(1, 5, 6, 2, 0, 4, "hogehoge", 7, 3,);
foreach ($values as $value) {
        [todo];
}
上記のforeach文の中身を書き換え
ループの内側でtry-catch構文を使いなさい。
そのtryの内部で(1)で作成した関数を呼び出して出力しなさい。
ただし、第一引数は15120、第二引数は$valueとすること。

さらに、tryの内側で関数実行後に「実行しました」と出力する処理を書きなさい。
また、catchの内側では、発生したエラーメッセージを取得して出力する処理を書きなさい。

(3)
上記(2)で作成したコードにもう一つforeach文を追加し、
2つ目のforeach文では例外が発生した場合はエラーメッセージを出力して、
処理を終了させるようにしなさい。

■最終課題

はじめに、 tweet.json をダウンロードしなさい。

tweet.jsonを読み込み、ユヒーロ公式アカウントがツイートした時刻を時間別に取得し、下記の形式で出力しなさい。
ただし、東京の時刻を基準とする。また、機能ごとに適宜クラスに分けて使用すること。

例:
12時 5回
13時 9回
14時 10回
狙い : 複数クラスの利用。オブジェクト指向の理解、PHP標準の関数の利用。適切な型変換。

■課題の答え

解答の例 をダウンロードできます。
このドキュメントは Sphinx 1.2.1 で生成しました。