31
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

簡単 STEPごとに作る自作MVC WebFramework PART1

Last updated at Posted at 2019-11-20

自作 Php MVC Web Framework

PHPで MVC WebFramework を自作していきます。
LaravelやDjangoとかWebFrameworkを使っていて、
「どうやってつくられてるんやろ??」ってなったので勉強がてら自作しようと思いました。
パーフェクトPHP を読んだ後に自作したので、引用されている部分もあります。
まだWebFrameworkを自作したことない方や、復習がてらもう一回やろうかなと思う方、一緒に頑張って自作していきましょう!!

この記事ではWebFrameworkをSTEPごとに作っていきます。
実際に細かい単位でコーディングしながら、動作確認ができる様になっていて、
コードが全文乗ってますので、頑張れば初学者の方でも理解しながら進めるかと思います。

あまりにも長くなってしまうので2部構成で記事にしていきます。
PART2はこちら

完成したレポジトリのリンクを張っておきます。
developブランチで確認してください。
レポジトリ

PART1でやるSTEPは以下になります。

PART2では以下です。

STEP0: 環境構築

LAMP仮想環境構築
上記の方法で環境を作成しました。
PHP7.4でDBはMySql8、ApacheServerということ前提で実装していきます。

ドメインはproject.comということで話を進めていくので、ホストPC側で仮想環境のIPアドレスにhostsでproject.comを登録することをお勧めします。
RemoteServer側では上記のLinkの方法で環境作成後に、下記を行いました。

mkdir /var/www/project.com
mkdir /var/www/project.com/public
echo "Hello World" > /var/www/project.com/public/index.html

# vhost.confの内容は後述してます。
vi /etc/httpd/conf.d/vhost.conf

service httpd restart

