1. Home
  2. PHP
  3. OOP
  4. FWを使わないOOP-PHPで少しだけ幸せになる〜オブジェクト指向/無印PHPプログラミングTips〜

FWを使わないOOP-PHPで少しだけ幸せになる〜オブジェクト指向/無印PHPプログラミングTips〜

  • 公開日
  • 更新日
  • カテゴリ:OOP
  • タグ:PHP,DI,PHPUnit,OOP
FWを使わないOOP-PHPで少しだけ幸せになる〜オブジェクト指向/無印PHPプログラミングTips〜

今や PHP も、FW(フレームワーク)を使ったアプリケーション構築が当たり前になっています。

Laravel/CakePHP/symfony などなど… FW の選択肢は多岐にわたりますが、 それでもたまに降ってきます、若干数のクラスを必要とする処理で、FW を使うほどではない超ミニマムな感じのやつです。

今回は、PHP において FW を使わずにオブジェクト指向プログラミングで小さなアプリケーションを 構築する際に、かゆいところに手が届くようなスケルトンを作成します。

Contents

  1. 開発環境
  2. 無印ちゃんは「名前空間」を掌握したい
    1. composer.json 作成
    2. コントローラ作成
  3. 無印ちゃんは「環境変数」を env ファイルで管理したい
  4. 無印ちゃんは「Configure クラス」を使いたい
  5. 無印ちゃんは「DI コンテナ」を入れたい
  6. 無印ちゃんは「ユニットテスト」を走らせたい

開発環境

今回の開発環境は以下の通りです。

  • PHP 7.2
  • composer 1.8.0

今回 DB は使わないので、以下の環境があれば OK です。

  • PHP が動作してブラウザから結果が確認できる
  • composer コマンドが叩ける(composer をインストールしておいてください。)

また、作業ディレクトリ(プロジェクトルート)には何も無い空の状態から始めます。ディレクトリのみ、作成しておいてください。

尚、composer コマンドは全てプロジェクトルートから叩く前提での記述になっています。

無印ちゃんは「名前空間」を掌握したい

まずは、ベースとなる部分を作成します。

コントローラを設置したいので、app ディレクトリを作ってそれに名前空間を当てます。

composer.json 作成

プロジェクトルートに移動し、以下の composer コマンドで composer.json を生成します。

# composer.json 生成
composer init

色々と聞かれますが、興味がなければ全て Enter 連打で問題ありません。

composer.json が生成されたら、autoload >> psr-4 プロパティに名前空間 App を定義します。

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}

追加したら、以下の composer コマンドを叩いて名前空間を適用(オートローダを更新)させます。

# オートローダ更新
composer dump-autoload

コントローラ作成

名前空間を設定したので、動作確認の為にコントローラを作成します。 (ここからは適宜足りないディレクトリ・ファイルは作成してください。)

app/Controllers/IndexController.php
<?php
declare(strict_types=1);

namespace App\Controllers;

class IndexController
{
    public function index()
    {
        var_dump('IndexController - index');exit;
    }
}

これを、index.php でインスタンス化して動作させる流れにします。

index.php
<?php
declare(strict_types=1);

const __PROJECT_ROOT__ = __DIR__;

require_once __PROJECT_ROOT__ . "/vendor/autoload.php";

use App\Controllers\IndexController;

$controller = new IndexController();
$controller->index();

ブラウザから確認してみます。

ひとまずここまでは動作しました。ここをベースに、色々とかゆいところに手を伸ばしていきます。

無印ちゃんは「環境変数」を env ファイルで管理したい

こういう簡単なプログラムだと、ついつい定数をクラスにべた書きする傾向にあります。

「だって単発のプログラムだし、まあいいよね。」

いいのですが気持ち悪いので、環境変数は env ファイルで管理できるようにします。

環境変数を env ファイルで管理する為に、PHP dotenv を導入します。

PHP dotenv
https://github.com/vlucas/phpdotenv

