「業務として適切と思われる切り口」でのHello world

まずは「Hello world」を出してみましょう……はよいのですが。
plainなSlimだと、よく、こんなコードを見かけます。

$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});

今回いれているSkeletonでも、こんなコードになっています。

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

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

このコードだと、「ちょっとしたお試し」はともかく、実際の業務に使うには「ちょっと……」という感じになるか、と思います。

そのため。まずは、いわゆるControllerと呼ばれるものを想定して
・Controllerを置くためのディレクトリを設置
・Controllerクラスを作成してメソッドとしてactionを実装する
・src/route.phpを書き直す
といった、通常のフレームワークでよく見かけるやり方にしていきましょう。

Controllerを置くためのディレクトリを設置

あちこちのフレームワークを見ると、なんとなし、 app[s]/ または src/ のディレクトリに置いている事が多い印象です。
srcはすでにSkeletonだと使われているのと、ここにはわりと「設定ファイル」とかが置いてあるので。
今回は app/ に置いていきたいと思います。
以下のコマンドで、Controllerを置く場所を作成しましょう。
slim_startディレクトリに自分がいることを確認してからディレクトリを作成してください。

mkdir -p app/Controller

また、ここをautoloadの対象にしたいので。
composer.jsonに、autoloadの位置を知らせておきます。
おそらく、現在、composer.jsonはこんな感じになっていると思います(インストールのタイミングで、バージョンの数値は変わります)。