/var/www/project.com/ がプロジェクトのディレクトリになります。
/var/www/project.com/public ディレクトリが公開環境に設定します。
/etc/httpd/conf.d/vhost.conf を以下の内容で作成しました。
Httpdを再起動するまえに**/var/www/project.com/** と /var/www/project.com/public それぞれを作成してください。

<VirtualHost *:80>
  ServerName project.com
  DocumentRoot /var/www/project.com/public
  
  <Directory /var/www/project.com/public>
    AllowOverride All
    Require all granted
  </Directory>
  
</VirtualHost>

STEP1: 全体像

とりあえず完成した時の全体像を確認しましょう。

プロジェクト構成

プロジェクト構成は以下のようなものを想定しています。
これから実装していきますのでちらっと全体像を把握する程度で大丈夫です。

project.com
      ├─ Config/
      │     ├─ ConfigApplication.php
      │     ├─ DBSettings.php
      │     ├─ DirectorySettings.php
      │     └─ ProjectSettings.php
      │
      ├─ Libs/
      │     ├─ Apps
      │     │   └─ Auth/
      │     │       ├─ Controllers/
      │     │       ├─ Entities/
      │     │       ├─ Middleware/
      │     │       ├─ migrations/
      │     │       ├─ Repositories/
      │     │       ├─ Services/
      │     │       ├─ AuthApplication.php
      │     │       └─ RoutingTable.php
      │     │
      │     ├─ Controllers/
      │     │   ├─ Controller.php
      │     │   └─ NotFoundController.php
      │     │
      │     ├─ DB/
      │     │   ├─ DBManager.php
      │     │   ├─ Entity.php
      │     │   └─ Repository.php
      │     │
      │     ├─ Https/
      │     │   ├─ Request.php
      │     │   ├─ Response.php
      │     │   ├─ Session.php
      │     │   └─ Status.php
      │     │
      │     ├─ Middleware/
      │     │   ├─ BaseMiddleware.php
      │     │   └─ MiddlewareManager.php
      │     │
      │     ├─ Routing/
      │     │   ├─ Router.php
      │     │   └─ RoutingTable.php
      │     │
      │     ├─ Utils/
      │     │   └─ AutoLoader.php
      │     │
      │     ├─ Views/
      │     │   └─ View.php
      │     │
      │     ├─ Application.php
      │     └─ Project.php
      │
      ├─ TaskApp/
      │     ├─ Controllers/
      │     ├─ Entities/
      │     ├─ Middleware/
      │     ├─ migrations/
      │     ├─ Repositories/
      │     ├─ TaskApplication.php
      │     └─ RoutingTable.php
      │
      ├─ templates/
      │     ├─ auth/
      │     │   ├─ login.tmp.php
      │     │   ├─ my_page.tmp.php
      │     │   └─ sign_up.tmp.php
      │     │
      │     └─ task/
      │         ├─ create.tmp.php
      │         ├─ detail.tmp.php
      │         ├─ edit.tmp.php
      │         └─ index.tmp.php
      │
      ├─ public/
      │     ├─ index.php
      │     └─ .htaccess
      │
      └─ bootstrap.php

Applications

各ApplicationにはそれぞれControllerEntity(DBで扱うテーブルのこととして扱います)、MiddlewareRoutingTableなどを実装していきます。

Libs/Apps

Libs/Appsはフレームワークが提供するAPP群です。
標準機能としてフレームワークが提供したいAPPを登録していく感じです。
今回はAuth(ユーザー認証系のアプリ)をこの中に作っていきたいと思います。

User Applications

TaskApp ディレクトリは各機能を提供するアプリケーションのひとつで、用はTODOアプリを提供します。
フレームワークの使用者が各機能ごとにAppを作成してProjectに登録するような作りです。
Djangoのようにアプリケーションを作成して登録するみたいな感じで実装していこうかと思います。

Config

Config ディレクトリには各設定に関するディレクトリです。
これ自体もProjectにAppとして登録しています。
フレームワーク自体がConfigのAppに依存しているので少し特殊です。
DBの設定Appの登録Middlewareの登録AppごとのRoutingの登録などをおこないます。

Templates

templates ディレクトリには実際に表示するHtmlのテンプレートが格納されています。
Templateエンジン使ったらややこしいかなと思うので、べた書きのphpでechoとかして出していくつもりです。

Libs

Libs ディレクトリの中にWebFrameworkメイン機能を実装していこうと思います。
ここにメインの処理を実装していき、TaskAppで動作確認をしていくという流れになります。

Public

public ディレクトリ内のみを利用者に公開していきます。
public以外を公開しないことで利用者にアクセスしてほしくないソースに対してのアクセス制限を書けます。
.htaccesspublic/index.phpに毎回アクセスするようにしていきます。
index.phpではbootstrap.phpを呼び出して、Projectの起動をしてもらいます。

Bootstrap

bootstrap.php ここでリクエストが来たときのプロジェクトの設定の反映や準備を行います。
AutoLoaderを読んだりもここかなと思います。

WebFrameWorkの処理フロー

では実際にユーザーからのリクエストがあった時の処理のながれをみていきたいと思います。

  1. まず登録されているMiddlewareにリクエストを通していきます。
    ここでユーザーがログインしてないから401をかえすよーとかします。
  2. 次にルーターがuriから登録されているControllerのActionを探します。
    ControllerやActionが見つからなかった場合は404を返します。
  3. 見つけた場合はControllerのActionを実行します。
  4. 実行した結果をレスポンスとして返します。
  5. そのレスポンスをまたMiddlewareに通していきます。
    ここで何か不備があればエラーのレスポンスを返すなりします。
  6. 正常に処理が進んだ場合は最後に実行結果のレスポンスをユーザーにかえします。

図で表すと以下になります。
web_flow.PNG

全体像の把握は以上になります。
全体像を意識しながら順番に実装していきます。

STEP2: プロジェクト

まずはプロジェクトの処理autoloaderURLのリライトなどを実装していきたいです。
さらにConfigアプリを登録していきたいと思います。

このSTEP2で実装するファイルは以下になります。

project.com
      ├─ Config/
      │     ├─ ConfigApplication.php
      │     ├─ DirectorySettings.php
      │     └─ ProjectSettings.php
      │
      ├─ Libs/
      │     ├─ Utils/
      │     │   └─ AutoLoader.php
      │     │
      │     ├─ Application.php
      │     └─ Project.php
      │
      ├─ public/
      │     ├─ index.php
      │     └─ .htaccess
      │
      └─ bootstrap.php

Public

まずはpublicディレクトリから見ていきましょう。
publicディレクトリのみserverに公開されています。
ここにindex.phpと**.htaccess**を設置します。

.htaccess

public/.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

このコードは
「urlをみて、対象のファイルがなければ、index.phpにアクセスするようにしますよ」 という意味です
実際にpublicディレクトリにaaa.htmlなどを置いてブラウザで http://project.com/aaa.html とアクセスするとaaa.htmlの内容が表示されます。
そのほかのurlでアクセスするとindex.phpにアクセスします。

index.php

public/index.php
<?php

require_once '../bootstrap.php';

$project->run();

ここでは bootstrap.php をロードして、生成された Libs/Projectクラスのインスタンスのrun関数 を実行します。

実際にbootstrap.phpとLibs/Project.phpを作成しましょう。

Libs/Project.php
<?php
namespace Libs;


class Project
{
    private static Project $_instance;
    
    private function __construct()
    {
    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new Project();
        }

        return self::$_instance;
    }

    public function run()
    {
        echo "Project is running.";
    }
}
bootstrap.php
<?php
require_once 'Libs/Project.php';
use Libs\Project;

$project = Project::instance();

Projectクラスは一つしか存在しないことを定義したいのでSingletonパターンで実装してます。

では実際にブラウザで確認してみましょう。 http://project.com/
下記の内容が表示されていれば成功です。

Project is running.

Autoloader

今回はComposerを使用せずに進めるのでAutoloaderもついでに作っちゃいましょう。
Autoloaderとは簡単に説明すると毎回requireするのがめんどくさいので、自動でrequireしてくれるようにする機能です。
今回の本質とは一致しないのでコードの提示だけとします。

Libs/Utils/AutoLoader.php
<?php

namespace Libs\Utils;


class AutoLoader
{
    private string $system_root_dir;
    private array $applications_root_dir;

    public function __construct(string $root_dir)
    {
        $this->system_root_dir = $root_dir;
        $this->applications_root_dir = array($this->system_root_dir);
    }

    public function run()
    {
        spl_autoload_register(array($this, "loadClass"));
    }

    public function loadClass($class)
    {
        $php_file = $this->create_php_file_path($class);
        if (is_readable($php_file)) {
            require_once $php_file;
            return;
        }
    }

    private function create_php_file_path($class)
    {
        foreach ($this->applications_root_dir as $dir) {
            $pieces = array($dir);
            $class_with_name_space = ltrim($class, '\\');
            $pieces = array_merge($pieces, explode('\\', $class_with_name_space));
            $result = implode(DIRECTORY_SEPARATOR, $pieces) . ".php";
            if (is_readable($result)) return $result;
        }
        return null;
    }
}

AutoLoaderクラスにプロジェクトのルートディレクトリを渡してインスタンを生成し、実行します。

ルートディレクトリを指定したいのでConfigディレクトリにDirectorySetting.phpを作成していきます。

Config/DirectorySettings.php
<?php
namespace Config;


class DirectorySettings
{
    public const APPLICATION_ROOT_DIR = __DIR__ . DIRECTORY_SEPARATOR . ".." . DIRECTORY_SEPARATOR;
}

ではbootstrap.phpでAutoLoaderを実行するようにしましょう。

<?php
require_once 'Config/DirectorySettings.php';
require_once 'Libs/Utils/AutoLoader.php';

use Config\DirectorySettings;
use Libs\Utils\AutoLoader;


$auto_loader = new AutoLoader(DirectorySettings::APPLICATION_ROOT_DIR);
$auto_loader->run();

$project = \Libs\Project::instance();

Libs/Projectが自動でRequireされるようになりました。
ブラウザで確認して動作に問題がなければ成功です。http://project.com/

Config

次はConfigアプリを実装していきましょう。

Application.php

まずはLibs/Application.phpを作成します。
これは各Appごとを登録する際に使用します。
Appを起動する際にApplicationクラスのreadyを実行していくという仕様になります。

Libs/Application.php
<?php
namespace Libs;


class Application
{
    public function ready(){
        // Override this method in subclasses to run code when Project starts.
    }
}

では実際にApplicationクラスを継承したConfigApplicationを実装しましょう。

Config/ConfigApplication.php
<?php
namespace Config;


use Libs\Application;

class ConfigApplication extends Application
{
    public function ready()
    {
        echo "Config application is ready <br>";
    }
}

これでConfigApplicationのreadyが実行されると
Config application is ready と表示されるます。

ProjectSettings.php

プロジェクトの設定をする場所が必要になるので作成します。
ここに先ほど作成したConfigApplicationを登録しましょう。

Config/ProjectSettings.php
<?php
namespace Config;


class ProjectSettings
{
    public const IS_DEBUG = true;
    public const APPLICATIONS = [
        ConfigApplication::class,
    ];
}

Bootstrap

ではbootstrap.phpで以下のことをやっていきましょう。

  • Appの登録と初期化
  • DEBUGモードならエラーがあった時に表示するようにする。
bootstrap.php
<?php
require_once 'Config/ProjectSettings.php';
require_once 'Config/DirectorySettings.php';
require_once 'Libs/Utils/AutoLoader.php';

use Config\ProjectSettings;
use Config\DirectorySettings;
use Libs\Utils\AutoLoader;

if (ProjectSettings::IS_DEBUG)
    ini_set('display_errors', 'On');

$auto_loader = new AutoLoader(DirectorySettings::APPLICATION_ROOT_DIR);
$auto_loader->run();

foreach (ProjectSettings::APPLICATIONS as $APPLICATION){
    $application = new $APPLICATION();
    $application->ready();
}

$project = \Libs\Project::instance();

では実際にブラウザで確認してみましょう。 http://project.com/
下記の内容が表示されていれば成功です。

Config application is ready
Project is running.

STEP3: リクエストとレスポンス

次はリクエストとレスポンスの実装を行っていきます。

このSTEP3で実装・編集するファイルは以下になります。

project.com
      └── Libs/
            ├─ Https/
            │   ├─ Request.php
            │   ├─ Response.php
            │   └─ Status.php
            │
            └─ Project.php
      

レスポンス

ではまず、レスポンス用のクラスを作成し動作の確認までを行いましょう。

Libs/Https/Status.php

まず最初にLibs/Https/Status.phpを作成します
これはHttpのステータスコードやメッセージを扱うクラスです。
DjangoRestFrameworkに列挙されているステータスを格納しています。
ただ列挙しているだけなのでコピペで問題ないです。

Libs/Https/Status.php
<?php
namespace Libs\Https;


class Status
{
    public static function text($status_code)
    {
        return empty(self::TEXT[$status_code]) ? "" : self::TEXT[$status_code];
    }

    # Success
    public const HTTP_200_OK = 200;
    public const HTTP_201_CREATED = 201;
    public const HTTP_202_ACCEPTED = 202;
    public const HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203;
    public const HTTP_204_NO_CONTENT = 204;
    public const HTTP_205_RESET_CONTENT = 205;
    public const HTTP_206_PARTIAL_CONTENT = 206;
    public const HTTP_207_MULTI_STATUS = 207;
    public const HTTP_208_ALREADY_REPORTED = 208;
    public const HTTP_226_IM_USED = 226;

    # Redirect
    public const HTTP_300_MULTIPLE_CHOICES = 300;
    public const HTTP_301_MOVED_PERMANENTLY = 301;
    public const HTTP_302_FOUND = 302;
    public const HTTP_303_SEE_OTHER = 303;
    public const HTTP_304_NOT_MODIFIED = 304;
    public const HTTP_305_USE_PROXY = 305;
    public const HTTP_306_RESERVED = 306;
    public const HTTP_307_TEMPORARY_REDIRECT = 307;
    public const HTTP_308_PERMANENT_REDIRECT = 308;

    # Client Error
    public const HTTP_400_BAD_REQUEST = 400;
    public const HTTP_401_UNAUTHORIZED = 401;
    public const HTTP_402_PAYMENT_REQUIRED = 402;
    public const HTTP_403_FORBIDDEN = 403;
    public const HTTP_404_NOT_FOUND = 404;
    public const HTTP_405_METHOD_NOT_ALLOWED = 405;
    public const HTTP_406_NOT_ACCEPTABLE = 406;
    public const HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407;
    public const HTTP_408_REQUEST_TIMEOUT = 408;
    public const HTTP_409_CONFLICT = 409;
    public const HTTP_410_GONE = 410;
    public const HTTP_411_LENGTH_REQUIRED = 411;
    public const HTTP_412_PRECONDITION_FAILED = 412;
    public const HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413;
    public const HTTP_414_REQUEST_URI_TOO_LONG = 414;
    public const HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415;
    public const HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    public const HTTP_417_EXPECTATION_FAILED = 417;
    public const HTTP_422_UNPROCESSABLE_ENTITY = 422;
    public const HTTP_423_LOCKED = 423;
    public const HTTP_424_FAILED_DEPENDENCY = 424;
    public const HTTP_426_UPGRADE_REQUIRED = 426;
    public const HTTP_428_PRECONDITION_REQUIRED = 428;
    public const HTTP_429_TOO_MANY_REQUESTS = 429;
    public const HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
    public const HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451;

    # Server Error
    public const HTTP_500_INTERNAL_SERVER_ERROR = 500;
    public const HTTP_501_NOT_IMPLEMENTED = 501;
    public const HTTP_502_BAD_GATEWAY = 502;
    public const HTTP_503_SERVICE_UNAVAILABLE = 503;
    public const HTTP_504_GATEWAY_TIMEOUT = 504;
    public const HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505;
    public const HTTP_506_VARIANT_ALSO_NEGOTIATES = 506;
    public const HTTP_507_INSUFFICIENT_STORAGE = 507;
    public const HTTP_508_LOOP_DETECTED = 508;
    public const HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509;
    public const HTTP_510_NOT_EXTENDED = 510;
    public const HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511;

    public const TEXT = [
        200 => 'Ok',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        207 => 'Multi Status',
        208 => 'Already Reported',
        226 => 'Im Used',
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        306 => 'Reserved',
        307 => 'Temporary Redirect',
        308 => 'Permanent Redirect',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request Uri Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        422 => 'Unprocessable Entity',
        423 => 'Locked',
        424 => 'Failed Dependency',
        426 => 'Upgrade Required',
        428 => 'Precondition Required',
        429 => 'Too Many Requests',
        431 => 'Request Header Fields Too Large',
        451 => 'Unavailable For Legal Reasons',
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'Http Version Not Supported',
        506 => 'Variant Also Negotiates',
        507 => 'Insufficient Storage',
        508 => 'Loop Detected',
        509 => 'Bandwidth Limit Exceeded',
        510 => 'Not Extended',
        511 => 'Network Authentication Required',
    ];
}

Libs/Https/Response.php

次にLibs/Https/Response.phpを作成します。

このクラスは、実際にResponseを返す際に利用します。
ResponseにHeaderをセットしたり、結果の出力を行ったりします。
Redirectの機能もここに実装します。

Libs/Https/Response.php
<?php
namespace Libs\Https;


class Response
{
    private string $content;
    private int $status_code;
    private string $status_text;
    private array $http_headers;

    /**
     * Response constructor.
     * @param string $content
     * @param int $status_code
     * @param string $status_text
     */
    public function __construct(
        string $content = "",
        int $status_code = Status::HTTP_200_OK,
        string $status_text = '')
    {
        $this->content = $content;
        $this->status_code = $status_code;
        $this->status_text = empty($status_text) ? Status::text($status_code) : $status_text;
        $this->http_headers = array();
    }

    /**
     * Send response with header and content.
     */
    public function send()
    {
        header('HTTP/1.1 ' . $this->status_code . ' ' . $this->status_text);

        foreach ($this->http_headers as $name => $value) {
            header($name . ': ' . $value);
        }

        echo $this->content;
    }

    public function setHttpHeaders($name, $value)
    {
        $this->http_headers[$name] = $value;
    }

    public function statusCode()
    {
        return $this->status_code;
    }

    public function statusText()
    {
        return $this->status_text;
    }

    public static function redirect($location){
        $response = new self("",
            Status::HTTP_301_MOVED_PERMANENTLY);
        $response->setHttpHeaders('Location', $location);
        return $response;
    }
}

