【PHP】ざっくりオブジェクト指向について②

【PHP】ざっくりオブジェクト指向について②

はじめに

本記事は、オブジェクト指向について学習した内容をまとめています。前回の記事の続きになります。

サンプルコードの PHP のバージョンは8.1.20で、開発環境に Docker を使用しています。

名前空間

名前空間は PHP5.3 から出来た仕組みで、クラスをディレクトリ構造のように階層的に分類できる仕組みのことです。

名前空間を使用すると、関連するクラス、関数、定数をグループ化できます。また、同じ名前のクラスや関数が存在する可能性がある場合、名前空間を使用するとこれらの名前の衝突を避けることができます。

以前はこの機能は使えなかったので、既存のクラス名にClass-prefix1, Class-prefix2のようにプレフィックスをつけたものが多かったようです。

PHP の名前空間は、namespaceキーワードを使って定義します。

例えば、

"Class1\MyNamespace.php"
// namespaceで定義する
namespace MyNamespace;

class MyClass {
    public function MyFunction() {
        echo 'Hello, World!';
    }
}

Class1というディレクトリにMyNamespace.phpというファイルを定義してその中に名前空間とクラスを定義します。

これらのファイルを使用する場合は以下のように記述します。

"index.php"
// フルパスで使用する場合
$obj = new \MyNamespace\MyClass();

// useキーワードを使用して名前空間をインポートする場合
use MyNamespace\MyClass;

$obj = new MyClass();

違う名前空間に同じクラス名が存在していた場合asキーワードを使用して別名を指定することも出来ます。

"example.php"
// 同じクラス名を使う場合(別名を付与している)
use Auth\Login as AuthLogin;
use Db\Login as DbLogin;

$login = new AuthLogin();

既存ファイルの配置を変更する

これまで使用してきたサンプルコードは、全てのクラスをindex.phpに記述していました。

これを1 クラス 1 ファイルとして配置し、必要であればディレクトリも分けていきます。

namespace

名前空間を指定したそれぞれのファイルを確認していきます。

"BaseClass/Human.php"
<?php

namespace BaseClass;

class Human
{
    // ~ 省略 ~
}
"ExtendTrait/ActorSkill.php"
<?php

namespace ExtendTrait;

trait ActorSkill
{
    // ~ 省略 ~
}
"ExtendTrait/SingerSkill.php"
<?php

namespace ExtendTrait;

trait SingerSkill
{
    // ~ 省略 ~
}

これらのファイルをindex.phpから呼び出すには以下のように記述します。

"index.php"
<?php

// 各ファイルを読込み
require __DIR__. "/BaseClass/Human.php";
require __DIR__. "/ExtendTrait/ActorSkill.php";
require __DIR__. "/ExtendTrait/SingerSkill.php";

// 指定した名前空間内のクラスを記述する
use BaseClass\Human;
use ExtendTrait\ActorSkill;
use ExtendTrait\SingerSkill;

class Man extends Human
{
    use ActorSkill;
    use SingerSkill;

    private $job;

    // ~ 省略 ~
}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");
$man1->echoName();
$man1->useParentEchoNam();

名前空間を付与することで、ディレクトリ構造と同じように管理することが出来るようになりました。

PSR-4 に沿った名前空間を指定する

名前空間の指定に、言語自体の成約はありません。

先程指定したBaseClassと言う名前空間はBaseClass\AAA\BBBと指定することも出来ます。その場合は呼び出し側でuse BaseClass\AAA\BBBと指定する必要があります。

このように、適当な名前空間やルールが定められていない状況で名前空間が使われてしまうと、コードが理解しづらくなったり保守性が無いコードになってしまいます。

そこでPSR-4という PHP のコーディング規約に沿った名前空間の指定が重要になってきます。

PSR-4 は、PHP のオートローディング(自動クラス読み込み)のための標準規約で、PHP のフレームワークやライブラリの間で広く使われています。

これは PHP-FIG(PHP Framework Interoperability Group)というグループによって定められたもので、PHP の標準規格(PSR)の一つです。

PSR-4 の規約は以下の通りです。

ルール内容
一つのクラスあたり一つのファイル各クラスをそれぞれ独自のファイルに定義する
クラス名とファイル名の一致クラス名はその定義が含まれるファイル名と同じにする(MyClassというクラスはMyClass.phpというファイルに記述する)
名前空間とディレクトリ構造の一致名前空間とディレクトリ構造を一致させる(Foo\Bar\Bazというクラスは、Foo/Bar/Baz.phpというパスに存在するべきである)
名前空間のプレフィクスと基底ディレクトリオートローダーは、特定の名前空間のプレフィクスを特定の基底ディレクトリにマッピングされるようにする
(例えば、Human\という名前空間のプレフィクスを/path/to/Human/というディレクトリにマッピングすると、Human\Foo\Bar\Bazというクラスは/path/to/Human/Foo/Bar/Baz.phpというパスに存在するように配置する)

オートローディング

オートローディングとは、PHP のコードが実行されたときに未定義のクラスやインターフェースが呼び出された場合に、それらが定義されているファイル及びクラスを自動的に読み込む機能のことです。

index.phpでは以下のようにファイルを読み込んでいました。

"index.php"
<?php

// 各ファイルを読込み
require __DIR__. "/BaseClass/Human.php";
require __DIR__. "/ExtendTrait/ActorSkill.php";
require __DIR__. "/ExtendTrait/SingerSkill.php";