以下のコマンドを叩いてインストールします。

# PHP dotenv インストール
composer require vlucas/phpdotenv

インストールが完了したら、以下のファイルをプロジェクトルートに作成します。

.env
環境変数を設定する
.env.example
.env と同じ項目を持ち、値を空(もしくは適当な初期値)にしたもの
.env は機密情報の塊なので、ソース管理(Githubなど)にはアップしてはいけません。 その代わりに .env.example をソース管理に含めます。

.gitignore を作成し設定しておきます。

.gitignore
vendor
.env

.env には以下を記述しておきます。

.env
SAMPLE_ENV=ABCDEFG

ここまで完了したら、dotenv を有効化します。

これまでは index.php で読み込みを行っていましたが、ここににつらつら書いていくとその他の機能を導入していった時にファットになるので、bootstrapファイルを作って切り出しておきます。

/bootstrap/bootstrap.php
<?php
declare(strict_types=1);

require_once __PROJECT_ROOT__ . "/vendor/autoload.php";

$dotenv = Dotenv\Dotenv::create(__PROJECT_ROOT__);
$dotenv->load();

index.php も変更します。

index.php
<?php
declare(strict_types=1);

const __PROJECT_ROOT__ = __DIR__;

require_once "bootstrap/bootstrap.php";

use App\Controllers\IndexController;

$controller = new IndexController();
$controller->index();

セッティングが完了したので、ENV から値を取得してみます。 コントローラの index() メソッドを以下に変更します。

public function index()
{
    var_dump(getenv('SAMPLE_ENV'));exit;
}

ブラウザから確認してみます。

ENV から環境変数を取得できるようになりました。

無印ちゃんは「Configure クラス」を使いたい

env を導入したら次にやりたい事は、Configure クラスです。 アプリケーションの設置値を集約していて、そこから値を取得できるやつです。 これは手製でいきましょう。

まずは名前空間を設定します。 composer.json の autoload >> psr-4 プロパティに名前空間 Configure を定義します。

composer.json
{
"autoload": {
"psr-4": {
"App\\": "app/",
            "Configure\\": "config/"
        }
    }
}

追加したら、以下の composer コマンドを叩いて名前空間を適用(オートローダを更新)させます。

# オートローダ更新
composer dump-autoload

config ディレクトリを作成し Configure クラスを定義します。

config/Configure.php
<?php
declare(strict_types=1);

namespace Configure;

class Configure
{
    private static $instance;

    private static $Config;

    private function __construct()
    {
        self::$Config['app'] = include "app.php";
    }

    public static function Instance()
    {
        if (empty(self::$instance)) {
            self::$instance = new Configure();
        }
        return self::$instance;
    }

    /**
     * @param string $keys
     * @return mixed
     * @throws \Exception
     */
    public static function read(string $keys)
    {
        if ($keys === '') throw new \Exception('Argument 1 passed to Configure\Configure::read() must be Not Blank.');

        $keys = explode('.', $keys);
        $result = self::$Config;
        foreach ($keys as $key => $val) {
            $result = $result[$val];
        }
        return $result;
    }

    /**
     * @throws \Exception
     */
    public final function __clone()
    {
        throw new \Exception('This Instance is Not Clone');
    }

    /**
     * @throws \Exception
     */
    public final function __wakeup()
    {
        throw new \Exception('This Instance is Not unserialize');
    }
}

シングルトンでの定義です。
今回は read だけ定義しました。(write まで定義するとボリューム的に light じゃなくなるので)

次に、設定値を記述する app.php を作成します。

config/app.php
<?php

return [
    'sample'  =>  [
        'env' => getenv('SAMPLE_ENV')
    ]
];

ここで、配列でも文字列でも好きに定義して設定値として Configure クラスで保持します。 (でも、機密な環境変数は必ず ENV で設定してください。)

最後に、bootstrap.php でインスタンスを生成します。