Responseクラスの引数にコンテント、ステータスコードを渡し、send関数で出力します。
Headerを別途追加したいときはsetHttpHeaders関数で追加します。
ではProject.phpを編集し、実際に確認してみましょう。

Libs/Project.php
...

class Project
{
    ...

    public function run()
    {
        $response = new \Libs\Https\Response('This is content of response.');
        $response->send();
    }
}

ついでにConfig/ConfigApplication.phpのecho文が邪魔なので消してしまいましょう。

Config/ConfigApplication.php
<?php
...

class ConfigApplication extends Application
{
    public function ready()
    {
    }
}

デベロッパーツール(ChromeならF12)でResponseのHeaderも確認しましょう。
では実際にブラウザで確認してみましょう。 http://project.com/
HttpのStatusが200になっていて、下記の内容が表示されていれば成功です。

This is content of response.

ResponseのStatusを変更したりHeaderを追加してみたりして確認してみましょう。

...

use Libs\Https\Status;

class Project
{
    ...
    
    public function run()
    {
        $response = new \Libs\Https\Response('This is content of response.', \Libs\Https\Status::HTTP_404_NOT_FOUND);
        $response->setHttpHeaders('ADD_NEW_HEADER', "ADDED");
        $response->send();
    }
}

HttpのStatusが404になっていて。
Response HeaderADD_NEW_HEADER: ADDEDが追加されていれば成功です。