もちろんこのような記述でも問題なく動作しますが、使用するクラスのファイルを読み込み忘れていると実行時にエラーになります。

そのため、開発が大規模になってくると記載忘れによるミスで思わぬバグが発生してしまうことがります。

このような問題を解決するためにオートローディングという機能を使用して、ファイルやクラスを自動的に読み込むようにします。

オートローディングを実装するには、主に以下の 2 つの方法があります。

  • spl_autoload_register 関数を使用する
  • Composer のオートロード機能を使用する

spl_autoload_register 関数を使用する場合

spl_autoload_register関数は PHP の組み込み関数で、未定義のクラスやインターフェースが呼び出された時に自動的に呼び出される関数(オートローダ)を登録します。

spl_autoload_register(function ($class_name) {
    require 'classes/' . $class_name . '.php';
});

$obj  = new MyClass1();
$obj2 = new MyClass2();

この例では、MyClass1 と MyClass2 のクラスが未定義であった場合、それぞれのクラス名に対応'classes/MyClass1.php''classes/MyClass2.php'が自動的に読み込まれます。

Composer のオートロード機能を使用する場合

Composer は PHP の依存関係管理ツールで、オートロード機能も提供しています。composer.jsonファイルでオートロードの設定を行い、その設定に基づいてクラスが自動的に読み込まれます。

今回はこちらの機能を使用することにします。既にプロジェクト内に composer がインストールされた状態であると仮定して解説を進めます。

まずはcomposer.jsonファイルを作成します。

$ composer init

初期化時にいくつかの質問に回答すると以下のようなファイルが作成されます。

"composer.json"
{
    "name": "hn_pgtech/sample_php_project",
    "description": "PHPの機能を検証する",
    "require": {}
}

ここにオートロードに関する設定を記述します。

"composer.json"
{
    "name": "hn_pgtech/sample_php_project",
    "description": "PHPの機能を検証する",
    "require": {},
    "autoload": {
        "psr-4": {
            "BaseClass\\": "ObjectSample/BaseClass/",
            "ExtendTrait\\": "ObjectSample/ExtendTrait/"
        }
    }
}

その後、以下のコマンドを実行します。

$ composer dump-autoload

実行後、プロジェクトルートディレクトリにvendorディレクトリが作成され、autoload.phpが作成されていることを確認します。

また、登録されたクラスを確認するにはvendor/composer/autoload_psr4.phpを参照してください。

autoload.png

このファイルを、index.phpで読み込みます。

"index.php"

// 以下の記述を削除
- require __DIR__. "/BaseClass/Human.php";
- require __DIR__. "/ExtendTrait/ActorSkill.php";
- require __DIR__. "/ExtendTrait/SingerSkill.php";

// 追記
+ require __DIR__ . '/../vendor/autoload.php';

use BaseClass\Human;
use ExtendTrait\ActorSkill;
use ExtendTrait\SingerSkill;

// ~ 省略 ~

/../vendor/autoload.phpで一つ上の階層のディレクトリを指定しているのは、以下のようなディレクトリ構造になっているためです。

ここはご自身の環境に合わせて指定してください。

|-- README.md
|-- public
    |-- ObjectSample
    |   |-- BaseClass
    |   |-- ExtendTrait
    |   |-- index.php # vendor/autoload.php を読み込むファイル
    |-- composer.json
    |-- composer.lock
    |-- index.php
    |-- vendor
        |-- autoload.php # オートローダー

# ~ 省略 ~

ObjectSampleディレクトリに移動し、php index.phpとコマンドを実行して、スクリプトがエラーなく実行されるか確認してください。

グローバル空間とクラス名

名前空間を指定した際に、バックスラッシュをつけたときはグローバル空間のクラスを指定していることになります。

Humanクラスを例に見ていきます。

"BaseClass/Human.php"
<?php

// 名前空間が指定されているクラス
namespace BaseClass;

class Human
{

    // ~ 省略 ~

    public function setAge($age)
    {

        if(!is_int($age)) {
            // グローバル名前空間のExceptionを指定
            throw new \Exception("数値を指定してください。");
        }

        if($age < 0) {
            // グローバル名前空間のExceptionを指定
            throw new \Exception("年齢に負の値は指定できません。");
        }

        $this->age = $age;
    }
}

セッターメソッド内でnew \Exception()という記述ですが、Exceptionの先頭にバックスラッシュを記述しています。

もし、バックスラッシュを記述せずにExceptionを使用すると、BaseClass\Exceptionを呼び出していると解釈されるため、Exceptionのようなグローバル空間にある組み込みクラスを使用する意図であれば、Exceptionが見つからずにエラーになってしまいます。

呼び出し元のindex.phpのように、名前空間が指定されていないファイルではExceptionのようにバックスラッシュを指定しなくてもエラーなく実行できます。

"index.php"
<?php

class Man extends Human
{
    // ~ 省略 ~

    // スーパークラスのメソッドをオーバーライド
    public function echoName()
    {
        // 名前空間を指定していないので、バックスラッシュを付けなくても動作する
        throw new Exception("名前空間テスト");
        echo "私の名前は、{$this->first_name} {$this->last_name} で、職業は {$this->job} です。", PHP_EOL;
    }
}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");
$man1->echoName(); // PHP Fatal error:  Uncaught Exception: 名前空間テスト in /workspace/public/ObjectSample/index.php:53