bootstrap/bootstrap.php
<?php
declare(strict_types=1);

require_once __PROJECT_ROOT__ . "/vendor/autoload.php";

$dotenv = Dotenv\Dotenv::create(__PROJECT_ROOT__);
$dotenv->load();

// 追加
Configure\Configure::Instance();

準備が整ったので、Configure クラスから値を取得してみます コントローラの index() メソッドを以下に変更します。

<?php
declare(strict_types=1);

namespace App\Controllers;

use Configure\Configure;

class IndexController
{
    public function index()
    {
        var_dump(Configure::read('app.sample.env'));exit;
    }
}

ブラウザから確認してみます。

Configure クラスから設定値が取得できるようになりました。

無印ちゃんは「DI コンテナ」を入れたい

極ミニマムのアプリケーションとはいえ、処理分けたりパッケージ導入したりしてなんだかんだクラスは数個程度にはなるものです。

DI, 、したいですよね。

私達の港にもコンテナ入れましょう。

まずは、DI する適当なインターフェースとサービスクラスを定義しておきます。

app/Controller/SampleInterface.php
<?php
declare(strict_types=1);

namespace App\Controller;

interface SampleInterface
{
    public function index(): string;
}
app/Service/SampleAService.php
<?php
declare(strict_types=1);

namespace App\Service;

use App\Controller\SampleInterface;

class SampleA implements SampleInterface
{
    public function index(): string
    {
        return 'This is Sample A';
    }
}
app/Service/SampleBService.php
<?php
declare(strict_types=1);

namespace App\Service;

use App\Controller\SampleInterface;

class SampleB implements SampleInterface
{
    public function index(): string
    {
        return 'This is Sample B';
    }
}

これらをコントローラから DI できるように仕組みを構築していきます。

DI コンテナの実現には、PHP-DI を導入します。

PHP-DI
http://php-di.org/

PHP-DI をインストールします。 composer でインストールします。

# PHP-DI インストール
composer require php-di/php-di

インストールが完了したら、まずは有効化します

/bootstrap/bootstrap.php
<?php
declare(strict_types=1);

require_once __PROJECT_ROOT__ . "/vendor/autoload.php";

$dotenv = Dotenv\Dotenv::create(__PROJECT_ROOT__);
$dotenv->load();

Configure\Configure::Instance();

// 追加
$builder = new DI\ContainerBuilder();
$builder->addDefinitions('bootstrap/container.php');
$container = $builder->build();

次に、container.php を作成し、Factory を定義します。

/bootstrap/container.php
<?php
declare(strict_types=1);

use Psr\Container\ContainerInterface;
use function DI\factory;

return [
    'SampleService' => factory(function (ContainerInterface $c) {
        return new App\Service\SampleA();
    }),
    'IndexController' => factory(function (ContainerInterface $c) {
        return new App\Controller\IndexController($c->get('SampleService'));
    }),
];

最後に index.php を、コンテナ経由でインスタンスを受け取るように変更します。

index.php
<?php
declare(strict_types=1);

const __PROJECT_ROOT__ = __DIR__;

require_once "bootstrap/bootstrap.php";

$controller = $container->get('IndexController');
$controller->index();

準備が整ったので、コントローラでサービスクラスを DI でセットしてみます。

app/Controllers/IndexController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class IndexController
{
    protected $SampleService;

    public function __construct(SampleInterface $SampleService)
    {
        $this->SampleService = $SampleService;
    }

    public function index()
    {
        var_dump($this->SampleService->index());exit;
    }
}

ブラウザから確認します。

依存性の注入が正常に行われている事が確認できました。 サービスを切り替えてみます。

/bootstrap/container.php
<?php
declare(strict_types=1);

use Psr\Container\ContainerInterface;
use function DI\factory;