リクエスト

次に、リクエスト用のクラスを作成し動作の確認までを行いましょう。

Libs/Https/Request.php

Libs/Https/Request.phpを実装します。

このクラスは以下の機能を持っています。

  • Httpメソッドタイプの取得 (POST / GET / PUT / ...)
  • GETやPOSTのデータを取得
  • HttpHeaderの取得
  • ホスト部分の取得 (project.com/aaa/bbb => project.com)
  • ドメイン以降のパスの取得 (project.com/aaa/bbb => aaa/bbb)
Libs/Https/Request.php
<?php
namespace Libs\Https;


class Request
{
    private static Request $instance;
    private array $headers;

    private function __construct()
    {
        $this->headers = getallheaders();
    }

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

        return self::$instance;
    }

    public function methodType(): string
    {
        if (is_null($this->post('_method')))
            return $_SERVER['REQUEST_METHOD'];
        return $this->post('_method');
    }

    public function get(string $name, $default = null)
    {
        if (isset($_GET[$name]))
            return $_GET[$name];
        return $default;
    }

    public function post($name, $default = null)
    {
        if (isset($_POST[$name]))
            return $_POST[$name];
        return $default;
    }

    /**
     * @param null $name
     * @return array | string
     */
    public function header($name = null)
    {
        if (empty($name))
            return getallheaders();
        return empty($this->headers[$name]) ? '' : $this->headers[$name];
    }

    public function host(): string
    {
        if (!empty($_SERVER['HTTP_HOST']))
            return $_SERVER['HTTP_HOST'];
        return $_SERVER['SERVER_NAME'];
    }

    public function requestUri(): string
    {
        return $_SERVER['REQUEST_URI'];
    }

    public function baseUrl(): string
    {
        $script_name = $_SERVER['SCRIPT_NAME'];
        $request_uri = $this->requestUri();

        if (0 === strpos($request_uri, $script_name))
            return $script_name;
        else if (0 === strpos($request_uri, dirname($script_name)))
            return rtrim(dirname($script_name));

        return '';
    }

    public function pathInfo(): string
    {
        $base_url = $this->baseUrl();
        $request_uri = $this->requestUri();

        $pos = strpos($request_uri, '?');
        if (false !== $pos)
            $request_uri = substr($request_uri, 0, $pos);

        $path_info = (string)substr($request_uri, strlen($base_url));

        return $path_info;
    }

}

では動作確認を行うためにProject.phpを編集します。

Libs/Project.php
<?php
namespace Libs;


use Libs\Https\Request;
use Libs\Https\Response;


class Project
{
    private static Project $_instance;
    private Request $_request;

    private function __construct()
    {
        $this->_request = Request::instance();
    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new Project();
        }

        return self::$_instance;
    }

    public function run()
    {
        $content = '';
        $content .= 'Method type:' . $this->_request->methodType() . '<br>';
        $content .= 'Header Connection:' . $this->_request->header('Connection') . '<br>';
        $content .= 'Host :' . $this->_request->host() . '<br>';
        $content .= 'Request uri:' . $this->_request->requestUri() . '<br>';
        $content .= 'Path info:' . $this->_request->pathInfo() . '<br>';
        $content .= 'GET name:' . $this->_request->get('name') . '<br>';
        $content .= 'GET aaa:' . $this->_request->get('aaa') . '<br>';
        $response = new Response($content);
        $response->send();
    }

}

では実際にブラウザで確認してみましょう。 http://project.com/aaa/bbb?name=John&aaa=AAA_dao
下記の内容が表示されていれば成功です。

Method type:GET
Header Connection:keep-alive
Host :project.com
Request uri:/aaa/bbb?name=John&aaa=AAA_dao
Path info:aaa/bbb
GET name:John
GET aaa:AAA_dao

STEP4: Controller

次はコントローラーの実装を行っていきます。

このSTEP4で実装・編集するファイルは以下になります。

project.com
      ├─ Config/
      │     └─ ProjectSettings.php
      │
      ├─ Libs/
      │     ├─ Controllers/
      │     │   ├─ Controller.php
      │     │   └─ NotFoundController.php
      │     │
      │     └─ Project.php
      │
      └─ TaskApp/
            ├─ Controllers/
            │   └─ TasksController.php
            │
            └─ TaskApplication.php