PHP で用意されている組み込みクラスや関数を名前空間が指定されているファイルから実行する場合は、名前空間のスコープに注意が必要です。

静的メンバー

静的メンバーとは、特定のオブジェクトではなくクラス自体に関連付けられているメンバーになります。インスタンス化しなくてもアクセスすることが出来ます。

反対にこれまで記述していたコードのように、インスタンス化した際にアクセスできるメンバーをインスタンスメンバーと呼びます。

ここで言うメンバーとは、クラスが持つ変数(プロパティ)やメソッド(関数)のことを指します。

具体的なコードを以下に記述します。

"sampleStatic.php"
<?php

class MyClass
{
    public $myProperty = "インスタンスプロパティ";  // インスタンスプロパティ

    public static $myStaticProperty = "静的プロパティ"; // 静的プロパティ

    // インスタンスメソッド
    public function myMethod()
    {
        return $this->myProperty;
    }

    // 静的メソッド
    public static function myStaticMethodMain()
    {
        // クラス内で静的プロパティを呼び出す方法
        return self::$myStaticProperty . "\n";
    }

    // 静的メソッド2
    public static function myStaticMethodSub()
    {
        // クラス内で静的メソッドを呼び出す方法
        return self::myStaticMethodMain();
    }
}

$class1 = new MyClass();

$class1->myMethod(); // インスタンスメソッド
echo $class1->myProperty. "\n"; // インスタンス化後もアクセスできる
// echo $class1->myStaticProperty; // Error: インスタンスメンバーのようにアクセスは出来ない

// 静的メンバーへのアクセスは クラス名::静的メンバー名 or メソッド名 と指定する
echo MyClass::$myStaticProperty. "\n"; // 静的メンバー
echo MyClass::myStaticMethodMain(); // 静的プロパティ
echo MyClass::myStaticMethodSub(); // 静的プロパティ

このように、staticと指定したメンバーは呼び出し方が異なるので注意が必要です。

静的メンバーの使い方をまとめました。

// 定義方法
public or private static [プロパティ名]
public or private static function [メソッド名]

// クラス外での呼び出し方
ClassName::$プロパティ名 // publicのみ呼び出せる
ClassName::メソッド名() // publicのみ呼び出せる

// クラス内での呼び出し方法
self::$プロパティ名 // クラス内からであればprivateでも呼び出せる
self::メソッド名() // クラス内からであればprivateでも呼び出せる

どのオブジェクトからも呼び出せる

static クラスは、インスタンス化することなくメソッドやプロパティにアクセスできます。つまり、static なプロパティやメソッドはどのオブジェクトからでも呼び出せます。

その挙動を確認するために、MyClassSampleStaticというディレクトリ配下に設置し、名前空間を設定後、オートローディングされるように設定します。

"SampleStatic/MyClass.php"
<?php

// 名前空間を指定
namespace SampleStatic;

class MyClass
{
    public $myProperty = "インスタンスプロパティ";  // インスタンスプロパティ

    public static $myStaticProperty = "静的プロパティ"; // 静的プロパティ

    // インスタンスメソッド
    public function myMethod()
    {
        return $this->myProperty;
    }

    // 静的メソッド
    public static function myStaticMethodMain()
    {
        // クラス内で静的プロパティを呼び出す方法
        return self::$myStaticProperty . "\n";
    }

    // 静的メソッド2
    public static function myStaticMethodSub()
    {
        // クラス内で静的メソッドを呼び出す方法
        return self::myStaticMethodMain();
    }
}

composer.jsonを以下のように修正します。

"composer.json"
{
    "name": "hn_pgtech/sample_php_project",
    "description": "PHPの機能を検証する",
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.17",
        "phpstan/phpstan": "^1.10",
        "phpstan/extension-installer": "^1.3"
    },
    "config": {
        "allow-plugins": {
            "phpstan/extension-installer": true
        }
    },
    "autoload": {
        "psr-4": {
            "BaseClass\\": "ObjectSample/BaseClass/",
            "ExtendTrait\\": "ObjectSample/ExtendTrait/",
+           "SampleStatic\\": "ObjectSample/SampleStatic/"
        }
    }
}

その後、composer dump-autoloadを実行してcomposer.jsonの変更を反映させます。

$ composer dump-autoload

例として、ManクラスからMyClassの static メンバーを呼び出してみます。

"index.php"
<?php

class Man extends Human
{
    use ActorSkill;
    use SingerSkill;

    // ~ 省略 ~

     public function echoName()
    {
+       echo MyClass::$myStaticProperty. "\n"; // 静的プロパティ

+       echo MyClass::myStaticMethodMain(); // 静的プロパティ

+       MyClass::$myStaticProperty = "値を変更"; // 呼び出している箇所全てに影響する

        echo "私の名前は、{$this->first_name} {$this->last_name} で、職業は {$this->job} です。", PHP_EOL;
    }

    // ~ 省略 ~

}

$man1 = new Man("ジョニー", "デップ", 60, "俳優");

// インスタンスメソッド内で、Myclassの静的メンバーの呼び出しと、静的メンバーの値の変更を実施している
$man1->echoName();

// 値を変更することが出来るので、意図しない値の変更には注意する
+ echo MyClass::$myStaticProperty = "私はバグを埋め込みました!" . "\n";

このように、静的プロパティやメソッドは全てのクラスやインスタンス間で共有することが出来ます。そのため、全てのインスタンス間でデータを保持する必要がある場合に有効です。

