1
0

More than 3 years have passed since last update.

PHP5上級試験/準上級試験の上級合格に挑戦(10) 6章オブジェクト

Last updated at Posted at 2021-03-08

オブジェクト

いわゆるクラスに関する章。
初級でもこの話は出てくるので、ここでは上級ならではのテーマで取り組んでいきたい。
ちなみにオブジェクトというのはデータ(プロパティ)と、データの操作(メソッド)を一体にしたもの。
これをnewすれば、クラスのコピー(インスタンス)ができて、自由に動かせる。
中身をいじられないようにカプセル化する。

おさらい

・アクセス権の設定(public,protected,private) functionの前におく。
 なければpublic。
・オーバーライド(上書き)→親クラスのプロパティやメソッドを子クラスで同じものを指定して上書きすること
・クラスのメソッドやプロパティの呼び出し方法(クラス名->メソッド名orプロパティ名)
・クラス名はシングルクォートで囲むことはできない
・new するクラスはかっこで囲んでも囲まなくてもOK $b = new Mail; or $b = new Mail(); どちらもOK
・クラス名は大文字・小文字が区別されず、大文字小文字があっても同様のクラス名となる
・クラス名にアンダースコア2つを頭につけることはできない
・クラス名やクラス内のメソッド・プロパティを呼び出すのに可変関数・可変変数の仕組みを使うことは可能
・デフォルト値には変数・変数展開を伴う文字列・ヒアドキュメント・式・関数・オブジェクトを作成するnewキーワードは使えない
・静的プロパティstatic
・静的プロパティやメソッドへのアクセスは クラス名::静的プロパティ名or静的メソッド名でできる
・サブ(子)クラスのメソッドから親クラスのメソッドを呼び出すにはparent::メソッド名()と記載
・同じクラスのメソッドはself::メソッド名()になる
・親クラス名・親クラスのメソッド名の頭にfinalキーワードを入れるとそれは子クラスで使えなくなる
・コンストラクタ・デストラクタはそれぞれ前もってプロパティの初期化をしたり、後始末で不要なリソースを解放する処理をしたりする仕様。デストラクタはパラメータは設定できない。そもそも後始末にパラメータは必要ないよね。

まちがいやすいところ

  • クラス名->メソッド名orプロパティ名とするところ、$をつけてしまうミスが多い($はつけない)
  • メソッドは引数がなくても()を付ける必要がある(呼び出すときもメソッド名()になる)

静的メソッド

・私も見るまで走らなかったが、静的プロパティはもともとあるが、静的メソッドもある。
静的メソッドのメリットについては下記を参照。@suinさんありがとうございます!
 参考:PHP: 静的メソッドは何のためにあるか?|@suinさん

$thisとselfの違い

  • $thisは自分のオブジェクトを指す。(インスタンス化された場合クラスではなくインスタンスを指すことになる)
  • selfは自分のクラスを指す。クラス定数、static変数については、インスタンス化せずに使用するのでこの場合は、selfを使ってアクセス
  • 最近では遅延静的束縛のstaticも使えるので現在はstaticを使うことが多い(ややこしいから歓迎)
  • $thisはメソッド内限定で記述できる特別な変数
  • 静的メソッド内部コードでは$thisは使えない
  • 参考:PHPの「self::」と「$this」の違いを現役エンジニアが解説【初心者向け】|TECH ACADEMYマガジン

上記の参考をもとに、問題を作成

index.php

class Father{
  function Test(){
    self::CallWho();
    $this->CallWho();
  }

  function CallWho(){
    echo '「息子よ」';
  }
}

class Son extends Father{
  function CallWho(){
    echo '「父よ」';
  }
}

$calling = new Son();
$calling -> test();

出力は
「息子よ」「父よ」

※selfはクラスのメソッドを実行、
$thisはそもそも、$calling自体が、Sonクラスのインスタンスのため、SonクラスのメソッドCallWhoを実行。
クラス名は大文字小文字を区別しない。

※よく出題されるので、selfと$thisの概念の理解をしておこう。

クラス内の未定義のプロパティへのアクセス

・そのクラスにsetメソッドやgetメソッドがあればクラス内で定義される

 以下例

index.php

class Test {
    function __set($name,$value){
      echo $name, 'に',$value, 'を書き込み';
      print '<br>';
    }
    function __get($name){
      echo $name, 'を読み取り';
    }
}