Libs/Controllers/

まずはLibs/Controllersディレクトリに Controller.phpNotFoundController.php を作成していきます。

Libs/Controllers/Controller.php

このクラスは以下の機能を持っています。

  • アクションの実行
  • リダイレクト
  • 404を返す機能 (後述する、NotFoundControllerのindexを返します)
Libs/Controllers/Controller.php
<?php
namespace Libs\Controllers;


use Config\ProjectSettings;
use Libs\Https\Request;
use Libs\Https\Response;

class Controller
{
    /**
     * @var Request
     */
    protected Request $_request;

    public function __construct()
    {
        $this->_request = Request::instance();
    }

    public function run($action, $params = [])
    {
        if (!method_exists($this, $action)) {
            return $this->render404("Page not found.");
        }

        return $this->$action($params);
    }

    protected function redirect($uri)
    {
        return Response::redirect($uri);
    }

    protected function render404($message='Page not found.')
    {
        $controller = ProjectSettings::NOT_FOUND_CONTROLLER;
        $controller = new $controller($message);
        return $controller->index([]);
    }
}

Libs/Controllers/NotFoundController.php

このクラスは404のResponseを生成する機能を持っています。

Libs/Controllers/NotFoundController.php
<?php
namespace Libs\Controllers;


use Libs\Https\Response;
use Libs\Https\Status;

class NotFoundController extends Controller
{
    private string $message;

    public function __construct($message='Page not found.')
    {
        parent::__construct();
        $this->message = $message;
    }

    public function index($params)
    {
        return new Response($this->message, Status::HTTP_404_NOT_FOUND);
    }

}

NotFoundControllerクラスをProjectSettings::NOT_FOUND_CONTROLLER に登録します。

Config/ProjectSettings.php
<?php
namespace Config;

use Libs\Controllers\NotFoundController;

class ProjectSettings
{
    public const IS_DEBUG = true;

    public const APPLICATIONS = [
        ConfigApplication::class,
    ];

    public const NOT_FOUND_CONTROLLER = NotFoundController::class;
}

TaskApp

コントローラの実装が完了しました。
先ほど実装したコントローラーを使って、TaskAppのコントローラーを作成 して APPを登録 して動作確認をしましょう。

TaskApp/TaskApplication.php

とりあえずTaskAppを登録するためにTaskApplication.phpを作成します。

TaskApp/TaskApplication.php
<?php
namespace TaskApp;


use Libs\Application;

class TaskApplication extends Application
{

}

作成したTaskApplicationを登録します。

Config/ProjectSettings.php
<?php
namespace Config;

use Libs\Controllers\NotFoundController;
use TaskApp\TaskApplication;

class ProjectSettings
{
    public const IS_DEBUG = true;

    public const APPLICATIONS = [
        ConfigApplication::class,
        TaskApplication::class,
    ];

    public const NOT_FOUND_CONTROLLER = NotFoundController::class;
}

TaskApp/Controllers/TasksController.php

では実際に動作するコントローラーを作成しましょう。

TaskApp/Controllers/TasksController.php
<?php
namespace TaskApp\Controllers;


use Libs\Controllers\Controller;
use Libs\Https\Response;

class TasksController extends Controller
{
    public function index($params)
    {
        return new Response("This is index of task controller.");
    }

    public function detail($params)
    {
        return new Response("This is detail of task controller. <br> id: " . $params['id']);

    }
}

では、Project.phpを編集して、動作確認を行います。

Libs/Project.php
<?php
namespace Libs;


use Libs\Controllers\Controller;
use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Https\Status;
use TaskApp\Controllers\TasksController;

class Project
{
    private static Project $_instance;
    private Request $_request;

    private function __construct()
    {
        $this->_request = Request::instance();
    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new Project();
        }

        return self::$_instance;
    }

    public function run()
    {
        list($controller, $action, $params) = $this->_selectController();
        $response = $this->_actionController($controller, $action, $params);
        $response->send();
    }

    private function _selectController()
    {
        $controller = new TasksController();
        $action = 'index';
        $params = [];
        return [$controller, $action, $params];
    }

    private function _actionController(Controller $controller, string $action, array $params)
    {
        return $controller->run($action, $params);
    }
}

以下の関数が追加されました。

  • _selectController: コントローラー、アクション、パラムを選択する関数
  • _actionController: コントローラのアクションを実行する関数

流れとしては、run関数内で 

  1. _selectControllerを実行しコントローラーとアクションを選択し
  2. その情報をもとに _actionController関数で実行し
  3. そのレスポンスを返すという流れになります。

今回の場合はTaskControllerのindexアクションが実行されます。
では実際にブラウザで確認してみましょう。 http://project.com
下記の内容が表示されていれば成功です。

This is index of task controller.

アクションをdetailに変更し動作確認をしてみましょう。

Libs/Project.php
class Project
{
    ...
    
    private function _selectController()
    {
        $controller = new TasksController();
        $action = 'detail';
        $params = ['id' => 1];
        return [$controller, $action, $params];
    }

    ...
}

上記の変更によりTaskControllerのdetailアクションが実行されます。
では実際にブラウザで確認してみましょう。 http://project.com
下記の内容が表示されていれば成功です。

This is detail of task controller.
id: 1

STEP5: View

次はViewの実装を行っていきます。
Templateファイルを読み込んでResponseとして返すという処理を行います。
Templateファイル側ではechoなどを駆使して動的に生成を行っていきます。
ようはめちゃくちゃはしょったテンプレートシステムです。

このSTEP5で実装・編集するファイルは以下になります。

project.com
      ├─ Config/
      │     └─ DirectorySettings.php
      │
      ├─ Libs/
      │     ├─ Controllers/
      │     │   └─ Controller.php
      │     │
      │     ├─ Views/
      │     │   └─ View.php
      │     │
      │     └─ Project.php
      │
      ├─ TaskApp/
      │     └─ Controllers/
      │
      └─ templates/
            └─ task/
                ├─ detail.tmp.php
                └─ index.tmp.php

Config/DirectorySettings.php

まずはテンプレートのルートディレクトリがどこか設定しましょう。

Config/DirectorySettings.php
<?php
namespace Config;


class DirectorySettings
{
    public const APPLICATION_ROOT_DIR = __DIR__ . DIRECTORY_SEPARATOR . ".." . DIRECTORY_SEPARATOR;
    public const TEMPLATES_ROOT_DIR = self::APPLICATION_ROOT_DIR . DIRECTORY_SEPARATOR . "templates" . DIRECTORY_SEPARATOR;
}

Libs/Views/View.php