共通する処理の例として、データベースへの接続先の情報を設定やクエリを実行する手順など、それぞれの呼び出し元で毎回記述していれば冗長になってしまうコードをまとめておくことが出来ます。

しかし、全てのインスタンス間で呼び出せる反面、値の変更を不変にし意図しない変更を許容しないような設計をする必要がある場合があります。

静的メンバーまとめ

  • インスタンスを生成しなくても、クラスのプロパティやメソッド(メンバ)にアクセスできる仕組みのこと
  • 静的にしたいメンバーにはstaticを付ける
  • インスタンス化すればクラスで定義したプロパティやメソッドが使用できるが、静的メンバーに指定したものはインスタンス化後にインスタンスに受け継がれない(クラスには紐づいている)
  • そのため、インスタンスメンバーと静的メンバーは呼び出し方が違う
  • 静的メンバーを呼び出すにはインスタンス名->などのアロー演算子は使えない
  • 静的メンバーはグローバルなスコープを持つので、どのインスタンスからも共有できる。

クラス定数

クラス定数とは、クラス内で定義することの出来る定義後は値の変更が出来ない定数のことです。

静的メンバーとの違いは、プログラムが実行されている間に値の変更が出来るか出来ないかと言う点になります。

アクセス修飾子を指定しなかった場合のデフォルトのアクセス範囲はpublicです。

以下は定数の定義と使用の例です。新たにsampleConst.phpを作成して記述してきます。

"sampleConst.php"
<?php

class MyClass
{
    // アクセス修飾子を指定しなければデフォルトでpublic
    const CONST1 = "クラス定数";
    // public
    public const CONST2 = "public const";
    // private
    private const CONST3 = "private const";

    public function showConstant()
    {
        // 定数を定義後はクラス内部からでも値の変更は出来無い
        // self::CONST2 = "値の変更"; // error
        // self::CONST3 = "値の変更"; // error

        echo  self::CONST3 . "\n";
    }
}

// インスタンス化しなくともアクセスできる
echo MyClass::CONST1 . "\n"; // クラス定数
echo MyClass::CONST2 . "\n"; //  public const

// MyClass::CONST2 = "値を変更"; // 値の変更は出来ない

// クラス名の文字列を変数に格納
$classname = "MyClass";
// その変数からクラスにアクセスすることが可能
echo $classname::CONST1 . "\n"; // "クラス定数" を出力

$class = new MyClass();
$class->showConstant(); // private const

// インスタンス化後も外部から呼び出し可能
echo $class::CONST2 . "\n"; // public const
// privateは外部から呼び出せない
// echo $class::CONST3 . "\n"; // error

クラスを継承した場合、スーパークラスで定義された定数を呼び出すことも出来ます。

"sampleConst.php"
<?php

// 継承した場合
class BaseClass
{
    public const BASE_CONSTANT = 'スーパークラスのクラス定数';
}

class SubClass extends BaseClass
{
    public function showConstant()
    {
        // サブクラスのスコープで BASE_CONSTANT が定義されているか探す
        // なければスーパークラスで定義されているか見に行く
        echo self::BASE_CONSTANT . "\n";
        // 直接スーパークラスの定数を参照しに行く
        echo parent::BASE_CONSTANT . "\n";
    }
}

$subClass = new SubClass();
$subClass->showConstant(); // スーパークラスのクラス定数出力

echo SubClass::BASE_CONSTANT . "\n"; // スーパークラスのクラス定数出力

更に、継承したクラスの定数をサブクラスでオーバーライドすることも出来ます。これは抽象クラスでも同様のことが可能です。

スーパークラスで定義した定数と同じ名前を指定することで、サブクラスから上書きすることが出来ます。

なお、PHP 8.1.0 以降では、finalとして定義されたクラス定数は、サブクラスで再定義できません。

"sampleConst.php"
<?php

// 継承した場合
class BaseClass
{
    public const BASE_CONSTANT = "スーパークラスのクラス定数";

    final public const FINAL_BASE_CONSTANT = "finalクラス定数";
}

class SubClass extends BaseClass
{

    public const BASE_CONSTANT = "スーパークラス定数をオーバーライド";

    // スーパークラスでfinalが指定されている定数は上書き出来ない
    // public const FINAL_BASE_CONSTANT = "スーパークラス定数をオーバーライド";

    public function showConstant()
    {
        // サブクラスのスコープで BASE_CONSTANT が定義されているか探す
        // なければスーパークラスで定義されているか見に行く
        echo self::BASE_CONSTANT . "\n";
        // 直接スーパークラスの定数を参照しに行く
        echo parent::BASE_CONSTANT . "\n";
    }
}

$subClass = new SubClass();

echo SubClass::BASE_CONSTANT . "\n"; // スーパークラス定数をオーバーライド

// 抽象クラスでも同様のことが出来る
abstract class AbstractClass
{
    public const MY_CONSTANT = "Abstract constant";
}

class ConcreteClass extends AbstractClass
{
    // スーパークラスの定数を上書きしています
    public const MY_CONSTANT = "Concrete constant";
}

$test = new ConcreteClass();

echo $test::MY_CONSTANT. "\n"; // Concrete constant

実行ファイルであるsampleConst.phpにクラスが増えてきたので、ディレクトリにファイルを分けて名前空間を指定して読み込むことにします。