{
    "name": "slim/slim-skeleton",
    "description": "A Slim Framework skeleton application for rapid development",
    "keywords": ["microframework", "rest", "router", "psr7"],
    "homepage": "http://github.com/slimphp/Slim-Skeleton",
    "license": "MIT",
    "authors": [
        {
            "name": "Josh Lockhart",
            "email": "info@joshlockhart.com",
            "homepage": "http://www.joshlockhart.com/"
        }
    ],
    "require": {
        "php": ">=5.6",
        "monolog/monolog": "^1.17",
        "slim/php-view": "^2.0",
        "slim/slim": "^3.1",
        "twig/twig": "^2.9"
    },
    "require-dev": {
        "phpunit/phpunit": ">=5.0"
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "config": {
        "process-timeout": 0,
        "sort-packages": true
    },
    "scripts": {
        "start": "php -S localhost:8080 -t public",
        "test": "phpunit"
    }
}

これを、こんな風に修正してください。
端的には"require-dev"の上に、1配列、記述を追加しています。

{
    "name": "slim/slim-skeleton",
    "description": "A Slim Framework skeleton application for rapid development",
    "keywords": ["microframework", "rest", "router", "psr7"],
    "homepage": "http://github.com/slimphp/Slim-Skeleton",
    "license": "MIT",
    "authors": [
        {
            "name": "Josh Lockhart",
            "email": "info@joshlockhart.com",
            "homepage": "http://www.joshlockhart.com/"
        }
    ],
    "require": {
        "php": ">=5.6",
        "monolog/monolog": "^1.17",
        "slim/php-view": "^2.0",
        "slim/slim": "^3.1",
        "twig/twig": "^2.9"
    },
    "require-dev": {
        "phpunit/phpunit": ">=5.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "config": {
        "process-timeout": 0,
        "sort-packages": true
    },
    "scripts": {
        "start": "php -S localhost:8080 -t public",
        "test": "phpunit"
    }
}

以下の部分が追加されています。

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

上述を書いた後は、composer.pharコマンドで上述の記述を有効にしてください。

composer.phar dump-autoload

なお、時々 composer.phar dumpautoload という記述を見るのですが。
https://getcomposer.org/doc/03-cli.md#dump-autoload-dumpautoload-
を見ている限り「一応 dump-autoload だけど dumpautoload でもよい」っぽい感じです。
なんか歴史的経緯とかありそうな感じですねぇ。

閑話休題

Controllerクラスを作成してメソッドとしてactionを実装する

さて。ディレクトリが出来ましたので、早速クラスを作成していきましょう。
どこのフレームワークに行っても、比較的よく「baseController」的なクラスがあってその継承で作成されている事が多いのですが。
Slimでも、1つだけ「必要な実装」があるので、まずは基底になるクラスを作成しておきましょう。

マニュアルの記述としては、 www.slimframework.com/docs/v3/objects/router.html の Allow Slim to instantiate the controller にあるのですが。
Alternatively, if the class does not have an entry in the container, then Slim will pass the container’s instance to the constructor. You can construct controllers with many actions instead of an invokable class which only handles one action.
とありまして、サンプルコードにも

   protected $container;

   // constructor receives container instance
   public function __construct(ContainerInterface $container) {
       $this->container = $container;
   }

といった形で記述がされていますので、この処理だけは継承元で持っておく事にしましょう。
親になるクラス名は、そのまま「Controller」としておきます。
autoloaderに対応させたいので、名前空間をちゃんと書いておきましょう。
なので、ファイルの置き場所は app/Controller/Controller.php になります。

<?php

/**
 * すべてのContrlerの基底になるクラス
 *
 * @access public
 * @author XXXX<XXXX@XXXX>
 * @copyright XXXX
 */

namespace App\Controller;

use Psr\Container\ContainerInterface;

class Controller
{
    /**
     * コンストラクタ: containerを受け取る
     *
     * @access public
     * @param ContainerInterface $container container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

//
protected $container;
}

次に、実際の実装を書いていきましょう。
クラス名はざっくりと、一端「HelloWorld」クラス、としておきます。
こちらもautoloaderに対応させたいので、 app/Controller/HelloWorld.php にファイルを置き、名前空間などをちゃんと書いておきます。
まずは「処理を書くまえ」までを、ざっくりと記述します。

<?php

namespace App\Controller;

use App\Controller\Controller;
use Slim\Http\Request;
use Slim\Http\Response;

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

処理もなんもないですね。
処理内容ですが。route.phpに少しあわせまして
・ログにアクセスを記載
・テンプレートを出力
していきたいと思います。

まずはログの記述。
skeletonの記述だと

        // Sample log message
        $container->get('logger')->info("Slim-Skeleton '/' route");

となっています、が、変数の「$container」が、そもそも
・use ($container) から来ている
・$container = $app->getContainer(); で作られている
・function (App $app) で$appをとっている
となっていて、現時点だととりようがありません……が、ここで思い出して欲しい基底クラス。
基底クラスで「コンストラクタで、protectedなプロパティに $container がある」事を思い出してください。

そのため、ここではあまり悩まずに

        // Sample log message
        $this->container->get('logger')->info("Slim-Skeleton '/' route");

で動かすことが出来ます、ので、記載をしておきましょう。

さて。幾分面倒なのがrenderer周り、になります。
Skeletonでは「slim/php-viewを使って、テンプレートはPHPをほぼべた書き」なのに対して、今回の推奨はTwigなので「Twigをつかって、テンプレートはTwig用の書式」になります。
初手なので少し手間ですが、1つづつ解決をしていきましょう。

端的には
・Twigのインスタンスが取れるようにする
・テンプレートをTwig用に書き直す
となります。
ただ、そのままだと少しゴミが残りますので、上述のあとで
・slim/php-viewおよびそれ用のテンプレートを削除する
作業を、併せてやっておきましょう。
こういった「こまめなお掃除」が散らかりを防ぐのは、物理空間に限ったことではありませんので。

さて。「インスタンスの取得」ですが、その処理が書いてあるのは src/dependencies.php になります。
DIコンテナ、と言われるものの系譜になりますが、Slimの"それ"は、Pimpleと呼ばれるもの( https://pimple.symfony.com/ )がベースになります。
http://www.slimframework.com/docs/v3/concepts/di.html あたりを見ていただくとよいかと思うのですが、ここではまず簡単に「使い方」と「追加の仕方」を説明していきましょう。

src/dependencies.php の

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

および、Skeletonの

        return $container->get('renderer')->render($response, 'index.phtml', $args);

の、特に「$container->get('renderer')」を見ていただきたいのですが。

src/dependencies.php の $container['renderer'] に「return インスタンス」と書くと、$container->get('renderer') で受け取れるようになる、というのが、今回の基本的な重要事項です。
なので、これと似たようなコードのTwig版を書けばよい事になります。

rendererという名前自体は非常に抽象的で「よい」名前なので、これはこのまま使っていきましょう。
Twigですが、ものすごくシンプルには

new \Twig\Environment(new \Twig\Loader\FilesystemLoader(テンプレートディレクトリ));

で、インスタンスを作成する事ができます。
テンプレートディレクトリは、元々が

        $settings = $c->get('settings')['renderer'];

で取得できているようなので(この辺は別項で詳しく説明します)、一端、このまま使っていきましょう。
src/dependencies.phpを、以下のように書き換えます。

<?php

use Slim\App;

return function (App $app) {
    $container = $app->getContainer();

    // view renderer
    $container['renderer'] = function ($c) {
        $settings = $c->get('settings')['renderer'];
        return new \Twig\Environment(new \Twig\Loader\FilesystemLoader($settings['template_path']));
    };

    // monolog
    $container['logger'] = function ($c) {
        $settings = $c->get('settings')['logger'];
        $logger = new \Monolog\Logger($settings['name']);
        $logger->pushProcessor(new \Monolog\Processor\UidProcessor());
        $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
        return $logger;
    };
};

これで、コンテナ->get('renderer') で、Twigインスタンスを取得できるようになります。
インスタンスは取れるようになったのですが、slim/php-viewとTwigでは使い方が少し違いますので、そこを修正していきましょう。

        return $container->get('renderer')->render($response, 'index.phtml', $args);

だったのですが、Twigでは、renderは https://github.com/twigphp/Twig/blob/2.x/src/Environment.php

    public function render($name, array $context = [])
    {
        return $this->load($name)->render($context);
    }

となっているので。

        return $container->get('renderer')->render('index.phtml', $args);

としてあげる必要があります。
また
・毎回「$container->get('renderer')」を書くと、hash keyである renderer を「時々typo」とかすると面倒
・ファイルの拡張子をちょっと変えたい(気分)
などがあるので、もう少しここから修正をしていきましょう。

まず、Controllerクラス( app/Controller/Controller.php )に、以下のメソッドを追加します。

    /**
     * Twigインスタンスを取得して置換処理を行う
     *
     * @access public
     * @return object Twigインスタンス
     */
    public function renderer($name, array $context = array()) : string
    {
        return $this->container->get('renderer')->render($name, $context);
    }

これによって、このメソッドに「テンプレートファイル名」と「変換用の情報」を渡すと、「テンプレートと情報をぶつけて動的に文字列を生成」してくれます。
この手の「毎回書かなきゃいけない定型のもの」は、できるだけ、こんな風にラッパーを書いておくと、地味に便利ですし、忙しかったり疲れてたり締め切り直前だったりする時に限ってでてくる「訳のわからんバグ」を防ぐよいガードにもなりますので、こういった「ちょっとした所」を、丁寧に記述していくのは大切な事だと思います。
なお、このメソッドの戻り値はstringになります。

次に、これを使って、実際の処理を app/Controller/HelloWorld.php に記述していきます。

一応、Slimとしては「文字列をretunrするとよしなにしてくれる」挙動があるのですが( https://gallu.hatenadiary.jp/entry/20180624/p1 )。
基本的には「Objectをreturnしてくれ( http://www.slimframework.com/docs/v3/objects/response.html How to get the Response object )と書いてあるので、その通りにしていきましょう。
基本的には、こんな風に書きます。

    $response->getBody()->write(出力文字列);
    return $response;

ただ、実はこんなメソッドが、Responseクラスに実装されていまして。

    public function write($data)
    {
        $this->getBody()->write($data);

        return $this;
    }

これがあるので、じつは

    return $response->write(出力文字列);

という風に書くことができます。
このあたりから、トータルでControllerのメソッドには

        return $response->write($this->render('index.twig', $args));

という風に書くことができます。ファイル名の拡張子をこっそりと書き換えてありますが(笑

さて。続いては、テンプレートファイルを少し修正していきましょう。
綺麗なデザインは一端おいておきまして、まずは「最低限動く」程度の、ごくシンプルなテンプレートにしていきます。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Slim 3</title>
    </head>
    <body>
        <h1>Slim</h1>
        <div>a microframework for PHP</div>

        {% if '' != name %}
            Hello {{ name }}!</h2>
        {% else %}
            <p>Try <a href="http://www.slimframework.com">SlimFramework</a></p>
        {% endif %}
    </body>
</html>

Twigテンプレートの書式については、あちこちにコンテンツがあると思うので適宜調べてみてください。
一応、おいちゃんも https://gallu.hatenadiary.jp/entry/2019/03/26/234605 で一通りまとめたものを書かせていただいています。

src/route.phpを書き直す

さて。一通り必要な準備が(ようやく)整ったので、最後に、routeを修正していきましょう。
src/routes.php になります。

処理を「クラスで行う」時は、このように記述します。

    $app->get('/[{name}]', \App\Controller\HelloWorld::class . ':index');

getの第二引数に「クラス名::メソッド名」と書く感じですね。

\App\Controller\HelloWorld::class の記述については https://www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.class.class を見てみるとよいでしょう、PHP自身がもつ書式になります。
ただ、毎回 \App\Controller を書くのも面倒なので。
src/routes.php 自体の名前空間を \App\Controller にしておくことで省略が可能なので、そのほうが楽なのではないか、と思います。

上述から、src/routes.php をこのようにしていきましょう。

<?php

namespace App\Controller;

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

return function (App $app) {
    $container = $app->getContainer();

    //
    $app->get('/[{name}]', HelloWorld::class . ':index');
};

最後に、不要なものを削除していきます。
まずテンプレートファイルを削除しておきましょう。

rm templates/index.phtml

続いて、slim/php-view を削除しておきましょう。

composer.phar remove slim/php-view

これで一段落になります。

追加/修正したファイルのまとめ

少し色々とファイルを修正・追加していきましたので、最後にまとめてみましょう。

app/Controller/Controller.php

<?php

/**
 * すべてのContrlerの基底になるクラス
 *
 * @access public
 * @author XXXX<XXXX@XXXX>
 * @copyright XXXX
 */

namespace App\Controller;

use Psr\Container\ContainerInterface;

class Controller
{
    /**
     * コンストラクタ: containerを受け取る
     *
     * @access public
     * @param ContainerInterface $container container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * Twigインスタンスを取得して置換処理を行う
     *
     * @access public
     * @return object Twigインスタンス
     */
    public function render($name, array $context = array()) : string
    {
        return $this->container->get('renderer')->render($name, $context);
    }

//
protected $container;
}

app/Controller/HelloWorld.php

<?php

namespace App\Controller;

use App\Controller\Controller;
use Slim\Http\Request;
use Slim\Http\Response;

class HelloWorld extends Controller
{
    public function index(Request $request, Response $response, array $args)
    {
        // Sample log message
        $this->container->get('logger')->info("Slim-Skeleton '/' route");

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

src/dependencies.php

<?php

use Slim\App;

return function (App $app) {
    $container = $app->getContainer();

    // view renderer
    $container['renderer'] = function ($c) {
        $settings = $c->get('settings')['renderer'];
        return new \Twig\Environment(new \Twig\Loader\FilesystemLoader($settings['template_path']));
    };

    // monolog
    $container['logger'] = function ($c) {
        $settings = $c->get('settings')['logger'];
        $logger = new \Monolog\Logger($settings['name']);
        $logger->pushProcessor(new \Monolog\Processor\UidProcessor());
        $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
        return $logger;
    };
};

src/routes.php

<?php

namespace App\Controller;

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

return function (App $app) {
    $container = $app->getContainer();

    //
    $app->get('/[{name}]', HelloWorld::class . ':index');
};

templates/index.twig

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Slim 3</title>
    </head>
    <body>
        <h1>Slim</h1>
        <div>a microframework for PHP</div>

        {% if '' != name %}
            Hello {{ name }}!</h2>
        {% else %}
            <p>Try <a href="http://www.slimframework.com">SlimFramework</a></p>
        {% endif %}
    </body>
</html>

これで、いわゆる「FrameworkでHello world」を、比較的「業務でも使える構成に近い形」で記述する事ができました。
次回は、少し「どのディレクトリにどんな情報を置くか」といったあたりを、少し丁寧に考察していきましょう。


この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

エンジニアでゲーマーで講師で占い師でイベンター、な、おいちゃん。