では、テンプレートエンジン(笑)のかなめになる Libs/Views/View.php を実装していきます。

このクラスは以下の機能を持っています。

  • テンプレートファイルを読み込んで、実行し、コンテンツを生成する。
  • テンプレートファイル内でエスケープ機能を利用可能にする。
Libs/Views/View.php
<?php
namespace Libs\Views;


use Config\DirectorySettings;

class View
{
    protected array $_defaultData = [];

    public function __construct()
    {
        $this->_defaultData['escape'] = $this->escape();
    }

    public function render($_file_path_after_templates_dir, $_data = array())
    {
        $_file = DirectorySettings::TEMPLATES_ROOT_DIR
            . $_file_path_after_templates_dir . '.tmp.php';

        extract(array_merge($this->_defaultData, $_data));

        // echoとかの出力をため込む宣言です。
        ob_start();
        // ため込み先のバッファの上限を無効化します。
        ob_implicit_flush(0);
        require $_file;
        // ため込んだ出力を$contentに代入します。
        $content = ob_get_clean();

        return $content;
    }

    public function escape()
    {
        return function ($string, $echo = true) {
            $value = htmlspecialchars($string, ENT_QUOTES < 'UTF-8');
            if (!$echo)
                return $value;
            echo $value;
        };
    }
}

extractはここを参照してください。説明できません。
ob_start() ~ ob_get_clean()までの意味はここを参考にしてください

Libs/Controllers/Controller.php

Controllerクラスにrender関数を追加します。
テンプレートファイルを指定して、データを渡して動的にコンテンツを生成した結果をレスポンスとして返すための関数です。

Libs/Controllers/Controller.php
<?php
namespace Libs\Controllers;


use Config\ProjectSettings;
use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Views\View;

class Controller
{
   protected Request $_request;

   public function __construct()
   {
       $this->_request = Request::instance();
   }

   public function run($action, $params = [])
   {
       if (!method_exists($this, $action)) {
           return $this->render404("Page not found.");
       }

       return $this->$action($params);
   }

   protected function render($file_path_after_templates_dir, $data=[])
   {
       $view = new View();
       return new Response($view->render($file_path_after_templates_dir, $data));
   }

   protected function redirect($uri)
   {
       return Response::redirect($uri);
   }

   protected function render404($message='Page not found.')
   {
       $controller = ProjectSettings::NOT_FOUND_CONTROLLER;
       $controller = new $controller($message);
       return $controller->index([]);
   }
}

TaskApp

では実際にTaskAppで動作確認を行いましょう。
まず、コントローラでrender関数を使用するように変更しましょう。

TaskApp/Controllers/TasksController.php
...

class TasksController extends Controller
{
    public function index($params)
    {
        return $this->render('tasks/index');
    }

    public function detail($params)
    {
        $data = ['title' => 'Create web framework.', 'status' => 'DOING'];
        return $this->render('tasks/detail', $data);
    }
}

render関数の第一引数にはtemplateディレクトリ以下の.tmp.php拡張子のテンプレートファイルを指定します。
例:

  • tasks/index => templates/tasks/index.tmp.php
  • aaa/bbb/sample => templates/aaa/bbb/sample.tmp.php

templates/tasks/index.tmp.php

では実際にテンプレートファイルを用意します。
拡張子がtmp.phpのものをテンプレートファイルとして扱うように設定されています。
なのでtemplates/tasks/index.tmp.phpを作成しましょう。

templates/tasks/index.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>Tasks</h1>
This is tasks index page from template.
</body>
</html>

では実際にProjectを編集して動作確認を行いましょう。

Libs/Project.php
<?php

class Project
{
    ...
    
    private function _selectController()
    {
        $controller = new TasksController();
        $action = 'index';
        return [$controller, $action, []];
    }

    ...
}

これでテンプレートファイルからコンテントが生成されているはずです。
では実際にブラウザで確認してみましょう。 http://project.com
下記の内容が表示されていれば成功です。

Tasks
This is tasks index page from template.

templates/tasks/detail.tmp.php

次にデータの出力確認のためにdetail.tmp.phpを作成していきます。

TasksControllerでdetailテンプレートには下記のデータが渡されています。

  • 'title' => 'Create web framework'
  • 'status' => 'DOING'

これらのデータを出力するにはViewクラスで提供されている$escape関数を使用します。
動的な出力には基本的にエスケープ処理が必要です。
エスケープ処理をしないことで任意のJSを実行されたりなどのクラッキングの対象になります。
下記のファイルを参考にいろいろ試してみてください。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h2>Tasks Detail Page from Template.</h2>

<ul>
    <li>
        Title: <?php $escape($title) ?>
    </li>
    <li>
        Status: <?php $escape($status) ?>
    </li>
</ul>
</body>
</html>

では実際にProjectを編集して動作確認を行いましょう。

Libs/Project.php
<?php

class Project
{
    ...
    
    private function _selectController()
    {
        $controller = new TasksController();
        $action = 'detail';
        return [$controller, $action, []];
    }

    ...
}

これでテンプレートファイルにデータが組み込まれたコンテントが生成されているはずです。
では実際にブラウザで確認してみましょう。 http://project.com
下記の内容が表示されていれば成功です。

Tasks Detail Page from Template.
・ Title: Create web framework.
・ Status: DOING

STEP6: Router

次はRouterの実装を行っていきます。
RouterはURLによってコントローラとアクションを選択する機能を担当します。
例:

  • project.com/tasks => TaskController::index()
  • project.com/tasks/1 => TaskController::detail()

このFrameworkでは下記の手順でルートを登録していきます。

  • 各APPごとにルーティングテーブルを用意する。
  • 各APPごとのルーティングテーブルをProjectのルーターに登録する。

プロジェクトの実行時にプロジェクトのルーターからコントローラーとアクションを選択して、実行する流れになります。

自分の実装がややこしくなってしまって、いままでより複雑になりますがご了承ください。

このSTEP6で実装・編集するファイルは以下になります。

project.com
      ├─ Config/
      │     └─ ProjectSettings.php
      │
      ├─ Libs/
      │     ├─ Routing/
      │     │   ├─ Router.php
      │     │   └─ RoutingTable.php
      │     │
      │     └─ Project.php
      │
      ├─ TaskApp/
      │     ├─ Controllers/
      │     │   └─ TasksController.php
      │     │
      │     └─ RoutingTable.php
      │
      └─ templates/
            └─ task/
                ├─ detail.tmp.php
                └─ index.tmp.php

Libs/Routing/RoutingTable.php