└── public
    ├── ObjectSample
    │   ├── BaseClass
    │   │   └── Human.php
    │   ├── ExtendTrait
    │   │   ├── ActorSkill.php
    │   │   └── SingerSkill.php
    │   ├── SampleConst # 作成したファイルを格納するディレクトリ
    │   │   ├── AbstractClass.php
    │   │   ├── BaseClass.php
    │   │   ├── ConcreteClass.php
    │   │   ├── MyClass.php
    │   │   └── SubClass.php
    │   ├── SampleStatic
    │   │   └── MyClass.php
    │   ├── index.php
    │   └── sampleConst.php # 定数のサンプルコード呼び出し元

BaseClass.phpの例です。

"ObjectSample\SampleConst\BaseClass.php"
<?php

namespace SampleConst;

use SampleConst\BaseClass;

class SubClass extends BaseClass
{
    public const BASE_CONSTANT = 'スーパークラス定数をオーバーライド';

    // スーパークラスでfinalが指定されている定数は上書き出来ない
    // public const FINAL_BASE_CONSTANT = "スーパークラス定数をオーバーライド";

    public function showConstant()
    {
        // サブクラスのスコープで BASE_CONSTANT が定義されているか探す
        // なければスーパークラスで定義されているか見に行く
        echo self::BASE_CONSTANT . "\n";
        // 直接スーパークラスの定数を参照しに行く
        echo parent::BASE_CONSTANT . "\n";
    }
}

各ファイルを名前空間を使用してファイルを配置しました。

クラス名の文字列からクラスにアクセスしていた場合、名前空間まで指定して呼び出す必要があります。

"sampleConst.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

use SampleConst\MyClass;
use SampleConst\SubClass;
use SampleConst\ConcreteClass;

// ~ 省略 ~
// クラス名の文字列を変数に格納
// $classname = "MyClass";

// 名前空間まで指定する必要がある
$classname = "SampleConst\\MyClass";
echo $classname::CONST1 . "\n"; // "クラス定数" を出力
  • クラス定数とは、クラス内で定義することの出来る定義後は値の変更が出来ない定数のこと
  • define関数を使用した定数と違い、クラス定数を定義することでそのクラスに属した固定の値という意味が伝わる
  • クラス全体で共有されるという点で静的メンバーと使い方は同じである
    • ただし、主にクラス内の設定値に使われ、静的メンバと違い値の変更は出来ない
  • スーパークラスで定義した定数は、継承先のサブクラスでオーバーライドすることが出来る
    • finalを指定した定数はオーバーライド出来ない
  • 設定値をベタ書きすると後々修正する際に全て修正する必要があり、修正漏れがあるとバグが発生するため固定された値にはクラス定数を積極的に使うと良い
    • 使用する際は意味のわかる適切な命名を心がける

抽象クラス

抽象クラスとは、インスタンス化することが出来ない特別な種類のクラスです。サブクラスで継承することで利用できます(多重継承は出来ない)。

また、抽象クラス内で定義することのできるメソッドのことを抽象メソッドと呼びます。

後述するインターフェースとは違い、具体的な処理を記述したメソッドと具体的な処理を持たずサブクラスで実装を強制せるメソッドを定義することが出来ます。

今回はsampleAbstract.phpファイルを作成してコードを記述してきます。

以下はその例です。

"sampleAbstract.php"
<?php

abstract class AbstractClass
{
    // 抽象メソッドの宣言
    // このメソッドはサブクラスで実装が矯正される
    abstract protected function getValue();
    abstract protected function prefixValue($prefix);

    // 通常のメソッド
    // このメソッドはサブクラスをインスタンス化後も呼び出せる
    public function printOut()
    {
        print $this->getValue() ."\n";
    }
}

class ConcreteClass1 extends AbstractClass
{
    // 抽象メソッドが protected であれば、サブクラスでは protected か public を指定できる
    protected function getValue()
    {
        return "ConcreteClass1";
    }

    public function prefixValue($prefix)
    {
        return "{$prefix}ConcreteClass1";
    }
}

$class1 = new ConcreteClass1();
$class1->printOut();
// $class1->getValue(); // protected が指定されているメソッドは外部からは呼び出せない
echo $class1->prefixValue('FOO_') ."\n";

アクセス修飾子に関しては前回の記事でまとめています。

抽象メソッドのオーバーライドの注意点として、スーパークラスのメソッドと同じかそれ以上のアクセス修飾子を保つ必要があります。

また、戻り値の型を指定した場合や引数の数など、スーパークラスのメソッドと同じように指定する必要があります。

"sampleAbstract.php"
<?php

abstract class AbstractClass
{
    // スーパークラスの抽象メソッドで戻り値の方を指定
    abstract protected function getValue(): string;
    abstract protected function prefixValue($prefix): string;

    // 通常のメソッド
    // このメソッドはサブクラスをインスタンス化後も呼び出せる
    public function printOut()
    {
        print $this->getValue() ."\n";
    }
}

class ConcreteClass1 extends AbstractClass
{
    // protected から public に変更
    // スーパークラスが protected であれば、サブクラスでは public と指定することが出来る
    public function getValue(): string
    {
        return "ConcreteClass1";
    }

    public function prefixValue($prefix): string
    {
        return "{$prefix}ConcreteClass1";
    }
}

$class1 = new ConcreteClass1();
$class1->printOut();
$class1->getValue(); // public に指定すると呼び出せるようになる
echo $class1->prefixValue('FOO_') ."\n";

