Edited at

私家版 Slim Framework チュートリアル (5) 〜 Controllerクラス編

More than 1 year has passed since last update.


この記事について

PHP のマイクロフレームワークのひとつである、Slim Framework ですが、チュートリアルがいまいちイケてないので、お題を流用しつつ、初学者にももう少し分かりやすくなるよう、アレンジしてみようという試みです。

本記事は第5回で、前回はこちら。

私家版 Slim Framework チュートリアル (4) 〜 編集と削除、ついでにパーシャルビュー編 - Qiita


概要

今回は、肥大化した routes.php をスリムにするため、Controller クラスを作成してみます。

コピペしたコードもあちこちにあるので、ついでにそれらも共通化してしまいます。


11. Controller クラスの導入


11.1. 独自クラスを使うための準備

composer.json を開いて、以下の記述を追加します("psr-4" のセクションです)。



"autoload": {
"psr-4": {
"Classes\\": "src/classes/"
},
"files": ["src/helpers.php"]
},

クラス名とディレクトリ名は何でもいいので、いい名前があれば独自に命名しても OK です(ちなみに Laravel だと "App\\": "app/" になってますね)。

続いて、 src/classes というディレクトリを作成してください。

これで、 src/classes 配下に作成したPHPのクラスは、以下のように use 文を書くことで、他のPHPファイルから参照できるようになりました。

use Classes\SomeClass;


11.2. Controller 抽象基底クラスの作成

抽象基底クラスとは、直接インスタンス化することはできず、継承した子クラスを通してのみ利用できるクラスのことです。

簡単な例を書くと、

// 抽象基底クラスの宣言

abstract class AbstractClass
{
public function doSomething()
{
// 何かの処理
}
}

// 継承した子クラス
class Child extends AbstractClass
{
}

// $abstractClass = new AbstractClass(); <-- エラー
$child = new Child(); // <-- OK
$child->doSomething(); // 抽象基底クラスの処理を実行可能

今回の修正の目的は、 src/routes.php にずらずらと処理を書いてきた結果、行数が多くなってしまったのと、処理の重複がいくつかあって、保守性が下がってしまう恐れがあるので、クラス化してオブジェクト指向プログラミングできるようにしていこう、というものです。

では、 src/classes の下に controllers というディレクトリを作成し、Controller.php というファイルを追加してください。

中身はこんなかんじにしましょうか。


<?php

namespace Classes\Controllers;

use Psr\Container\ContainerInterface;
use Slim\Views\PhpRenderer;

abstract class Controller
{
/** @var \PDO */
protected $db;
/** @var PhpRenderer */
protected $renderer;

public function __construct(ContainerInterface $container)
{
$this->db = $container['db'];
$this->renderer = $container['renderer'];
}
}

Slim Framework では、 Controller クラスのコンストラクタにコンテナを渡してくれるので、そこから必要なコンポーネントを受け取ってプロパティに入れます。

これらのオブジェクトに何が入っているかは、 src/dependencies.php を見てください(renderer と db の箇所を抜粋します)。

// view renderer

$container['renderer'] = function ($c) {
$settings = $c->get('settings')['renderer'];
return new Slim\Views\PhpRenderer($settings['template_path']);
};