各APPごとで設定するためのルーティングテーブルクラスを実装していきます。
このクラスは以下の機能を持っています。

  • ルーティング情報の登録
  • $urlPatternsに登録されているルーティング情報の一括登録
  • パスとメソッドタイプよって登録されているコントローラーとアクションの取得

パスとはドメイン以下のurlのことと定義します(project.com/aaa/bbb => aaa/bbb)
登録と取得の実装方法としては木構造を使用しています。
パスの区切り( / )ごとに枝をはやしていって、葉っぱにコントローラとアクションが書いてある感じです。
実際の登録方法は後述します。

Libs/Routing/RoutingTable.php
<?php
namespace Libs\Routing;


class RoutingTable
{
    protected array $urlPatterns = [];
    private array $tables = [];

    /**
     * Register all $this->urlPatterns if table is empty.
     */
    public function registerMyUrlPatterns()
    {
        if (count($this->tables) > 0)
            return;

        foreach ($this->urlPatterns as $urlPattern) {
            $this->register(
                $urlPattern[0], $urlPattern[1], $urlPattern[2],
                empty($urlPattern[3]) ? 'index' : $urlPattern[3]);
        }
    }

    public function register($pattern, $methodType, $class, $action = 'index')
    {
        if (empty($this->tables[$methodType])) {
            $this->tables[$methodType] = [];
        }
        $pieces = explode('/', $pattern);
        $current_pointer = &$this->tables[$methodType];
        foreach ($pieces as $piece) {
            if (empty($current_pointer[$piece])) {
                $current_pointer[$piece] = [];
            }
            $current_pointer = &$current_pointer[$piece];
        }
        $current_pointer = [
            'class' => $class,
            'action' => $action
        ];
    }

    /**
     * Resolve controller information like this:
     *
     *   return [
     *    'class' => SomeController::class,
     *    'action' => 'index',
     *    'params' => ['id' => 1]
     *   ]
     *
     * Return null if failed to resolve.
     *
     * @param $pathInfo
     * @param $methodType
     * @return array|null
     */
    public function resolve($pathInfo, $methodType)
    {
        if (empty($this->tables[$methodType]))
            return null;

        $params = [];
        $branch = $this->tables[$methodType];
        $pieces = explode('/', $pathInfo);
        for($i = 0; $i < count($pieces); $i++){
            $result = $this->_pickBranch($branch, $pieces[$i], $params);
            if (is_null($result))
                return null;
            $branch = $result;
        }
        if (empty($result['class']) || empty($result['action']))
            return null;
        return ['class' => $result['class'], 'action' => $result['action'], 'params' => $params];
    }

    /**
     * Return null if not found.
     *
     * @param $branch
     * @param $piece
     * @param $params
     * @return mixed|null
     */
    private function _pickBranch($branch, $piece, &$params)
    {
        if (empty($branch[$piece])) {
            return null;
        }
        $result = $branch[$piece];
        return $result;
    }
}

TaskApp/RoutingTable.php

実際に例を見たほうがはやいので、TaskAppのRoutingTableを作成してみましょう。

TaskApp/RoutingTable.php
<?php
namespace TaskApp;


use TaskApp\Controllers\TasksController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', TasksController::class, 'index'],
        ['detail', 'GET', TasksController::class, 'detail'],
    ];
}

$urlPatternsに設定しておいて後で一括で登録するという方法をとっています。
$urlPatternsには以下のように情報を登録します。

protected array $urlPatterns = [
  // ['パス', 'メソッド', コントローラのクラス, 'アクション名'],
  ['detail', 'GET', TasksController::class, 'detail'],
]

Libs/Project.php

では実際にProject.phpを編集して動作確認を行いましょう

Libs/Project.php
<?php
...

class Project
{
    ...
    
    private function _selectController()
    {
        $routing_table = new \TaskApp\RoutingTable();
        $routing_table->registerMyUrlPatterns();
        $result = $routing_table->resolve($this->_request->pathInfo(), $this->_request->methodType());

        if (is_null($result)){
            $controller = \Config\ProjectSettings::NOT_FOUND_CONTROLLER;
            return [new $controller(), 'index', []];
        }

        return [new $result['class'], $result['action'], $result['params']];
    }
    
    ...
}

では実際にブラウザで確認してみましょう。 http://project.com
下記の内容が表示されていれば成功です。

Tasks
This is tasks index page from template.

ではパスをdetailに変えてブラウザで確認してみましょう。 http://project.com/detail
下記の内容が表示されていれば成功です。

Tasks Detail Page from Template.
・ Title: Create web framework.
・ Status: DOING

次にパスを登録されていないパスに変えてブラウザで確認してみましょう。 http://project.com/ajfij
下記の内容が表示されていれば成功です。

Page not found.

Libs/Routing/Router.php

RoutingTableの動作確認はできましたね。
しかしこれではTaskAppだけしか使えません
なのでAPPごとのRoutingTableをルーターに登録するようにしましょう。

Libs/Routing/Router.php
<?php
namespace Libs\Routing;


use Libs\Https\Request;

class Router
{
    private array $routingTables = [];

    public function __construct($routingTableClasses=[])
    {
        foreach ($routingTableClasses as $routingTableClass){
            $this->add($routingTableClass[0], new $routingTableClass[1]);
        }
    }

    public function add($prefixPregPattern, RoutingTable $routingTable)
    {
        $routingTable->registerMyUrlPatterns();
        $this->routingTables[] = [
            'prefixPregPattern' => $prefixPregPattern
            , 'table' => $routingTable];
    }

    public function resolve(Request $request){
        $path_info = $request->pathInfo();
        $result = null;
        foreach ($this->routingTables as $routingTable){
            if (preg_match($routingTable['prefixPregPattern'], $path_info, $matches)){
                $current_path_info = substr($path_info, strlen($matches[0]));
                $result = $routingTable['table']->resolve($current_path_info, $request->methodType());
                break;
            }
        }

        return $result;
    }
}

先ほどのRoutingTableをこのクラスに登録していきます。

ProjectSettingsで登録するRoutingTableの設定を行います。

Config/ProjectSettings.php
<?php
namespace Config;


use Libs\Controllers\NotFoundController;
use TaskApp\TaskApplication;

class ProjectSettings
{
    public const IS_DEBUG = true;
    public const APPLICATIONS = [
        ConfigApplication::class,
        TaskApplication::class
    ];

    public const ROUTING_TABLE_CLASSES = [
        ['/^tasks(\/|)/', \TaskApp\RoutingTable::class],
    ];


    public const NOT_FOUND_CONTROLLER = NotFoundController::class;
}