先程はインスタンス化後にgetValueは呼び出せませんでしたがpublicに変更したことにより呼び出せるようになりました。

ただし、カプセル化という観点からも、外部から公開したいメソッドかどうかは慎重に設計する必要があります。

具体的なユースケース

ここまでサンプルコードで抽象クラスの説明をしてきましたが、実際にどのような場面で使用するのが良いのでしょうか?

抽象クラスの特徴として、サブクラスで実装を強制させるメソッドと、具体的な処理を記述したメソッドを持つことが出来ました。

つまり、常に共通の処理(ファイルの読込みやデータベースへの接続など)を抽象クラスのメソッドに記述しておき、どのようにデータを取得したり加工したりするかをサブクラスで振る舞いを持たせることで、共通箇所のコードが抽象クラスに集まるのでコードの見通しや再利用性が向上します。

では、AbstractDatabase.phpMySQLDatabase.phpPostgreSQLDatabase.phpSQLServer.phpというファイルを作成して、異なるデータベースに接続する際の処理を記述していきます。

事前に各種データベースのコンテナを起動し、usersテーブルを作成しています。

抽象クラスのAbstractDatabaseの内容です。(※PHPDoc は ChatGPT に生成してもらいました。)

"SampleAbstract/AbstractDatabase.php"
<?php

namespace SampleAbstract;

use PDO;

/**
 * SampleAbstract 名前空間に存在する抽象データベースクラス
 *
 * この抽象クラスは、具体的なデータベース接続とクエリの実行に関する基本的な機能を提供します。
 * 子クラスは query メソッドを必ず実装する必要があります。
 *
 * @package SampleAbstract
 */
abstract class AbstractDatabase
{
    /**
     * PDO オブジェクトを保持します。
     *
     * @var PDO
     */
    protected $connection;

    /**
    * データベースへの接続を行います。
    *
    * @param string $dns      接続するデータベースのDSN。
    * @param string $username データベース接続に使用するユーザ名。
    * @param string $password データベース接続に使用するパスワード。
    * @param array  $option   PDOのオプション配列。
    *
    * @return void
    */
    public function connect($dns, $username, $password, $option)
    {
        $this->connection = new PDO($dns, $username, $password, $option);
    }

    /**
    * データベースからの切断を行います。
    *
    * @return void
    */
    public function disconnect()
    {
        $this->connection = null;
    }

    /**
    * データベースからエラー情報を取得します。
    *
    * @return array PDO::errorInfo()から返されるエラー情報を含む配列。
    */
    protected function errorLog()
    {
        return $this->connection->errorInfo();
    }

    /**
    * クエリを実行します。
    * このメソッドは、具体的な実装を持たない抽象メソッドであり、子クラスでの実装が必要です。
    *
    * @param string $query クエリ文字列。
    * @param array  $data  クエリにバインドするデータの配列。
    *
    * @return mixed 子クラスにより定義される。
    */
    abstract public function query($query, $data);
}

AbstractDatabaseクラスを継承したMySQLDatabaseクラスです。

queryメソッドの実装は強制されます。加えて、独自にlastInsertIdメソッドを実装します。

これは、データがインサートされたときの直前のレコードの ID を返却するメソッドを実装します。

また、今回はサンプルとしてデータベースへの接続文字列やユーザー情報などは定数としてクラス内に紐づくようにしました。実際の運用では文字列でハードコーディングするより、.envファイルで管理するのが良いかと思います。

PHP dotenvというライブラリを使用した.envファイルの読込み方法は以下の記事で紹介しています。

"SampleAbstract/MySQLDatabase.php"
<?php

namespace SampleAbstract;

use SampleAbstract\AbstractDatabase;

/**
 * SampleAbstract 名前空間内の MySQLDatabase クラス。
 *
 * AbstractDatabase を継承し、MySQLへのクエリを実行する具体的なメソッドを提供します。
 *
 * @package SampleAbstract
 */
class MySQLDatabase extends AbstractDatabase
{
    /**
    * MySQLのDSN文字列。
    *
    * @var string
    */
    public const DSN = "mysql:host=db-devcontainer-mysql;dbname=mysql_db";

    /**
    * MySQLのユーザ名。
    *
    * @var string
    */
    public const USER = "docker";

    /**
    * MySQLのパスワード。
    *
    * @var string
    */
    public const PASS = "docker";

    /**
    * SQLクエリを実行します。
    *
    * @param string $sql  実行するSQLクエリ。
    * @param array  $data SQLクエリにバインドするデータの配列。
    *
    * @throws \Exception クエリの実行に失敗した場合にスローされます。
    *
    * @return \PDOStatement 実行したクエリのPDOStatementオブジェクト。
    */
    public function query($sql, $data)
    {
        $stmt = $this->connection->prepare($sql);

        // PDO::ERRMODE_EXCEPTION を指定している場合は、SQL構文間違いなどはPDOExceptionをスローする
        if(!$stmt->execute($data)) {
            // メモリ不足など何らかな非例外的な失敗が発生した場合は、以下の例外をスローする
            throw new \Exception("クエリの実行に失敗しました");
        }

        return $stmt;
    }

    /**
     * PDO接続のエラー情報を返します
     *
     * @return mixed エラー情報、または接続が存在しない場合のメッセージ
     */
    public function errorInfo()
    {
        if ($this->connection !== null) {
            // errorInfoの戻り値は配列
            return print_r($this->connection->errorInfo(), true);
        } else {
            return "PDOは保持されていません。";
        }
    }