$TestA = new Test;
$TestA -> a = 1;  //aは上のクラス内で宣言していないけど設定できた
$TestB = $TestA -> a; //もちろんすぐ呼び出せます 

出力は
「aに1を書き込み
aを読み取り」

→上記、間違いだった。(2021.3.10修正)
本来、__setメソッドや、__getメソッドは未定義のプロパティへのアクセスに使うが、
上にあるように、__setメソッドのあとに定義される、とするとその後で__getメソッドが呼び出せるのはおかしい。
なぜなら定義済のプロパティには__getは呼び出せないはずだから。

なぜ出力できたのか、というと、結局定義できていなかったから。
つまり、__setメソッドだけではだめで、下のように定義することが必要になる

index.php

<?php

class Test {
    function __set($name,$value){
      echo $name, 'に',$value, 'を書き込み';
      $this->{$name} = $value; //ここの部分を追加
      print '<br>';
    }
    function __get($name){
      echo $name, 'を読み取り';
    }
}

$TestA = new Test;
$TestA -> a = 1; //aは上のクラス内で宣言していないけど設定できた
$TestB = $TestA -> a;//呼び出せなくなった 

?>

出力は
aに1を書き込み

そう、__getメソッドが呼び出せなくなったので、aを読み取り、がない。
念の為解説すると、{$name}は変数展開で、\$this -> a = $value(つまり1)になる。
ここで未定義だったプロパティaに定義できたので、
__getメソッドは読み取れなくなったわけです。

いやあスッキリした。
参考:クラスで定義済のプロパティに__getメソッドが使えてしまう?
@uasiさんありがとうございました。

なお、var_dump(property_exists($x, 'a'));で、定義できているかどうかを確認できる。
デバッグに使える。

オブジェクトのコピー

・clone コピーしたいオブジェクト名でできる
・クラス内に__clone()メソッドがあるとコンストラクタのようにクローンされたときにすぐ呼び出される
・ただし、これは他のメソッドとはちがって外部から直接アクセスすることはできない(エラーになる)

では、下記の出力はどうなるか答えよ。
下記のコードの後に、「$z = $x -> __clone();」を入れるとどうなるか。

index.php
class Test {
  public $a;
  public $b;
  function __clone(){
    $this -> b =0;
  }

}

$x = new Test;
$x -> a = 2;
$x -> b = 2;

$y = clone $x;
$y-> a = 4;

echo 'x:', $x->a, $x->b;
echo 'y:', $y->a, $y->b;


正解は
x:22y:40
警告エラー(何も出力されない)

アクセス権について

  • public,protected,privateという3pだけ覚えても役に立たず、実際にコードで書くとどうなるのかをしっかり理解しよう
  • var は public の古い宣言方法
  • protectedは、そのクラス自身と継承した子クラスからアクセス可能可 ただし、そのクラスの親クラスは対象にならないので注意。 これを出題されたことがある
  • サブクラスから親クラスのprivateなプロパティにアクセスしようとすると、サブクラスに同名のプロパティができてしまう仕様がある。 以下実例
index.php
class X {
  private $a = 1;  
}
class Y extends X{
  function test(){
    $this->a=2;
  }
}
$y = new Y;
$y->test();
var_dump($y);


出力
object(Y)#1 (2) { ["a":"X":private]=> int(1) ["a"]=> int(2) }
つまり、スーパークラスのプロパティ\$aと、サブクラスで新たに作られたプロパティの\$aの2つが個別にあるってことになる。

抽象クラス・抽象メソッド・インタフェース

さて、上級ならではのテーマがこちら、抽象クラス・メソッド・インタフェース。
はっきり言って問題集の説明を読んでいてよくわからなかった。
いきなりぱっと出てきた感じ(笑)
書き方はまあ覚えたらいけると思うけど、そもそもどんなときに使うのだと言う話。

調べたらちょっとわかりやすい感じの説明をしてくれてるサイトを見つけました
ありがとうございます!!

参考:
抽象クラス(abstract)とインターフェース(interface)についての復習|いぬごやねっと
インターフェースと抽象クラスの使い分け、活用方法|@igayamaguchi
【詳解】抽象クラスとインタフェースを使いこなそう!!|@yoshinori_hisakawa