return [
    'SampleService' => factory(function (ContainerInterface $c) {
        // return new App\Service\SampleA();
        // ↓ 変更
        return new App\Service\SampleB();
    }),
    'IndexController' => factory(function (ContainerInterface $c) {
        return new App\Controller\IndexController($c->get('SampleService'));
    }),
];

ブラウザから確認します。

問題なく動作しています。 これで DI コンテナが導入できました。

無印ちゃんは「ユニットテスト」を走らせたい

ユニットテストも最低限入れておきたいところです。

入れましょう。テストコードの無いアプリケーションはただのアプリケーションだ。

ユニットテストツールには PHPUnit を採用します。

PHPUnit をインストールします。 composer からインストールします。

# PHPUnit インストール
composer require --dev phpunit/phpunit

インストールが完了したら、バージョン確認のコマンドを叩きつつインストールできているか確認します。

# PHPUnit バージョン確認
./vendor/bin/phpunit --version

 => PHPUnit 7.5.10 by Sebastian Bergmann and contributors.

と、ここで。いちいちコマンドが長いので入力が面倒です。 composer コマンドで実行できるようにします。

composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Configure\\": "config/"
        }
    },
    "require": {
        "vlucas/phpdotenv": "^3.3",
        "php-di/php-di": "^6.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^7.5"
    },
    // 追加
    "scripts": {
        "test": "phpunit"
    }
}

これで composer test で PHPUnit が実行されるようになります。

次に、デフォルト(テスト箇所の指定無し)では一括でテストを回したいので、phpunit.xml を作成して設定を行います。

phpunit.xml
<phpunit bootstrap="./vendor/autoload.php">
    <testsuites>
        <testsuite name="MyTest">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

これでファイルやグループなどの指定を行わない場合は tests ディレクトリ配下のテストが全て実行されます。

最低限の設定はしたので、動作確認でいくつかテストを作成します。

tests/unit ディレクトリを作成して以下、テストを定義します。

tests/unit/ConfigureTest.php
<?php
declare(strict_types=1);

namespace tests\unit;

use Configure\Configure;
use PHPUnit\Framework\TestCase;

/**
 * Class ConfigureTest
 * @package tests\unit
 */
class ConfigureTest extends TestCase
{
    /**
     * @test
     */
    public function read_引数が空の場合は例外を投げる()
    {
        $this->expectException(\Exception::class);
        Configure::read('');
    }
}
tests/unit/SampleAServiceTest.php
<?php
declare(strict_types=1);

namespace tests\unit;

use App\Service\SampleA;
use PHPUnit\Framework\TestCase;

class SampleAServiceTest extends TestCase
{
    protected $SampleService;

    public function setUp()
    {
        parent::setUp();
        $this->SampleService = new SampleA();
    }

    public function tearDown()
    {
        parent::tearDown();
        unset($this->SampleService);
    }

    /**
     * @test
     */
    public function index_返却値の文字列が正しい事を検証()
    {
        $this->assertEquals('This is Sample A', $this->SampleService->index());
    }
}

もう一つのサービスクラスのテストも作成しましたが、SampleAServiceTest とソースコードがほとんど変わらないので割愛します。

テストを実行してみます。

$ composer test
> phpunit
PHPUnit 7.5.10 by Sebastian Bergmann and contributors.

...                                      3 / 3 (100%)

Time: 23 ms, Memory: 4.00 MB

OK (3 tests, 3 assertions)

テストが実行された事を確認できました。 PHPUnit の導入もこれで完了です。

まとめ

以上で作業は終了です。 本当に小さな処理をスポットで書く場合にはこれくらいミニマムで良いし、好きな機能だけ持っておいてスムーズにきれいに開発していく のは大事だと思います。

ただし、いろいろ付与しすぎるとオレオレ FW の領域に入ってきますし、しまいには「ねえ、それってフツーに FW 使えば良くない?」みたいな事になりかねないので、 テンションの上げすぎには注意が必要です。

用法用量を守って楽しくお使いください。
サンプルコード

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級