RitoLabo

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

  • 公開:
  • 更新:
  • カテゴリ: PHP OOP
  • タグ: PHP,DI,PHPUnit,OOP

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

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

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

アジェンダ
  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クラスから環境変数出力確認

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使えば良くない?」みたいな事になりかねないので、 テンションの上げすぎには注意が必要です。

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