さてこれらをもとに、自分自身でも咀嚼して説明していく。
まず先に
抽象クラスは、一番上のサイトにも書かれていたけど、
たとえば犬・猫・豚とかだったら「動物」、えんぴつ、消しゴム、ものさしなら「文房具」と抽象化できるが、
それらは単純に上記に共通する項目があるから。
下にあるのは、一番上のサイトに書かれたことをまとめただけだが、
要するに共通する項目を全部まとめてクラスにして、ここから継承して動物としての共通データやメソッドをオーバーライドし、犬とか豚とかのクラスを作るということ

次に、抽象メソッドは、、、まあわかるね、上記の動物としてのメソッドが抽象メソッドにあたる。
インターフェースは、「窓口」ということで、これらの複雑なクラスの内部を見なくても、手続き(書き方)さえ知っていれば外部からそのインターフェースがあるクラスへのアクセスができるようになるというもの。
つまり、カプセル化は中身をいじられないようにするものだが、インターフェースはそのカプセル化のために用意されたもの。

さてなにはともあれ実践
まず何より抽象クラスは基本的に親クラスになりやすい、ということ。

  • 抽象メソッドを一つでももつ場合、そのクラスは抽象クラスとしなければならない
  • 抽象メソッド or 抽象クラスを作る場合、メソッド名orクラス名の頭に abstruct
  • 当たり前だが、抽象クラスのインスタンスは作れない。抽象クラスから継承したクラスのインスタンスを作ることになる なぜなら、たとえば、動物クラスだけでは実際の動物は作れず、それから先、うさぎクラスなど、うさぎだけしかない特徴を持つクラスを作らないといけないからで、そう考えると、サブの実体クラスからしかインスタンスが作れないということになる。
  • 抽象クラスを継承したクラスでは、抽象クラス内にあるメソッドを同じだけ作らねばならない。
  • そうでない場合は、その継承したクラス(サブクラス)も抽象クラスとしなければならない。そうして、実クラスを作るまで続くことになる
  • インタフェースはそのインタフェースが実装されているクラスにおいて持つべき機能を明示することができる。そして抽象クラスと同様、インタフェースを実装しているクラスでは、インタフェースにあるメソッドを同じだけ作らねばならない。そしてそうでない場合は、そのクラスは抽象クラスとしなければならない。
  • ただし、抽象クラスに抽象メソッドでない普通のメソッドが入っててもOKである
  • インタフェースを作る場合クラス名の頭に Interface、インタフェースを実装する場合 class クラス名 implements インタフェース名とする
  • インタフェースにはプロパティを設定することができない。プロパティ設定は実装したクラスで行おう。
  • ただし、定数設定はconstでできる。大文字で宣言するのが推奨されている
  • インタフェースでは他のインタフェースを継承する時、継承元と同名のメソッドは作れない
  • 複数のクラスの継承はできない。ただし、サブクラスのサブクラスは作れる
  • インタフェースの場合は複数実装することが可能。その場合はコンマで区切る。
  • インタフェースのインスタンスは作成できない。理由は抽象クラスと同じ。

あなたは誰のインスタンス?

結構出題される。
instanceof()という関数を使った問題で、例えば次のような問題が出る。

index.php

interface W{}
class X{}
class Y extends X implements W{}
class Z extends Y{}
class A extends B implements W{}
class B{} 
$y = new Y;
$z = new Z;
b = new B;
if ($y instanceof X){
 echo 'YはXのインスタンスである';
}

if ($y instanceof W){
 echo 'YはWのインスタンスである';
}
if ($b instanceof A){
 echo 'AはBのインスタンスである';
}

if ($b instanceof W){
 echo 'WはBのインスタンスである';
}


さて、インスタンスであると言える条件は
・オブジェクトがそのクラス、またはそのクラスのサブクラスのインスタンスである場合
・オブジェクトがそのインタフェースを実装するクラスおよび、そのクラスのサブクラスのインスタンスである場合
の2つ。
頭のは、クラスYはクラスXを継承したものであるため、クラスXが親、クラスYが子である。また、
親XはWをインタフェースにしている。
この場合、YはYのインスタンス、YはXのインスタンス、YはWのインスタンス、ということができる。

さらに、クラスZはクラスYを継承したものなので、クラスYが親、クラスZが子である。またクラスXは大親である。
この場合、ZはXのインスタンス、ZはYのインスタンス、ZはWのインスタンス、ということができる。

次にクラスBはクラスAを継承したものなので、クラスAが親、クラスBが子。
この場合、AはAのインスタンス、AはBのインスタンス、AはWのインスタンス、ということができる。
しかし、BはBのインスタンス、BはAのインスタンスではない、BはWのインスタンスではない。
なぜなら、Bは子であるため、親クラスのインスタンスは作れず、ましてやインタフェースを実装したのは親クラスで子クラスではないため。 