    /**
     * 直前に挿入された行のIDを返します。
     *
     * @return string 直前の INSERT 操作によって生成されたオートインクリメントID。
     */
    public function lastInsertId()
    {
        return $this->connection->lastInsertId();
    }
}

次に、PostgreSQLDatabaseクラスの内容です。

"SampleAbstract/PostgreSQLDatabase.php"
<?php

namespace SampleAbstract;

use SampleAbstract\AbstractDatabase;

/**
 * SampleAbstract 名前空間内の PostgreSQLDatabase クラス。
 *
 * AbstractDatabase を継承し、PostgreSQLへのクエリを実行する具体的なメソッドを提供します。
 *
 * @package SampleAbstract
 */
class PostgreSQLDatabase extends AbstractDatabase
{
    /**
    * PostgreSQLのDSN文字列。
    *
    * @var string
    */
    public const DSN = "pgsql:host=db-devcontainer-postgres;dbname=postgres_db";

    /**
    * PostgreSQLのユーザ名。
    *
    * @var string
    */
    public const USER = "postgres";

    /**
    * PostgreSQLのパスワード。
    *
    * @var string
    */
    public const PASS = "rootpass";

    /**
     * SQLクエリを実行します。
     *
     * @param string $sql  実行するSQLクエリ。
     * @param array  $data SQLクエリにバインドするデータの配列。
     *
     * @throws \Exception クエリの実行に失敗した場合にスローされます。
     *
     * @return \PDOStatement 実行したクエリのPDOStatementオブジェクト。
     */
    public function query($sql, $data)
    {
        $stmt = $this->connection->prepare($sql);

        // PDO::ERRMODE_EXCEPTION を指定している場合は、SQL構文間違いなどはPDOExceptionをスローする
        if(!$stmt->execute($data)) {
            // メモリ不足など何らかな非例外的な失敗が発生した場合は、以下の例外をスローする
            throw new \Exception("クエリの実行に失敗しました");
        }

        return $stmt;
    }

    /**
     * PDO接続のエラー情報を返します
     *
     * @return mixed エラー情報、または接続が存在しない場合のメッセージ
     */
    public function errorInfo()
    {
        if ($this->connection !== null) {
            // errorInfoの戻り値は配列
            return print_r($this->connection->errorInfo(), true);
        } else {
            return "PDOは保持されていません。";
        }
    }

    /**
     * 指定されたシーケンスの現在の値を返します。
     *
     * @param string $seq 対象のシーケンス名。
     *
     * @return string 対象のシーケンスの現在の値。
     */
    public function lastInsertId($seq)
    {
        return $this->connection->lastInsertId($seq);
    }
}

PostgreSQLDatabaseクラスでも同様にlastInsertIdを実装しています。

ただし、MySQL とは若干仕様が異なり引数が必要なため追加しています。

最後に、SQLServerクラスの内容です。

"SampleAbstract/SQLServer.php"
<?php

namespace SampleAbstract;

use SampleAbstract\AbstractDatabase;

/**
 * SampleAbstract 名前空間内の SQLServer クラス。
 *
 * AbstractDatabase を継承し、SQL Serverへのクエリを実行する具体的なメソッドを提供します。
 *
 * @package SampleAbstract
 */
class SQLServer extends AbstractDatabase
{
    /**
    * SQL ServerのDSN文字列。
    * TrustServerCertificate=true; => PDOの接続文字列でSSLの検証を無効にする
    * @var string
    */
    public const DSN = "sqlsrv:server=db-devcontainer-sqlserver;database=master;TrustServerCertificate=true;";

    /**
    * SQL Serverのユーザ名。
    *
    * @var string
    */
    public const USER = "sa";

    /**
    * SQL Serverのパスワード。
    *
    * @var string
    */
    public const PASS = "Hn_Pgtech1234";

    /**
    * SQLクエリを実行します。
    *
    * @param string $sql  実行するSQLクエリ。
    * @param array  $data SQLクエリにバインドするデータの配列。
    *
    * @throws \Exception クエリの実行に失敗した場合にスローされます。
    *
    * @return \PDOStatement 実行したクエリのPDOStatementオブジェクト。
    */
    public function query($sql, $data)
    {
        $stmt = $this->connection->prepare($sql);

        // PDO::ERRMODE_EXCEPTION を指定している場合は、SQL構文間違いなどはPDOExceptionをスローする
        if(!$stmt->execute($data)) {
            // メモリ不足など何らかな非例外的な失敗が発生した場合は、以下の例外をスローする
            throw new \Exception("クエリの実行に失敗しました");
        }

        return $stmt;
    }

    /**
     * PDO接続のエラー情報を返します
     *
     * @return mixed エラー情報、または接続が存在しない場合のメッセージ
     */
    public function errorInfo()
    {
        if ($this->connection !== null) {
            // errorInfoの戻り値は配列
            return print_r($this->connection->errorInfo(), true);
        } else {
            return "PDOは保持されていません。";
        }
    }

    /**
    * 直前に挿入された行のIDを返します。
    *
    * @return string 直前の INSERT 操作によって生成されたオートインクリメントID。
    */
    public function lastInsertId()
    {
        return $this->connection->lastInsertId();
    }
}

これらのクラスをsampleAbstract.phpで実行します。