// database
$container['db'] = function ($c) {
$db = $c['settings']['db'];
$pdo = new PDO('mysql:host=' . $db['host'] . ';dbname=' . $db['dbname'],
$db['user'], $db['pass']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
};

PhpRenderer クラスのインスタンスと PDO クラスのインスタンスがそれぞれ renderer と db に入ってきます(見て分かるとおり、厳密には関数が登録されていますが、これは最初にコンポーネントにアクセスされるとこの関数が呼び出され、戻り値を登録し直すようになっているので、あまり気にしなくて大丈夫と思います)。

今は、各関数の戻り値がコンテナに入っている、ということだけ覚えておいてください。

さて、これで、Controller クラスを継承したクラスから、 $this->db$this->renderer という形で、それぞれ呼び出せるようになりましたので、各ルーティング関数を Controller クラスのメソッドに移動していきましょう。


11.3. TicketsController クラスの作成

src/classes/controllers の下に TicketsController.php というファイルを作成してください。

<?php

namespace Classes\Controllers;

use Slim\Http\Request;
use Slim\Http\Response;

class TicketsController extends Controller
{
public function index(Request $request, Response $response)
{
}
}

extends Controller の Controller は先ほど作成した抽象基底クラスです。


11.4. ルーティング関数から Controller クラスのメソッドへ処理を移す

一覧表示のルートを例に取って手順を示します。

現在の src/routes.php では以下のようになっています。

$app->get('/tickets', function (Request $request, Response $response) {

$sql = 'SELECT * FROM tickets';
$stmt = $this->db->query($sql);
$tickets = [];
while($row = $stmt->fetch()) {
$tickets[] = $row;
}
$data = ['tickets' => $tickets];
return $this->renderer->render($response, 'tasks/index.phtml', $data);
});

これを TicketsController クラスの index メソッドに移していきます。

    public function index(Request $request, Response $response)

{
$sql = 'SELECT * FROM tickets';
$stmt = $this->db->query($sql);
$tickets = [];
while($row = $stmt->fetch()) {
$tickets[] = $row;
}
$data = ['tickets' => $tickets];
return $this->renderer->render($response, 'tasks/index.phtml', $data);
}

続いて、 src/routes.php へ戻り、以下のように書き換えます。

use 文を追加

<?php

use Slim\Http\Request;
use Slim\Http\Response;
use Classes\Controllers\TicketsController;

getの第2引数を変更

// 一覧表示

$app->get('/tickets', TicketsController::class . ':index');

ポイントは :index の部分で、 : を忘れないようにしてください。

これで、いままでは

「/tickets という URL に GET アクセスが来たら、第2引数に書かれた関数を実行する」

という処理でしたが、

「/tickets という URL に GET アクセスが来たら、TicketsController クラスの index メソッドを実行する」

というように変更されます。

残りのメソッドの移動は、自分でやってみてください。

すべての処理を移行し終わった src/routes.php を載せておきます。

<?php

use Slim\Http\Request;
use Slim\Http\Response;
use Classes\Controllers\TicketsController;

// Routes

// 一覧表示
$app->get('/tickets', TicketsController::class . ':index');

// 新規作成用フォームの表示
$app->get('/tickets/create', TicketsController::class . ':create');

// 新規作成
$app->post('/tickets', TicketsController::class . ':store');

// 表示
$app->get('/tickets/{id}', TicketsController::class . ':show');

// 編集用フォームの表示
$app->get('/tickets/{id}/edit', TicketsController::class . ':edit');

// 更新
$app->put('/tickets/{id}', TicketsController::class . ':update');

// 削除
$app->delete('/tickets/{id}', TicketsController::class . ':delete');

$app->get('/[{name}]', function (Request $request, Response $response, array $args) {
// Sample log message
$this->logger->info("Slim-Skeleton '/' route");

// Render index view
return $this->renderer->render($response, 'index.phtml', $args);
});

ここまでの変更のすべては以下から閲覧できます。

https://github.com/nunulk/Tutorial-First-Application/tree/chapter11


12. 重複した処理を共通化する


12.1. チケットの取得処理を別メソッドに切り出す

TicketsController を見ると、以下の処理があちこちで使われていますので、これをメソッドに切り出します。

        $sql = 'SELECT * FROM tickets WHERE id = :id';

$stmt = $this->db->prepare($sql);
$stmt->execute(['id' => $args['id']]);
$ticket = $stmt->fetch();
if (!$ticket) {
return $response->withStatus(404)->write('not found');
}

最後の三行は重複してはいますが、レスポンスを返す処理なので呼び出し側に残しておいた方がいいですね。

    private function fetchTicket($id): array

{
$sql = 'SELECT * FROM tickets WHERE id = :id';
$stmt = $this->db->prepare($sql);
$stmt->execute(['id' => $id]);
$ticket = $stmt->fetch();
if (!$ticket) {
throw new \Exception('not found');
}
return $ticket;
}

呼び出し側はこうなります(show メソッドを例に取ります)。

    public function show(Request $request, Response $response, array $args)

{
try {
$ticket = $this->fetchTicket($args['id']);
} catch (\Exception $e) {
return $response->withStatus(404)->write($e->getMessage());
}
$data = ['ticket' => $ticket];
return $this->renderer->render($response, 'tasks/show.phtml', $data);
}

edit, update, delete も同様に置き換えてみてください。

ここまでの変更のすべては以下から閲覧できます。

https://github.com/nunulk/Tutorial-First-Application/tree/chapter11

次回、テストについて書いて、それで終わりにしようと思います。