PHP --- フレームワークを利用しないでToDoアプリ作成(2)

前回の記事にて、Viewをレンダリングして、レスポンスを生成して返すところまで実装しました。
PHP --- フレームワークを利用しないでToDoアプリ作成(1) - 何でもプログラミング

今回はURLに応じて表示する内容を変更するよう実装してみたいと思います。

また、ページ毎に同じ内容が表示できるよう、レイアウトの対応もしてみたいと思います。

今回の作成物

ページ共有部分を定義するlayout.phpと、適当な2つのページpage1.php、page2.php、コントロール部分のindex.phpからなります。

index.phpでは、リクエストのURL毎にレスポンスを生成する関数を定義してあり、URLと関数の対応を連想配列で定義してあります。

連想配列のキーでは、コロン( : )で始まる部分はそのまま関数の引数として渡されます。(今回は名前に意味はなく、出現順で引数に渡されます。)

また例外が生じた際のレスポンスを決める関数も定義してあります。

レイアウトの適用は、前記事でも利用したob_start、ob_get_cleanの仕組みを利用して、各ページで行っています。

またcompact関数は、変数を連想配列に変換してくれる関数です。


layout.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title><?php safeEcho($title) ?></title>
</head>
<body>
<header>
    <p>Site header</p>
</header>
<main>
<?php echo $body ?>
</main>
</body>
</html>

page1.php

<?php ob_start(); ob_implicit_flush(0); ?>

<h1>Page1</h1>
<p><?php safeEcho($text) ?></p>

<?php
$title = 'page1';
$body  = ob_get_clean();
require 'layout.php';
?>

page2.php

<?php ob_start(); ob_implicit_flush(0); ?>

<h1>Page2</h1>
<p><?php safeEcho($text) ?></p>

<?php
$title = 'page2';
$body  = ob_get_clean();
require 'layout.php';
?>

index.php

<?php
function page1(string $text) : Response {
    $content = render('page1.php', compact('text'));
    return Response::ok($content);
}

function page2(string $text) : Response {
    $content = render('page2.php', compact('text'));
    return Response::ok($content);
}

function onError(Exception $e) : Response {
    if ($e instanceof Exception404) {
        return Response::notFound('404 Not Found');
    } else {
        return Response::internalServerError('500 Internal Server Error');
    }
}

$routesGET = [
    '/page1/:text' => 'page1',
    '/page2/:text' => 'page2'
];

respond($_SERVER['PATH_INFO'], $routesGET, [], 'onError');

結果は下記のようになります。

f:id:any-programming:20180622154507p:plain f:id:any-programming:20180622154419p:plain f:id:any-programming:20180622154627p:plain


respond関数

入力のPathInfoに対し、適合するルートを探してコールバックを呼び、レスポンスを送出する関数になります。

今回はGETとPOSTのみ想定しており、別々の連想配列を受け取るようにしてあります。

<?php
function respond(string $pathInfo, array $routesGET, array $routesPOST, callable $onError) : void {
    try {
        if ($_SERVER['REQUEST_METHOD'] === 'GET') {
            $response = getResponse($pathInfo, $routesGET);
        } else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $response = getResponse($pathInfo, $routesPOST);
        }
        if (isset($response)) {
            $response->send();
        } else {
            throw new Exception404();
        }
    } catch (Exception $e) {
        $onError($e)->send();
    }
}

class Exception404 extends Exception {};

getResponseは下記のように定義してあります。

適合するルートがあれば、call_user_func_arrayでコールバックを呼び出しています。

<?php
function getResponse(string $pathInfo, array $callbackMap) : ?Response {
    foreach ($callbackMap as $route => $f) {
        $matches = matchRoute($route, $pathInfo);
        if (isset($matches)) {
            return call_user_func_array($f, $matches);
        }
    }
    return null;
}

matchRouteは下記のように定義してあります。

コロンで始まる場所を、正規表現の変数取り出しの形式に変更してからpreg_matchを呼び出しています。

matchesの最初の要素にはマッチした全体が格納されているので、取り除いています。

<?php
function matchRoute(string $route, string $pathInfo) : ?array {
    $pattern = preg_replace('#:[^/]+#', '([^/]+)', $route);
    $matches = [];
    if (preg_match("#^{$pattern}$#", $pathInfo, $matches) === 1) {
        array_shift($matches);
        return $matches;
    } else {
        return null;
    }
}


モバイル判定

HTTP_USER_AGENTを利用することにより、リクエストがモバイル端末からかどうかを判定することができます。

PCとモバイルで表示を変える際に利用することができます。

<?php
function isMoble() : bool {
    $userAgent = $_SERVER['HTTP_USER_AGENT'];
    // Android
    if (str_contains($userAgent, 'Android') && str_contains($userAgent, 'Mobile')){
        return true;
    }
    // others
    foreach ([ 'iPhone', 'iPod', 'Windows Phone', 'BlackBerry' ] as $name) {
        if (str_contains($userAgent, $name)) {
            return true;
        }
    }
    return false;
}
function str_contains(string $str, string $word) : bool {
    return strpos($str, $word) !== false;
}

.htaccessを利用している場合は、下記を追記するとデバイスで表示が変わることを明記できます。

Header set Vary User-Agent


PathInfo

今回は$_SERVERのPATH_INFOを利用しましたが、環境によっては利用できないことがあります。

回避の一例として、URLのパラメータとして渡した場合を紹介してみたいと思います。

<?php
function getPathInfo() : string {
    $pathInfo = '/';
    if (isset($_SERVER['PATH_INFO'])) {
        $pathInfo = $_SERVER['PATH_INFO'];
    } else if (isset($_GET['path-info'])) {
        $pathInfo = $_GET['path-info'];
    }
    return $pathInfo;
}

.htaccess

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.+)$ index.php?path-info=/$1 [L]
</IfModule>