サンプルとしてデータをインサートするクエリのみ実行しています。

"sampleAbstract.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

use SampleAbstract\SQLServer;
use SampleAbstract\AbstractClass;
use SampleAbstract\MySQLDatabase;
use SampleAbstract\PostgreSQLDatabase;

/******************************
 * MySQLを使用した実装
*******************************/
$mysql = new MySQLDatabase();
// PDOオブジェクトへ渡すオプションを連想配列で指定
$option = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true
];

try {

    // 抽象クラスで実装さている、データベースへ接続する処理を実行する
    $mysql->connect(MySQLDatabase::DSN, MySQLDatabase::USER, MySQLDatabase::PASS, $option);

    $sql = 'INSERT INTO users (username, password) VALUES(:username, :password)';

    $data = [':username' => "test01", ':password' => password_hash("123456789", PASSWORD_DEFAULT)];

    $stmt = $mysql->query($sql, $data);

    if($stmt) {

        echo "mysql: データのインサートが成功しました。\n";

        $lastId = $mysql->lastInsertId();

        echo "mysql: インサートしたレコードのIDは {$lastId} です。\n";
    }

} catch(PDOException $pdoE) {

    echo "mysql: PDOException -> " . $pdoE->getMessage() . "\n";

    echo "mysql: " . $mysql->errorInfo() . "\n";

} catch(Exception $e) {

    echo "mysql: " . $e->getMessage();

} finally {

    // データベースとの接続を切断
    $mysql->disconnect();
}


/******************************
 * PostgreSQLを使用した実装
*******************************/
$postgres = new PostgreSQLDatabase();
// PDOオブジェクトへ渡すオプションを連想配列で指定
$option = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];

try {

    // 抽象クラスで実装さている、データベースへ接続する処理を実行する
    $postgres->connect(PostgreSQLDatabase::DSN, PostgreSQLDatabase::USER, PostgreSQLDatabase::PASS, $option);

    $sql = 'INSERT INTO users (username, password) VALUES(:username, :password)';

    $data = [':username' => "test01", ':password' => password_hash("123456789", PASSWORD_DEFAULT)];

    $stmt = $postgres->query($sql, $data);

    if($stmt) {

        echo "postgres: データのインサートが成功しました。\n";

        // シーケンス名を指定して最後に挿入されたIDを取得
        // シーケンス名は通常 "[テーブル名]_[idカラム名]_seq" の形式
        $lastId = $postgres->lastInsertId('users_id_seq');

        echo "postgres: インサートしたレコードのIDは {$lastId} です。\n";
    }

} catch(PDOException $pdoE) {

    echo "postgres: PDOException -> " . $pdoE->getMessage() . "\n";

    echo "postgres: " . $postgres->errorInfo() . "\n";

} catch(Exception $e) {

    echo "postgres: " . $e->getMessage();

} finally {

    // データベースとの接続を切断
    $postgres->disconnect();
}

/******************************
 * SQL Serverを使用した実装
*******************************/
$sqlserver = new SQLServer();
// PDOオブジェクトへ渡すオプションを連想配列で指定
$option = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];

try {

    // 抽象クラスで実装さている、データベースへ接続する処理を実行する
    // SSL証明書の検証を無視する設定を追加
    $sqlserver->connect(SQLServer::DSN, SQLServer::USER, SQLServer::PASS, $option);

    $sql = 'INSERT INTO users (username, password) VALUES(:username, :password)';

    $data = [':username' => 'test01', ':password' => password_hash("123456789", PASSWORD_DEFAULT)];

    $stmt = $sqlserver->query($sql, $data);

    if($stmt) {

        echo "sqlserver: データのインサートが成功しました。\n";

        // シーケンス名を指定して最後に挿入されたIDを取得
        // シーケンス名は通常 "[テーブル名]_[idカラム名]_seq" の形式
        $lastId = $sqlserver->lastInsertId();

        echo "sqlserver: インサートしたレコードのIDは {$lastId} です。\n";
    }

} catch(PDOException $pdoE) {

    echo "sqlserver: PDOException -> " . $pdoE->getMessage() . "\n";

    echo "sqlserver: " . $sqlserver->errorInfo() . "\n";

} catch(Exception $e) {

    echo "sqlserver: " . $e->getMessage() . "\n";

} finally {

    // データベースとの接続を切断
    $sqlserver->disconnect();
}

各種データベースへの接続とクエリ文が正常に実行されるとターミナルに以下のように出力され、データも正常にインサートされていることが分かります。

db-insert.png

db-insert_02.png

このように、接続先のリソースが異なっても共通の処理を抽象クラスでまとめたり、リソースごとに異なる処理はサブクラスで実装を強制させたりすることで、コードの重複や実装忘れなどを防止することが出来ます。

抽象クラスまとめ

  • 直接インスタンスを生成できないクラスのことで、必ず継承して使用するクラスということ
  • 抽象クラスには抽象メソッドが定義できる
    • 抽象メソッドとは処理内容を持たずに名前だけ定義されたメソッド
    • 抽象メソッドでは処理内容をもたないので、{}を付けない
  • 抽象メソッドは継承先のクラスで必ずオーバーライドする必要ある(実装が強制される)
  • 抽象クラスでは通常のクラス同様、プロパティや(抽象でない)メソッドも定義できる

まとめ

サンプルコードを交えて解説してきましたが、記事が長くなってきたので続きは次回の記事でまとめようと思います。