このように逆パターンやインタフェースをインスタンスにしたりするシーンでの出力を問われるややこしい問題が出題されるので注意。

子 extends 親 implements 子のインターフェース と考えること。
つまり最初が必ず子になり、extends の先が親。

タイプヒンティング(型宣言の強制)

関数及びメソッドが受け取る引数がどのオブジェクトor配列かをを指定することが出来る機能。
これによって誤った引数を引き渡すことを避けることができるというもの。
タイプヒンティングで指定されたものでないもの場合、「キャッチ可能な致命的エラー」が発生する
下の例では、キャッチ可能なエラーをキャッチしてタイプヒンティングエラーというのを出力している
ErrorExceptionではとらえられなかったのでTypeErrorでキャッチした

index.php
class X{}
class Y{}
class Z{
  function test(X $obj){
    echo 'OK';
   }
}

//エラーハンドラ
function exception_error_handler($error, $errstr, $errfile, $errline){
  throw new ErrorException($errstr,$errno, 0, $errfile, $errline);
}

//問題ない例
set_error_handler('exception_error_handler');
try{
$y = new Z();
$y -> test(new X);
} catch (TypeError $e){
  echo '例外発生';
}
//エラー例
try{
$z = new Z();
$z -> test(new Y);
echo 'OK';
} catch (TypeError $e){
  echo '例外発生';
}

クラスの調査

ここは関数を覚えたらOk
(1)クラスが定義済かどうかを調べる関数
(2)定義されたすべてのクラス名を配列で返す関数
(3)指定されたクラスのメソッド名(静的メソッド含む)の一覧を配列で返す関数
(4)指定されたクラスのプロパティとそれらのデフォルト値を配列で返す関数
(5)オブジェクトのスーパークラスを取得
(6)指定された値がオブジェクトかどうかを調べる関数
(7)指定されたオブジェクトのクラス名を取得する関数
(8)オブジェクト(またはクラス)が指定した名前のメソッドを持つかどうかを調べる関数
(9)指定されたオブジェクトについて、現在のスコープからアクセス可能なプロパティ(静的プロパティは含まない)とそれらの現在の値を取得する関数。つまり、設定したプロパティのみを返す。

正解
(1)class_exists()
(2)get_declared_classes()
(3)get_class_methods()
(4)get_class_vars()
(5)get_parent_class()
(6)is_object()
(7)get_class()
(8)method_exists()
(9)get_object_vars()

※(1)のclass_exists(class_name [,autoload = TRUE]);
デフォルトでオンになっているautoloadは、_autoload()をコールするかどうかを指定するもの。
※get_class_methodsはそのクラスだけでなく、親クラスのメソッドも合わせて返すが、同名のクラスは1つだけしか返さないので注意。つまり上書きしてるクラスは1つだけしか返さないってことになる。もちろん、外から呼び出した場合はpublicしか返さないので注意。→これを利用した問題が出題されている。
問題集の最後の仕上げ問題にもあるので解いておくと良い。

autoloadとはファイルを自動的に読み込む仕様。
クラスを作成する場合、再利用する性質上1クラスを1ファイルで管理することになるが、扱うファイル数が増えた時に、各スクリプトの先頭で一つ一つrequireしなければいけなくなる。そこでファイルを自動で読み込む仕組みであるautoloadを使用する。

シリアライズ

  • オブジェクトをバイトストリームにすること。これによってオブジェクトをファイルに保存可能になる
  • バイトストリームとは「byte型データの並び」のこと。下の例で出力すればわかる。ほかにも文字ストリームとかがある。
  • 要するに、配列とかをそのまま文字列とかにすることなく、配列のまま保存したり送受信できるようにすること
  • 下の例は配列だが、本来はデータファイルを引数にそのままシリアライズすることが多い
  • シリアライズ直前に呼び出されるのは__sleep(),直後に呼び出されるのは__wakeup()。インストラクタ・デストラクタみたいな働き方をする。シリアライズするクラスでこれらがあれば実行される
  • 参考:覚えておきたい「シリアライズ serialize」|hijiriworld Web ありがとうございます!
index.php
  $string = serialize(array(1,2,3));
  echo $string;
  print '<br>';
  var_dump(unserialize($string));

出力
a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) }

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0