RoutingTableをROUTING_TABLE_CLASSESに登録していきます。
/^tasks(/|)/は正規表現でtasksかtasks/ということになります。
URLがこのパターンにマッチしたときに登録されているRoutingTableのresolve関数を使用することになります。
例:
project.com/tasks/detail にアクセスした場合、正規表現のパターンに一致しているので
TaskApp/RoutingTableのresolve関数の引数に'detail'とメソッドタイプを渡して実行します。
ちょっとややこしいので動作確認を行いましょう。

Project.php

project.phpを編集して動作確認を行います。

Libs/Project.php
<?php
namespace Libs;


use Config\ProjectSettings;
use Libs\Controllers\Controller;
use Libs\Https\Request;
use Libs\Routing\Router;

class Project
{
    private static Project $_instance;
    private Request $_request;
    private Router $_router;

    private function __construct()
    {
        $this->_request = Request::instance();
        $this->_router = new Router(ProjectSettings::ROUTING_TABLE_CLASSES);
    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new Project();
        }

        return self::$_instance;
    }

    public function run()
    {
        list($controller, $action, $params) = $this->_selectController();
        $response = $this->_actionController($controller, $action, $params);
        $response->send();
    }

    private function _selectController()
    {
        $result = $this->_router->resolve($this->_request);
        if (is_null($result)){
            $controller = ProjectSettings::NOT_FOUND_CONTROLLER;
            return [new $controller(), 'index', []];
        }
        return [new $result['class'], $result['action'], $result['params']];
    }

    private function _actionController(Controller $controller, string $action, array $params)
    {
        return $controller->run($action, $params);
    }
}

では実際にブラウザで確認してみましょう。 http://project.com/tasks
下記の内容が表示されていれば成功です。

Tasks
This is tasks index page from template.

ではパスをdetailに変えてブラウザで確認してみましょう。 http://project.com/tasks/detail
下記の内容が表示されていれば成功です。

Tasks Detail Page from Template.
・ Title: Create web framework.
・ Status: DOING

次にパスを登録されていないパスに変えてブラウザで確認してみましょう。 http://project.com/
下記の内容が表示されていれば成功です。

Page not found.

RoutingTableの拡張

とりあえずRoutingができるようになりましたのでちょっと拡張していきましょう。
WebFrameworkでよくみるパスに以下のようなものがあると思います。

  • tasks/1 => idが1のタスクを表示する
  • tasks/2 => idが2のタスクを表示する

上記のように動的にパスが指定できるやつです。
これがないと大変不便なのでこれを実装していきたいと思います。

Libs/Routing/RoutingTable.php

パスに動的な数字か文字列をしていできるようにします。

例:

  • detail/str:name => str:nameの部分が可変になります。
  • detail/int:id => int:idの部分が可変になります。
  • Controllerのアクションのparamsに['id' => ?]と渡るようにします。
  • detail/2 とパスが来たらparamsは ['id' => 2]となります

_pickBranch関数以下を変更します。

Libs/Routing/RoutingTable.php
<?php
...

class RoutingTable
{
    ...
    
    private function _pickBranch($branch, $piece, &$params)
    {
        if (empty($branch[$piece])) {
            list($real_piece, $params) = $this->_pickIntParam($branch, $piece, $params);
            if($real_piece === false){
                list($real_piece, $params) = $this->_pickStrParam($branch, $piece, $params);
            }
            if($real_piece === false)
                return null;
            $piece = $real_piece;
        }
        $result = $branch[$piece];
        return $result;
    }


    /**
     * @param $branch
     * @param $piece
     * @param $params
     * @return array
     */
    private function _pickIntParam($branch, $piece, $params)
    {
        return $this->_pickParam($branch, $piece, $params, '/^\d+$/', 'int');
    }

    private function _pickStrParam($branch, $piece, $params)
    {
        return $this->_pickParam($branch, $piece, $params, '/^.+$/', 'str');
    }

    /**
     * @param $branch
     * @param $piece
     * @param $params
     * @param $value_pattern
     * @param $value_type
     * @return array
     */
    private function _pickParam($branch, $piece, $params, $value_pattern, $value_type){
        if (preg_match($value_pattern, $piece)) {
            foreach (array_keys($branch) as $key) {
                if (preg_match('/' . $value_type . ':(.+)/', $key, $matches)) {
                    $params[$matches[1]] = $piece;
                    $piece = $key;
                    return [$piece, $params];
                }
            }
        }
        return [false, false];
    }
}

では実際にTaskApp/RoutingTable.phpのパスを変更していきます。

TaskApp/RoutingTable.php
...

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['str:name', 'GET', TasksController::class, 'index'],
        ['detail/int:id', 'GET', TasksController::class, 'detail'],
    ];
}

TaskControllerも変更します。

TaskApp/Controllers/TasksController.php
...

class TasksController extends Controller
{
    public function index($params)
    {
        $data = ['name' => $params['name']];
        return $this->render('tasks/index', $data);
    }

    public function detail($params)
    {
        $data = ['id' => $params['id'], 'title' => 'Create web framework.', 'status' => 'DOING'];
        return $this->render('tasks/detail', $data);
    }

}

テンプレートも動作確認のため変更します。

templates/tasks/index.tmp.php
...

<body>
<h1>Tasks</h1>
This is tasks index page from template.<br>
Name: <?php $escape($name) ?>
</body>

...
templates/tasks/detail.tmp.php
...

<body>
<h2>Tasks Detail Page from Template.</h2>

<ul>
    <li>
        ID: <?php $escape($id) ?>
    </li>
    <li>
        Title: <?php $escape($title) ?>
    </li>
    <li>
        Status: <?php $escape($status) ?>
    </li>
</ul>
</body>
...

ブラウザで確認してみましょう。 http://project.com/tasks/john
最後の名前の部分を変えて表示が変わるか確認しましょう。
下記の内容が表示されていれば成功です。

Tasks
This is tasks index page from template.
Name: john

次にdetailをブラウザで確認してみましょう。 http://project.com/tasks/detail/2
最後のID部分を変えて表示が変わることを確認しましょう。
下記の内容が表示されていれば成功です。

Tasks Detail Page from Template.
・ ID: 2
・ Title: Create web framework.
・ Status: DOING

まとめ

1部でとりあえずルーターまで進めることができました。
ここまでの内容でDBとか使わないものなら作れちゃいそうですね。

2部目の内容としましては、DBを使ったTaskAppの作成、Sessionを利用したユーザー認証機能の作成、Middlewareの実装という風に考えてます。
PART2はこちら

31
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?