LoginSignup
0
0

More than 1 year has passed since last update.

パーフェクトPHP ミニブログアプリケーション

Last updated at Posted at 2022-12-25

デモサイト

demo

開発環境

  • xampp

  • アプリケーション名

    • MiniBlog
  • 配置場所

    • xamppのhtdocsの直下
  • ヴァーチャルホストの追加

    • ヴァーチャルホストの追加は追加せず localhostでいく
      • パーフェクトPHPでは ヴァーチャルホストの追加している
        • http://Mini-blog.localhost
  • ドキュメントルート

    • ドキュメントルートは変更せず アプリケーションが入っているディレクトリにはアクセスできないように.htaccessで制限する。
      • xampp の htdocs のまま。
      • パーフェクトPHPではドキュメントルートを変更をしている
        • htdocs/MiniBlog/webがドキュメントルートになる。
  • ヴァーチャルホストの追加とドキュメントルートの変更

  • MVCモデルによる役割の分離

    • model と view と controller で ファイルを分割して管理している。
      image.png
      image.png
  • フロントコントローラー
    image.png

web ディレクトリーにリライトさせる

  • /MiniBlog/web/フォルダを 事実上の ドキュメントルートにするため
確認のための構成
MiniBlog
├── web <- ここ以下のフォルダ以外のアクセスはリライトさせる。
│   ├── css
│   │   └── style.css
│   ├── img
│   │   └── img01.png
│   ├── index.php
│   └── .htaccess
├── .htaccess <-- リライト処理するファイル
└── file.php <-webフォルダ以外のファイルはURLからアクセスできないようにする
  • .htaccessはApacheの設定を変更できるファイル
  • MiniBlog 以下のディレクトリすべてに適用される。
    • url から MiniBlog 以下にアクセスされれば、/MiniBlog/web/ にインナーリダイレクトさせる。
.htaccess
<IfModule mod_rewrite.c>
# リライト機能を有効にする設定
    RewriteEngine On
    RewriteRule ^(.*)$ /MiniBlog/web/$1 [QSA,L]
# ^(.*)$ の対象文字列は .htaccess がおかれた 場所の 相対パス になる。
#  隣の /MiniBlog/web/$1 は リダイレクト先のパス
# リダイレクト先のパスは ドメイン以降のパスで指定 http://localhost '/以降のパス'
#                             または、httpから指定する。
#                             または相対パスでも指定できる。
# $1 は この(.*) 後方参照された 相対パス になる。
# preg_matchにおける2番目の引数はデフォルトなのでを省略している感じ。 
#   -> preg_match('/(.*)+/','MiniBlogを含まないpathが対象文字列');
# マッチしたら 隣の /MiniBlog/web/$1 [QSA,L] に インナーリダイレクトする。
</IfModule>
  • このままではweb以下のディレクトリまでwebディレクトリにリダイレクトされ永久ループになる。
  • web以下のディレクトリはファイルがあればアクセスできるように変更する
    • ファイルがなければ、web\index.php (フロントコントローラー)にアクセスされるようにすることで、ループすることなく最終的にリダイレクトは終了する
    • URLに対応するファイルが存在しない場合 index.phpにアクセスされるため url から index.php を 隠すことができる。
web\.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ /MiniBlog/web/index.php [QSA,L]
</IfModule>
  • リライト機能の動作確認
web\index.php
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>web/index.php</title>
<?php // hrefやsrcは相対パスで指定してもOK       ?>
<?php // ただ一般的にはコンテキストパスで指定する。     ?>
    <link rel="stylesheet" href="/MiniBlog/css/style.css">
</head>
<body>
    <h1>ミニブログ</h1>
<?php // 適当な画像 ?>
    <img src="/MiniBlog/img/01.png" alt="">
</body>
</html>
web\css\style.css
body{
    background: green;
}
file.php
<h1>file</h1>
  • MiniBlogにアクセスすると
    image.png
  • /MiniBlog/web/index.php ファイルにインナーリダイレクトされindex.phpがレスポンスされる。
    image.png
  • 内部のスタイルも
    image.png
  • このurlにインナーリダイレクトされる。
    image.png
  • 画像も同様にインナーリダイレクトされる。
    image.png
    image.png
  • 赤文字のパスがそのまま後方参照されている。
    • パールによる正規表現が使われている。
      image.png
  • web ディレクトリにリライトされるとオレンジの処理によってファイルが存在する場合はリライトせずそのままファイルが取得される。
    image.png
  • file1.php を 指定した場合
    image.png
  • /MiniBlog/web/index.php が レスポンスされる。
    image.png
  • web ディレクトリには file1.php が存在しないため、/MiniBlog/web/index.php にインナーリダイレクトされるため

image.png

  • .htaccess の 適用順位
    • URL http://localhost/MiniBlog/web/index.php にアクセスした場合
      • 上は web ディレクトリに直接アクセスされている
      • web ディレクトリには web/.htaccessがおかれている
      • web/.htaccess では改めて リライト処理 を 定義しているため
      • その上の .htaccess の リライト処理は 適用されず 無視 される。
      • 以下の処理は web ディレクトリでは適用されないので
        image.png

ClassLoaderクラスの作成

  • クラスファイルを自動で読み込むために必要なプロパティとメソッドを集めたクラス
  • php では他ファイルは読み込むことで アクセス することができるようになる。
    file1.php
     // 配列だけを返すファイル
     return [
        'a' => 'A'
      ];
    
    file2.php
     $aaa = require 'file1.php';
    
     print_r($aaa);
     // Array( 'a' => 'A')
    
.core/ClassLoader.phpの作成
.
├── core
│   └── Classloader.php (作成)
│   └── Application.php (作成)
└── bootstrap.php (作成)
core\ClassLoader.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class ClassLoader
{
// クラスを検索するディレクトリのフルパスを格納する
    protected $dirs=[];

    /**
     * spl_autoload_register 関数 を実行するメソッド
     * この関数実行以後 未登録のクラスが new されると
     * 引数に登録したコールバック関数が自動で実行される。
     */
    public function register()
    {
// コールバック関数のloadClass()の引数には発火させた未登録のクラス名が自動で入る
// 実引数のarray(インスタンス,メソッド) はインスタンスのメソッドをコールバック関数で実行する時の書き方
// array(objct,'メソッド')(); <- メソッドを文字列で実行できる。
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     * オートロード対象のディレクトリのフルパス登録
     * このアプリケーションでは
     * coreディレクトリ や modelsディレクトリ  の フルパスが $dir になる。
     *
     * @param string $dir
     */
    public function registerDir($dir)
    {
        $this->dirs[] = $dir;
    }

    /**
     * CB の引数には ネームスペースを含んだクラス名が渡ってくる
     * クラス名からフルパスを作成して
     * フルパスが読み込めたら読み込んで終了させる。
     * 読み込めなかったら 未定義のクラスが new されたとエラーがはかれる。
     * @param string $class
     */
    public function loadClass($class)
    {
        foreach ($this->dirs as $dir) {
            $file = $dir . '/' . $class . '.php';
            if (is_readable($file)) {
                require_once $file;
// return は 関数(メソッド)を終了させる。
// foreach から 抜ける場合は break
                return;
            }
        }
    }
}
  • bootstrap.php
    • オートローダーを実行するファイル
    • bootstrap.phpの配置場所について ※パーフェクトPHP p.207
      • bootstrapにはアプリケーションを立ち上げるための動作という意味がある
      • アプリケーションを実行するにあたってオートロードはまず最初に行う処理である
      • また、オートロードの他に特別に必要な前処理が出てきた場合には、それらを実行するための処理を記述する場所でもある
      • フレームワークは特定のアプリケーションに依存しない共通処理をまとめたものなので、ルートディレクトリ(階層型ファイル構造の最上階層のディレクトリのこと)の直下に配置するのが望ましい
bootstrap.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */

require 'core/ClassLoader.php';

$loader = new ClassLoader();
// オートロードの対象ディレクトリの登録
$loader->registerDir(dirname(__FILE__).'/core');
$loader->registerDir(dirname(__FILE__).'/models');
// オートローダーの起動
$loader->register();
  • dirname(__FILE__) について
    • dirname(__FILE__)__DIR__ は同じ
    • basename(__FILE__) で ファイル名が取得できる。
      • dirname(__FILE__)./core./クラス名.php
        • dirname(__FILE__)./core を dirsプロパティに保存して
        • この クラス名 を オートローダーで取得し読み込んで処理する。
core\Application.php
<?php

class Application
{
    public function run(){
        echo 'hello world';
    }
}

フロントコントローラーの作成

ディレクトリ構成
.
├── core
│   └── Classloader.php
├── web 
│   ├── index.php   (作成)
│   └── .htaccess   (作成して👆のコードをコピペ)
├── .htaccess       (作成して👆のコードをコピペ)
└── bootstrap.php
  • 全てのリクエスト(web/index.php)を受けとるファイル。
  • ファイルの読み込みを一ヵ所で記述できる点でページコントローラーより効率的になる。
web\index.php
<?php
require_once __DIR__.'/../bootstrap.php';
// new したタイミングで オートロードしてくれるため改めて読みこむ処理が不要
$app = new Application();
$app->run();
  • http://localhost/MiniBlog/ にアクセスして hello world が出力されていればOK
    image.png

アプリケーションクラスの作成

ディレクトリ構造
.
├── core
│   ├── Application.php 
│   └── Classloader.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php (作成)
  • アプリケーションクラス(フロントコントローラーの機能を一部 委譲させている)
    • デバックモードを設定するメソッド
    • 各クラスの絶対パスを取得するメソッド
    • 全てのクラスを初期化するためのメソッド
    • メインルーチンを実行するメソッド
      1. ルータークラスから
      2. リクエストに応じた
      3. コントローラークラスを初期化してメソッドを実行
      4. その中でmodelクラスでデータを取得したり
      5. viewクラスでHTMLを作成して
      6. それをレスポンスクラスに渡して
      7. 最後にユーザーにレスポンスする。
core/Application.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */

// abstract 使用するには クラスを抽象化 させる必要がある
// abstract すると 直接インスタンス化できない。
abstract class Application
{
    protected $debug = false;
    /**
     * コンストラクタ
     * @param boolean $debug
     */
    public function __construct($debug = false)
    {
        $this->setDebugMode($debug);
    }

    /**
     * デバッグモードか判定
     *
     * @return boolean
     */
    public function isDebugMode()
    {
        return $this->debug;
    }

    /**
     * デバッグモードを設定
     * @param boolean $debug
     */
/** web/index.php 
* 本番でエラーが出ないよう
* $app = new MiniBlogApplication(false);
*/
/** web/index_dev.php 
* 開発環境ではエラーが出るよう
* $app = new MiniBlogApplication(true);
*/
    protected function setDebugMode($debug)
    {
        if ($debug) {
            $this->debug = true;
            ini_set('display_errors', 1);
            error_reporting(-1);
        } else {
            $this->debug = false;
            ini_set('display_errors', 0);
        }
    }


// abstract することで
// 継承クラスで必ずgetRootDir()の記述するよう親クラスで強制できる
// プロパティはabstractできない
    abstract public function getRootDir();

    /**
     * ドキュメントルートへのパスを返す
     * パーフェクトPHPではwebディレクトリがドキュメントルート
     *
     * @return string
     */
    public function getWebDir()
    {
      //abstractメソッドは具体的な処理を継承クラスで記述しながら
      //普通に親クラスで使用することができる。
        return $this->getRootDir() . '/web';
    }

    /**
     * コントローラファイルが格納されているディレクトリへのパスを取得
     *
     * @return string
     */
    public function getControllerDir()
    {
        return $this->getRootDir() . '/controllers';
    }

    /**
     * ビューファイルが格納されているディレクトリへのパスを取得
     *
     * @return string
     */
    public function getViewDir()
    {
        return $this->getRootDir() . '/views';
    }

    /**
     * モデルファイルが格納されているディレクトリへのパスを取得
     *
     * @return string
     */
    public function getModelDir()
    {
        return $this->getRootDir() . '/models';
    }

/**
 * アプリケーションを実行する
 */
    public function run()
    { 
        $root_path = $this->getRootDir();    
        // 出力
        echo $root_path;
    }
}
  • 継承クラス
    • 具体的な処理は作成するプロジェクトに任せる。
MiniBlogApplication.php
<?php

class MiniBlogApplication extends Application
{
// 継承クラスでは必ずgetRootDir()が記述する必要がある
    public function getRootDir() // 階層型ファイル構造の最上階層のディレクトリのこと
    {
// 自身がいるディレクトリの絶対パスを返す。
//MiniBlogApplication.phpのbasename(ファイル名)を除いたパスがかえる。
       return dirname(__FILE__); 
    }
}
  • web/index.phpを変更
web/index.php
<?php
require '../bootstrap.php';
// autoloaderの対象ディレクトリと ことなるため読み込む必要がある
require '../MiniBlogApplication.php';

// debugmodeをtrue
$app = new MiniBlogApplication(true);
$app->run(); // runメソッドの実行

※確認 http://localhost/MiniBlog/index.php

  • MiniBlogApplication.phpのdirname(__FILE__)echo されている。
    image.png
👆パス構成
 MiniBlogApplication  => $this->getRootDir() = dirname('__FILE__')
// 1つ下の階層に配置している。
    ├── controllers   => $this->getRootDir() + '/controllers'
    ├── core          => $this->getRootDir() + '/core'
    ├── models        => $this->getRootDir() + '/models'
    ├── views         => $this->getRootDir() + .'/views'
    └── web           => $this->getRootDir() + '/web'

helper関数の作成

core\functions.php
<?php

function dd()
{
    // header("Content-type: text/plain; charset=UTF-8");
    $args = func_get_args();
    foreach ($args as $arg) {
        echo '<pre>';
        // print_r($arg);
        var_dump($arg);
        echo '</pre>';
    }

    exit;
}
  • web/index.php
    • フロントコントローラーで読み込む (クラスでないためオートロードしない)
    • phpでは 関数は必ずグローバル空間に配置されるためファイルを読みこめば、そのままアクセスが可能になる。
web/index.php
<?php
// -
// require '../bootstrap.php';
// require '../MiniBlogApplication.php';
//+
// 相対パスから絶対パスに書き換え
require __DIR__.'/../bootstrap.php';
require __DIR__.'/../MiniBlogApplication.php';

//+
require __DIR__.'/../core/functions.php';

$app = new MiniBlogApplication(true);
$app->run();

Routerクラスの作成の前に

  • 動的ルーティングにするため正規表現を使用しているための最低限の正規表現の解説

正規表現について

  • URLの正規表現のデリミタは'/'ではなく'#'を使う
    • urlを正規表現する時の注意点
    • ようするに URLの区切り文字であるこれ / はメタ文字の/と区別する必要がないためエスケープする必要がなくなる
      • \/こうではなく普通に / これでOKになる。
    • preg_match('#^'.$request_url.'$#', $url, $matches);
    • バリデーションで行頭と行末を表すメタ文字は^$ではなく\A\zのほうがセキュリティ的にはいいらしい
  • 動的ルーティングに対応するため正規表現のパターンには名前付きキャプチャを利用する
  • パターン例 : '#^user/(?P<username>[^/]+)$#'だった場合
    • (?P<username>[^/]+) <- この部分が名前付きキャプチャ
    • ()の後方参照 と 一緒につかう
      • 後方参照 とは
        • ()がないパターン : #^user/[^/]+$#
        • ()があるパターン : #^user/([^/]+)$#
          • ^user/ 行頭はuser/で始まりその後の行末までの文字列は
          • [^/]+$ 行末までの1文字以上の文字列なかに/が含まれていないならマッチする。
php 後方参照であるときとない時
<?php
function dd()
{
    header("Content-type: text/plain; charset=UTF-8");
    $args = func_get_args();
    foreach ($args as $arg) {
        print_r($arg);
    }
    exit;
}
// 行頭から行末まで `user/` からはじまり `/` が含まれていない文字列(山田太郎)で終わっている。なので対象文字列は パターンと完全一致 している。
$url = 'user/山田太郎';
// 後方参照ではない時
$pattern1 = '#^user/[^/]+$#';
// 後方参照である時
$pattern2 = '#^user/([^/]+)$#';

preg_match($pattern1, $url, $matches1);
preg_match($pattern2, $url, $matches2);

dd($matches1,$matches2);
結果
// 後方参照なし
Array
(
    [0] => user/山田太郎
)
// 後方参照あり
Array
(
// パターンと一致したら一致したところがかえる。
    [0] => user/山田太郎
// 後方参照と一致した部分を抜き出してくれる。
// [0] 以降にかえってくる
    [1] => 山田太郎
)
  • 名前付きキャプチャ
    • (?P<名前>名前付き返却したい正規表現のパターン)になる
    • 名前付きキャプチャのためusernameというkeyで返してくれる
php
<?php

function dd()
{
    header("Content-type: text/plain; charset=UTF-8");
    $args = func_get_args();
    foreach ($args as $arg) {
        print_r($arg);
    }
    exit;
}

$pattern = '#^user/(?P<username>[^/]+)$#';
$url1 = 'user/山田太郎';
$url2 = 'user/山田太郎/二世';
preg_match($pattern, $url1, $matches1);
preg_match($pattern, $url2, $matches2);

dd($matches1,$matches2);
print_r ( $matches );
Array
(
// 一致したら [0] には user/文字列 がかえる
    [0] => user/山田太郎  

// [1] 以降に 後方参照のパターンとマッチした文字列がかえる
// 名前付きキャプチャにすると 名前とインデックス 両方でかえる
    [username] => 山田太郎
    [1] => 山田太郎
)
// $url = 'user/山田太郎/二世' この場合は一致しないので空の配列がかえる
Array()

ルートを登録するメソッド

  • ルートはアプリケーション固有の情報のためMiniBlogApplicationクラスに実装
    • larabel でいうと routes/web.php ファイルにあたる
core\Application.php
<?php
abstract class Application
{

//+    
    /**
     * ルーティングを登録するための abstract メソッド
     * @return array
     */
    abstract protected function registerRoutes();

    /**
     * アプリケーションを実行する
     */
    public function run()
    {
// 修正 ルーティング情報を返すメソッド
         dd($this->registerRoutes());
    }
}

MiniBlogApplication.php
<?php

class MiniBlogApplication extends Application
{

//+
//ルートの登録 
    protected function registerRoutes()
    {
        return array(
            '/'
            => array('controller' => 'status', 'action' => 'index'),
            '/status/post'
            => array('controller' => 'status', 'action' => 'post'),
            // :user_name <= 動的に変更したいパスは ':' 先頭にcolonをつける
            '/user/:user_name'
            // 上は '/user/(?p<user_name>[^/]+)' になる。 
            => array('controller' => 'status', 'action' => 'user'),
            '/user/:user_name/status/:id'
            // 上は '/user/(?p<user_name>[^/]+)/status/(?p<id>[^/]+)' になる。
            => array('controller' => 'status', 'action' => 'show'),
            '/account'
            => array('controller' => 'account', 'action' => 'index'),
            '/account/:username'
            => array('controller' => 'account', 'action' => 'show'),
            '/account/:action'
            // 上は '/account/(?p<action>[^/]+)' になる
            => array('controller' => 'account'),
            '/follow'
            => array('controller' => 'account', 'action' => 'follow'),
        );
    }
}
  • 結果
    image.png

  • 👆 : コロンをつけたパスは [^/]+ (/を含まない任意の文字列)に変更してどのような(/以外の)値でもmatchするよう変更する。

Routerクラスを作成する

Routerクラスとは

.
├── core
│   ├── Application.php
│   ├── Classloader.php
│   ├── functions.php
│   └── Router.php          (作成)
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php
  • リクエストされたurl登録してあるルートを正規表現でマッチングするためのプロパティとメソッドをまとめたクラス
  • compileRoutes()
    • 登録したルートのキーを名前付き参照パターンに変更して配列にするメソッド
      • /user/:user_name/user/(?P<user_name>[^/]+)に変換するメソッド
  • resolve()
    • リクエストされたURLでコントローラーとアクションの配列を返すメソッド
core\Router.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */

class Router
{
    protected $routes;

    /**
     * コンストラクタ
     * @param array $definitions
     *  $definitionsはアプリケーションクラスで登録したルートの配列
     */
    public function __construct($definitions)
    {
        $this->routes = $this->compileRoutes($definitions);
    }

    /**
     * ルーティング定義配列を内部用に変換する
     *
     * @param array $definitions
     *     $definitionsはアプリケーションクラスで登録したルートの配列
     * @return array
     *  array は $definitionsのキーをパターンに変更した新しいルートの配列
     */
    public function compileRoutes($definitions)
    {
        $routes = array();
// $url:'/user/:user_name/status/:id'
// $params:['controller'=>'status','action'=>'index']
        foreach ($definitions as $url => $params) {
            $tokens = explode('/', ltrim($url, '/'));
            foreach ($tokens as $i => $token) {

// $tokenの最初の文字が':'だったら
                if (0 === strpos($token, ':')) {
// ':'を削除してuser_name を取得
                    $name = substr($token, 1);
// (?P< $name >[^/]+) に 文字列を変換する
// user_name を (?p<user_name>[^/]+) に変更
                    $token = '(?P<' . $name . '>[^/]+)';
                }
// [0 =>'user, 1 => '(?p<user_name>[^/]+)', 2 =>'status', 3 => '(?p<id>[^/]+)']
                $tokens[$i] = $token;
            }
// '/user/(?p<user_name>[^/]+)/status/(?p<id>[^/]+)'
            $pattern = '/' . implode('/', $tokens);
            $routes[$pattern] = $params;
        }

        return $routes;
    }

    /**
     * 指定されたPATH_INFOを元に上記$paramsを特定する
     * $path_infoは /user/山田太郎 などの リクエストされたurl
     * $paramsはコントローラーやアクションを登録したroutes の値
     * @param string $path_info
     * @return array|false
     */
    public function resolve($path_info)
    {
// $path_info は 上記説明では urlの user/山田太郎 のこと
// $path_infoの最初の文字が / ではない時
        if ('/' !== substr($path_info, 0, 1)) {
// $path_info の文字列の最初には / を必ずつけさせる 
            $path_info = '/' . $path_info;
        }

        foreach ($this->routes as $pattern => $params) {
// $patternは作り変えられたキー  '/user/(?p<user_name>[^/]+)/status/(?p<id>[^/]+)' => Array     
/* $params は 
        (
            [controller] => status
            [action] => index
        )
*/
// $path_infoは /user/山田太郎 などの リクエストされたurl
// $pattern は 上で正規表現に作り変えた文字列
// パターンは /user/(?p<user_name>[^/]+)/status/(?p<id>[^/]+)
// 対象文字列は $path_info の /user/山田太郎
            if (preg_match('#^' . $pattern . '$#', $path_info, $matches)) {
// 一致したら、
// $matchesには名前付きのキーでuser_nameやidなどの情報が入っている。
// $matches = ['username'=>'山田太郎'];
                $params = array_merge($params, $matches); 
/* $params は 
        (
            [controller] => status
            [action] => index
            ['username'=>'山田太郎']
        )
*/
// 一緒に返す
                return $params;
            }
        }

        return false;
    }
}

Applicationクラスに実装させる

core\Application.php
<?php
abstract class Application
{
//+
    protected Router $router;

    /**
     * コンストラクタ
     */
    public function __construct($debug = false)
    {
        $this->setDebugMode($debug);
//+
        $this->initialize();
    }

//+
    /**
     * 各クラスの初期化
     */
    protected function initialize()
    {
//Routerオブジェクトのコンストラクタにルートの配列を渡している。
        $this->router = new Router($this->registerRoutes());
    }

    /**
     * アプリケーションを実行する
     */
    public function run()
    {
//+
        $pathInfo1 = '';
        $pathInfo2 = '/user/山田太郎';
        $pathInfo3 = '/user/山田太郎/status/1';
        $params1 = $this->router->resolve($pathInfo1);
        $params2 = $this->router->resolve($pathInfo2);
        $params3 = $this->router->resolve($pathInfo3);
        dd($params1,$params2,$params3);
    }
}

※確認 http://localhost/MiniBlog/
image.png

  • 本来 PathInfo は URLから取得する。
    • http://localhost/MiniBlog/web/index.php/user/yamadatarou
      • MiniBlog/web/index.php フロントコントローラー以下のパスが対象
      • /user/yamadatarou$path_infoとして取得します。

Requestクラスを作成

  • 必然的に$_SERVER$_POST,$_GETのラッパークラスになる
.
├── core
│   ├── Application.php
│   ├── Classloader.php
│   ├── functions.php
│   ├── Request.php            (作成)
│   └── Router.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php
  • ユーザーからのリクエストを 制御するためのプロパティとメソッドをまとめたクラス

  • フロントコントローラーへアクセスできるURL(ベースURL)は 4 パターンある。

    • パーフェクトPHPでは xamppの ドキュメントルートを htdocsからhtdocs/MiniBlog/webに変更していますが、このアプリケーションでは変更せずにhtdocs で開発しているため ベースURLの取得を変更する必要がある。
    • http://localhost/MiniBlog/web/index.php
    • http://localhost/MiniBlog/web/
    • http://localhost/MiniBlog/
    • http://localhost/MiniBlog/index.php
  • 取得したいのはフロントコントローラー以下のパス

    • $_SEVER['REQUEST_URL']では URLのドメイン以下のパスを全て取得できる
    • このパスから フロントコントローラーまでのパス(MiniBlogからindex.php)を削除して $path_infoを取得したい。
    • そのため 入力されるフロントコントローラーのURLに応じて場合分けしてフロントコントローラーのパスを削除している。
core\Request.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp> 変更あり
 */
class Request
{
    /**
     * リクエストURIを取得
     * @return string
     */
    public function getRequestUri()
    {
// 現在のURI(ドメイン以下のパス)を返す。
// http://example.com/foo/bar/index.php(フロントコントローラー)/list?foo=bar
// foo/bar/index.php(フロントコントローラー)/list?foo=bar が リクエストURL
        return $_SERVER['REQUEST_URI'];
    }

    /** 
     * フロントコントローラーまでのパスがベースURL
     * URLにフロントコントローラーまでのフルパスが入力されている時
     * http://example.com/foo/bar/index.php(フロントコントローラー)/list
     * /foo/bar/index.php が ベースURL
     * 
     * URLにフロントコントローラーのindex.phpファイルがない時
     * http://example.com/foo/bar/list
     * /foo/bar/ が ベースURL
     * 
     * URLにフロントコントローラーまでのフルパスがない時 (普通はこれ)
     * http://example.com/list
     * ベースURLはない。
     *
     * @return string
     */
// 要するに このメソッドでは 👇 ながなが書きましたが結論はこれだけです。
// 🔴リクエストされた URL のなかから ベースURLを取得したい
    public function getBaseUrl()
    {
// http://example.com/foo/bar/index.php/list なら
// 1⃣ `/foo/bar/index.php`
// http://example.com/foo/bar/list なら
// 2⃣  `/foo/bar/`
// http://example.com/list なら
// 3⃣ ``
        $script_name = $_SERVER['SCRIPT_NAME'];
        $request_uri = $this->getRequestUri();

//1⃣ URLに$script_name が 完全に含まれるとき
        if (0 === strpos($request_uri, $script_name)) {
//http://localhost/MiniBlog/web/index.php/user/yamadataro?name=tarro
// dd($script_name); // MiniBlog/web/index.php

            return $script_name;

// 2⃣ URLにindex.php(ファイル名がない時) // MiniBlog/web/ の時
        } else if (0 === strpos($request_uri, dirname($script_name))) {
            // http://localhost/MiniBlog/web/user/yamadataro?name=tarro
// dd(rtrim(dirname($script_name), '/')); // /MiniBlog/web

            return rtrim(dirname($script_name), '/');

// 追加 url に web がない場合 // MiniBlog/index.php の時
// perfectPHPでは必要ない MiniBlog が含まれるため追加の処理
        } else if (0 === strpos($request_uri, str_replace('/web', '', $script_name))) {
// dd(str_replace('/web', '', $script_name)); // MiniBlog/index.php

            
            return str_replace('/web', '', $script_name);

// 3⃣ http://localhost/MiniBlog/user/yamadataro?name=tarro
        } else {
// MiniBlogというアプリケーション名があるため工夫する必要がある
// ドメインからindex.php含むまでのパスは ベースURLとして処理するため。
            return '/MiniBlog'; // これはよくないフレームワークのコアクラスにこのようなプロジェクトに依存するような書き方はダメ
                    // env や configファイルから取得するとかした方がいい
                    // もとのコードをあまり変更したくないので。
            //return ''; パーフェクトPHPでは空を返す。
        }
    }

    /**
     * PATH_INFOを取得
     * http://example.com/foo/bar/index.php/list?foo=bar
     * フロントコントローラー以下でクエリパラメーターを含めない /list が $PATH_INFO
     * ?foo=bar(クエリパラメーター)はpath_infoではないので削除
     *
     * @return string
     */
    public function getPathInfo()
    {
// request_urlからbase_urlとクエリパラメーターを削除して作成する。
        $base_url = $this->getBaseUrl();
// request_uri は '/base_url/path_info?query=value' でできている。
        $request_uri = $this->getRequestUri();

        if (false !== ($pos = strpos($request_uri, '?'))) {
// GETパラメーターを削除している
            $request_uri = substr($request_uri, 0, $pos);
        }

// リクエストURLからベースURLを削除して$path_infoを作る
        $path_info = (string) substr($request_uri, strlen($base_url));
        // dd($path_info); //`list`が取得できる
        // $str = 'abcdefg' substr($str,3); 返り値は 'defg'

        return $path_info;
    }
}
  • Applicationクラスに実装する
core\Application.php
<?php
abstract class Application
{
    protected Router $router;
//+
    protected Request $request;

    /**
     * アプリケーションの初期化
     */
    protected function initialize()
    {
        $this->router = new Router($this->registerRoutes());
//+
        $this->request    = new Request();
    }


//+
    /**
     * Requestオブジェクトを取得
     * @return Request
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * アプリケーションを実行する
     */
    public function run()
    {
//変更
// urlから path_infoを取得して ルータクラスに渡して $params を取得 
        $params = $this->router->resolve($this->request->getPathInfo());
        dd($params);
    }
}

※確認
urlにフロントコントローラーまでのフルパス

http://localhost/MiniBlog/web/index.php/user/yamadataro

※確認
urlにフロントコントローラーまでのディレクトリパス

http://localhost/MiniBlog/web/user/yamadataro

※確認
urlにフロントコントローラーまでのフルパスがない 1

http://localhost/MiniBlog/user/yamadataro

※確認
urlにフロントコントローラーまでのフルパスがない 2

http://localhost/MiniBlog/account
※確認
urlにフロントコントローラーまでのフルパスがない 3

http://localhost/MiniBlog/account/signup

core\Request.php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp> 
 */
// +
    /**
     * リクエストメソッドがPOSTかどうか判定
     *
     * @return boolean
     */
    public function isPost()
    {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            return true;
        }

        return false;
    }
// +
    /**
     * GETパラメータを取得
     *
     * @param string $name
     * @param mixed $default 指定したキーが存在しない場合のデフォルト値
     * @return mixed
     */
    public function getGet($name, $default = null)
    {
        if (isset($_GET[$name])) {
            return $_GET[$name];
        }

        return $default;
    }
// +
    /**
     * POSTパラメータを取得
     *
     * @param string $name
     * @param mixed $default 指定したキーが存在しない場合のデフォルト値
     * @return mixed
     */
    public function getPost($name, $default = null)
    {
        if (isset($_POST[$name])) {
            return $_POST[$name];
        }

        return $default;
    }
// +
    /**
     * ホスト名を取得
     *
     * @return string
     */
    public function getHost()
    {
        if (!empty($_SERVER['HTTP_HOST'])) {
            return $_SERVER['HTTP_HOST'];
        }
        
        return $_SERVER['SERVER_NAME'];
    }
// +
    /**
     * SSLでアクセスされたかどうか判定
     *
     * @return boolean
     */
    public function isSsl()
    {
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
            return true;
        }
        return false;
    }
  • 👆の$paramsから コントローラーファイルを読み込んで
    コントローラークラスのメソッドを実行させる。

Controllerクラスの作成

  • urlに応じて特定のコントローラークラスのアクションメソッドを実行することでリクエストに応じた処理がクライアントにレスポンスされる。
    image.png
.
├── controllers
│   └── AccountController.php  (作成 継承クラス)
├── core (フレームワーク)
│   ├── Application.php
│   ├── Classloader.php
│   ├── Controller.php        (作成)
│   ├── functions.php
│   ├── Request.php
│   └── Router.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php
  • よってコントローラーが返すのは主にVIEWになる
    • あとは特定のURLにリダイレクトさせるぐらい
core\Application.php
// core\Application.php
// 修正
    public function run()
    {
 // url から パターンに一致した コントローラー名とアクション名の連想配列を取得する
        $params = $this->router->resolve($this->request->getPathInfo());
// 一致しなかった場合
        if ($params === false) {
            echo '下記のアドレスが存在いたしません';
            echo '<br>';
            exit;
        }
// 一致したら
        $controller = $params['controller'];
        $action = $params['action'];
// runAction()メソッド コントローラークラスのアクションメソッドにパラメーターを渡して実行している。
// コントローラークラスのアクションメソッドは html を返すので
        $content = $this->runAction($controller, $action, $params);
// 返ってきた Hhml を 最後に エコーして出力する。
        echo $content;
    }

//+ 
    /**
     * 指定されたコントローラーのアクションを実行する
     *
     * @param string $controller_name
     * @param string $action
     * @param array $params
     *
     */
    public function runAction($controller_name, $action, $params = array())
    {
// ucfirst — 文字列の最初の文字を大文字にする
        $controller_class = ucfirst($controller_name) . 'Controller';
// クラス名から new クラス するメソッド findController() メソッドを発火させる。
// 戻り値は コントローラーのオブジェクトが返ってくる。
        $controller = $this->findController($controller_class);
// コントローラーが見つからなかった場合
        if ($controller === false) {
            echo 'コントローラーが見つかりません';
            exit();
        }
// コントローラーが見つかった場合
// 上で取得したコントローラーオブジェクトのrun()メソッドを実行する
// この後は 取得したコントローラークラス そのアクションメソッドで呼び出されるviewクラスを通って 最後に html が返ってくる。
        $content = $controller->run($action, $params);
// 返ってきた html は そのまま 呼び出し元の アプリケーションの run() に返す。
        return $content;
    }


//+ findController() メソッド
    /**
     * 指定されたコントローラ名から対応するControllerオブジェクトをインスタンスする
     *
     * @param string $controller_class
     * @return Controller|bool
     */
// 呼び出し元の run() には コントローラーのオブジェクト か false(失敗) を返す
    protected function findController($controller_class)
    {
// class_existメソッドで引数のクラスが定義済みかどうかを確認する
        if (!class_exists($controller_class)) {
// 未定義なら まだrequireされていないなら
            $controller_file = $this->getControllerDir() . '/' . $controller_class . '.php';
//is_readableメソッド 引数に 渡した パスから 読み込み可能か判定してくれる。
            if (!is_readable($controller_file)) {
// 読み込み不可なら
                return false;
            } else {
// 読み込みが可能なら
                require_once $controller_file;

                if (!class_exists($controller_class)) {
// ファイルを読み込んだのに ファイルの中にクラスが未定義の場合ここが実行される。
                    return false;
                }
            }
        }
// 定義済みのクラスにアプリケーションオブジェクトを引数に渡して 
// 指定されたコントローラーnew (インスタンス化) して呼び出し元の runActionメソッドに 返す。
//  public function __construct($application) <- controllerのコンストラクタ
        return new $controller_class($this);
    }
}
  • コントローラークラス (フレームワーク)
    • コントローラーで共通する処理をまとめている。
    • それぞれのコントローラーはこのクラスを継承することで重複する処理を削減できる。
core\Controller.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */

abstract class Controller
{
    protected $controller_name;
    protected $action_name;
    protected $application;
    protected $request;
    /**
     * コンストラクタ
     * @param Application $application
     */
// applicationクラスのfindController()で new される。
    public function __construct($application)
    {
// substr()で クラス名から クラス名以降のcontrollerの10文字を削除している。
// 小文字の文字列のクラス名を取得 //StatsuControllerなら status になる
        $this->controller_name = strtolower(substr(get_class($this), 0, -10));
// 注入されたapplicationクラスだけに依存するような設計になっている
        $this->application = $application;
// applicationクラスを通して取得している。
        $this->request     = $application->getRequest();
//      👆 コントローラクラスで Requestクラスを new するとそれは別のオブジェクトになる。
    }

    /**
     * コントローラーのアクションメソッドを 文字列で 実行 させるメソッド
     *
     * @param string $action
     * @param array $params
     * @return string
     *
     */
// applicationクラスのrunメソッドで呼び出しされたメソッド
    public function run($action, $params = array())
    {
        $this->action_name = $action;

// action名が index なら メソッド名は indexAction になる。
        $action_method = $action . 'Action';
// indexActionメソッドがない場合
        if (!method_exists($this, $action_method)) {
            echo "ファイルが存在しません";
            exit();
        }
// indexActionメソッドがある場合
// 文字列で関数を実行している 'indexAction'();
// $params は コントローラー名とアクション名とpathInfo(username=>山田太郎)
// 戻り値は Html <- apllicationクラスのrunActionメソッドの$contentに返す値は html である  
        $content = $this->$action_method($params);

//取得したhtmlを 呼び出し元の apllicationクラスのrunActionメソッド に返す
        return $content;
    }
}
  • 継承クラス (作成するプロジェクト)
    • AccountControllerはusersのCRUDに必要な情報を制御して返すためのコントローラークラス
controllers\AccountController.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
// コントローラーを継承したクラス
class AccountController extends Controller
{
// actionメソッドの呼び出し時引数に $params を 渡しているが、
//    $content = $this->$action_method($params);

// 実際のアクションメソッドには 仮引数が ない場合でも エラーにはならない。
      public function indexAction()
    {
// データを作成
        $user = ['name'=>'山田太郎'];
// renderメソッドに 作成した データを渡す。
// renderメソッドからはHtmlが返ってくる。
// そのhtmlをそのまま呼び出し元の コントローラークラスのrunメソッドに返す。
        return $this->render(array(
            'user'=>$user
        ));
    }
}
  • renderメソッドなどの重複する処理は フレームワークの core\Controller.php に記述する
    • 個々のプロジェクトはフレームワークの汎用的なコードを利用しながら記述していけるようになる。
core\Controller.php
<?php
//  core\Controller.php に記述する
/** 
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
// + 
    /**
     * ビューファイルのレンダリング
     *
     * @param array $variables テンプレートに渡す変数の連想配列
     * @return string レンダリングしたビューファイルの内容
     */
// xxxアクションメソッドから呼び出しされた render メソッド
    protected function render($variables = array())
    {
// $variables は 継承クラスで作成したデータ
// ここで view クラス を 取得して Html を 取得する
// とりあえず一旦ここで 簡易的にhtmlを作成して 動作確認する
        extract($variables);

        $content = <<<EOF
<body>
<h1>AccountControllerのindexAction画面</h1>
{$user['name']}
</body>
EOF;
        return $content;
    }

http://localhost/MiniBlog/accountにアクセス
http://localhost/MiniBlog/index.php/accountでもアクセス

  • html が レスポンス されれば OK.
    image.png

  • コントローラーからデータを受けっとってHTML作成するのが 👇のViewクラス

Viewクラスの作成

.
├── controllers
│   └── StatusController.php
├── core
│   ├── Application.php
│   ├── Classloader.php
│   ├── Controller.php
│   ├── functions.php
│   ├── Request.php
│   ├── Response.php
│   ├── Router.php
│   └── View.php    (作成)
├── views
│   └── sample
│       ├── 1.php         (作成)
│       ├── 2.php         (作成)
│       └── layout.php    (作成)
├── web
│   ├── index.php
│   └── test.php  (作成)
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php
  • ob_start()の確認
    • ob_start() から ob_get_contents(); または ob_end_clean();の間に出力されたデータを内部で保持する。
      • 内部でデータを保持することをバッファリングというらしい。
    • 保持したデータは ob_get_contents(); で 取得し 変数に 代入できる。
web\test.php
<?php
// 実行
ob_start();
// デフォルトだとバッファの上限が来ると自動で出力されてしまうので、無効化しておきます。
ob_implicit_flush(0);

echo '今日の天気は';
?>

<h1>曇りのちはれ</h1>

<?php
// 作成したファイルを変数コンテントにいれる。
$content = ob_get_clean();

http://localhost/MiniBlog/test.php にアクセス

  • 結果何も出力されない
    • 出力した echo '今日の天気は'; , <h1>曇りのちはれ</h1> は 内部でバッファリングされるため。
      image.png
  • $content = ob_get_clean();
    • 保持したデータを取得し変数に代入する
    • データ 代入された 変数を echo することで 出力できる。
web\test.php
<?php
ob_start();
// デフォルトだとバッファの上限が来ると自動で出力されてしまうので、無効化しておきます。
ob_implicit_flush(0);

echo '今日の天気は';
?>

<h1>曇りのちはれ</h1>

<?php
// 作成したファイルを変数コンテントにいれる。
$content = ob_get_clean();
// + それを出力する
echo $content;
  • 出力される。
    image.png
  • require + ob_start
    • ob_start() は require で 読みこんだファイルのデータも保存できる。
web\test.php
<?php
class view{
    private $world = 'world';

    public function render($path){
        ob_start();
        ob_implicit_flush(0);
        $hello = 'hello';
 
// 👇 php 変数のスコープの ルールと全く同じ
// view クラスの中で require したので viewクラスのメソッドやプロパティは
// require 'ファイル' の中でも使用できる。
// この読み込み場所からアクセスできる変数や関数も読みこみ先のファイルで使用できる。
// メソッド内で読み込まれているため ファイル内の変数は このメソッドの ローカルスコープになる。
        require __DIR__. '/../views/'.$path;

// バッファーしたデータを取得 // バッファリングの終了
        $content = ob_get_clean();

// バッファーしたデータの出力
        echo $content;
    }
    public function escape($str){
        htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
    }
}
// クラスの初期化
$view = new View();
// renderメソッドに 読みこむ ファイルのパスを指定して実行。
$view->render('sample/1.php');
  • 読みこむファイルを作成
views\sample\1.php
<h1>sample/1.php のファイル</h1>
<p><?php echo $this->escape('<script>alert("今日は涼しいです。")</script>') ?></p>

<?php
// 読みこみ元の render メソッドにも ファイルの中から当然アクセスできる。
$this->render('sample/2.php');
views\sample\2.php
<h1>sample/2.php のファイル</h1>
<h2><?php echo $hello.$this->world ?></h2>

image.png

sample/1.phechoされる流れは上と全く同じです。
sample/2.phpについて

  • 👆 sample/2.phpsample/1.php の ファイルの中で 最終的に echo される。
    image.png

  • sample/1.php は バッファされているため sample/2.phpsample/1.phpのなかでバッファされる。

  • 最後に sample/1.phpのデータが ob_get_clean();関数で取得しその結果をechoして出力される。
    image.png

  • 👇の layout.php は ことなる 経過 で 保持され 出力される。

layout ファイルを読み込む場合

web\test.php
// render メソッドの引数 パス、変数、layoutファイルのパス にすることで、
// 呼び出し時に、データ渡したり、、layoutファイルを指定することができる。
    public function render($path,$variables,$layout=null){}
// 呼び出し時に layoutファイルパスを指定する。
    $view->render('sample/1.php',array(),'sample/layout.php');
  • views クラスを修正する
web\test.php
<?php
class view{
// + 共通のグローバル変数みたいなもの
    protected $layout_variables = array();

// + 👆のプロパティのセッター
    public function setLayoutVar($name, $value)
    {
        $this->layout_variables[$name] = $value;
    }

    public function render($path,$variables,$layout=null){
        ob_start();
        ob_implicit_flush(0);
//+
        extract($variables);
    
        require __DIR__. '/../views/'.$path;
        
        $content = ob_get_clean();
// layout が null でなかったら
        if($layout){
// $contentを $this->layout_variables['layout_content'] に 代入して
            $this->layout_variables['layout_content'] = $content;

// render(パス,変数) layoutをNullにして実行する
            $this->render($layout,$this->layout_variables);
        }
// layout が null なら $content を echo する。
        echo $content;
    }
    public function escape($str){
        return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
    }
}
sample/1.php
<?php
// layoutファイルはこのプロパティにアクセスして変数を取得する
$this->setLayoutVar('title', 'sample1/phpです');
?>
<h1>sample/1.php のファイル</h1>
<p><?php echo $this->escape('<script>alert("今日は涼しいです。")</script>') ?></p>
<?php
// sample/2.php に sample/1.php からデータを渡すこともできる。
$this->render('sample/2.php',array());
views\sample\2.php
<h1>sample/2.php のファイル</h1>
  • layoutファイル を指定して実行する
web\test.php
$view = new View();
// sample/1.php
$view->render('sample/1.php',array(),'sample/layout.php');
流れ 1⃣
// $view->render('sample/1.php',array(),'sample/layout.php');
    public function render($path,$variables,$layout=null){
        ob_start();
        ob_implicit_flush(0);
//$variables は array()
        extract($variables);

// $path は  'sample/1.php'
// 'sample/2.php' は 'sample/1.php' の中で echo され 保持される。
        require __DIR__. '/../views/'.$path;
        
        $content = ob_get_clean();
// layout が null でなかったら
// layout は 'sample/layout.php' なので true
        if($layout){
// $contentを $this->layout_variables['layout_content'] に 代入して
// $content は saple/2.php のデータを含む sample/1.php
            $this->layout_variables['layout_content'] = $content;

// render(パス,変数) layoutを空にして実行する
// パス は sample/layout.php 変数は $this->layout_variables
// $this->layout_variables には 'layout_content'のほか セッターで渡したデータも含む。
            $this->render($layout,$this->layout_variables);
        }
// layout が null なら $content を echo する。
// ここは実行されない
        echo $content;
    }
流れ 2⃣
// $this->render('sample/layout.php',$this->layout_variables);
    public function render($path,$variables,$layout=null){
        ob_start();
        ob_implicit_flush(0);
//$variables は $this->layout_variables
// saple/2.php のデータを含む sample/1.php は 変数 $layout_contentになる。
        extract($variables);

// $path は  'sample/layout.php'
// $layout_content は 'sample/layout.php' の中で echo され 保持される。
        require __DIR__. '/../views/'.$path;

// $sample/1.php を保持した sample/layout.phpのデータを取得し代入する
        $content = ob_get_clean();
// layout が null でなかったら
// layout は Null  なので false
        if($layout){
            $this->layout_variables['layout_content'] = $content;
            $this->render($layout,$this->layout_variables);
        }

// layout が null なら $content を echo する。
// sample/layout.phpのデータを取得し代入した $content が 出力 される。
        echo $content;
    }
  • 出力された。
    image.png

  • view クラスを Controller クラス に 実装する。

    • コントローラクラスにも同名のrenderメソッドがあるため混同しなよう注意
    • コントローラーからテンプレートファイルや、layout ファイルを指定できるような制度設計になっている。
  • データの流れを理解するために 先に継承クラスの使い方を見る

controllers\StatusController.php
<?php
class StatusController extends Controller
{
// 修正
// url http://localhost/MiniBlog/
    public function indexAction()
    {
        $variables = ['title' => '今日の天気', 'content' => '<script>alert("今日はとても涼しかった")</script>'];

// コントローラークラスの render メソッド に 作成したデータを渡す。
        return $this->render($variables);

// templeteファイルも、layoutファイルも渡すことができる。
//      return $this->render($variables,$templete,$layout);
    }
}
core\Controller.php
// Controller クラス
// 修正
    protected function render($variables = array(), $template = null, $layout = 'layout')
    {
        $defaults = array(
            'request'  => $this->request,
            'base_url' => $this->request->getBaseUrl(),
            // 'session'  => $this->session,
        );
// view クラスをインスタンスする。
        $view = new View($this->application->getViewDir(), $defaults);

// templete を コントローラクラスで別途指定できるような制度設計になっている。
// うーん ためになる開発。
// 通常 $template は Null なので
        if (is_null($template)) {
// アクションネーム  'index' を $template に代入
            $template = $this->action_name;
        }
// controller_name と $template(通常 $action_name ) から パスを作成する
// $path =  'status' + '/' + 'index'
        $path = $this->controller_name . '/' . $template;
// dd($path);

// $path は 上で作成したパス  $variables は アクションメソッドで作成したデータ
// $layout は controller クラスの renderメソッドの引数にある 
// protected function render($variables = array(), $template = null, $layout = 'layout')
// なにもコントローラークラスのrennderメソッドの引数で指定しなければデフォルトの'layout' が 指定される。
        return $view->render($path, $variables,$layout);
//        👆 ビューオブジェクトで作成された HTMLが かえってくる。
    }
  • View クラスの作成
    • 基本は上と同じ
    • コントローラクラスの同名のrenderメソッドと混同しないように注意。
core\View.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class View
{
    protected $base_dir;
    protected $defaults;
    protected $layout_variables = array();

    /**
     * コンストラクタ
     *
     * @param string $base_dir
     * @param array $defaults
     */
    public function __construct($base_dir, $defaults = array())
    {
        $this->base_dir = $base_dir;
        $this->defaults = $defaults;
    }

    /**
     * レイアウトに渡す変数を指定
     *
     * @param string $name
     * @param mixed $value
     */
    public function setLayoutVar($name, $value)
    {
        $this->layout_variables[$name] = $value;
    }

    /**
     * ビューファイルをレンダリング
     *
     * @param string $_path
     * @param array $_variables
     * @param mixed $_layout
     * @return string
     */
    public function render($_path, $_variables = array(), $_layout = false)
    {
        $_file=  $this->base_dir . '/' . $_path . '.php';

        extract(array_merge($this->defaults, $_variables));

        ob_start();
// バッファの上限を無効化し出力させないようにする。
        ob_implicit_flush(0);
        require $_file;
        $content = ob_get_clean();

        if ($_layout) {
            $content = $this->render($_layout,
                array_merge($this->layout_variables, array(
                    '_content' => $content,
                )
            ));
        }

        return $content;
    }

    /**
     * 指定された値をHTMLエスケープする
     * @param string $string
     * @return string
     */
    public function escape($string)
    {
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
    }
}
  • veiw クラスの動作確認
    • まずは コントローラーを作成する。
controllers\AccountController.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
// コントローラーを継承したクラス
class AccountController extends Controller
{
// actionメソッドの呼び出し時引数に $params を 渡しているが、
//    $content = $this->$action_method($params);

// 実際のアクションメソッドには 仮引数が ない場合でも エラーにはならない。
      public function indexAction()
    {
        return $this->render();
    }

    public function showAction($params)
    {
        $name = urldecode($params['username']);
        $variables = ['user' => ['name' => $name]];
        return $this->render($variables);
    }
}
  • veiw クラスの動作確認
    • 次にviewファイルを作成する
views\account\index.php
<?php
$this->setLayoutVar('title', 'userのindex.php');
?>
<h1>userのindex.php</h1>
views\account\show.php
<?php
$this->setLayoutVar('title', 'statusのshow.php');
?>
<h1>ユーザーの名前: <?php echo $user['name'] ?></h1>
views\layout.php
<!DOCTYPE html>
<head>
    <title><?php echo $title ?></title>
</head>
<body>
    <?php echo $_content ?>
</body>
</html>

※確認 indexActionメソッドにアクセス
http://localhost/MiniBlog/account
image.png
※確認 showActionメソッドにアクセス
http://localhost/MiniBlog/account/山田太郎
image.png

Responseクラスの作成

  • HTTPヘッダやHTML を ユーザーに 返すためのクラス
    • phpでは header関数でいつでも HTTPヘッダ情報をユーザーに送信できる。
    • header関数 の ラッパークラス
.
├── controllers
│   └── StatusController.php
├── core
│   ├── Application.php
│   ├── Classloader.php
│   ├── Controller.php
│   ├── functions.php
│   ├── Request.php
│   ├── Response.php          (作成)
│   ├── Router.php
│   └── View.php
├── views
│   └── status
│       ├── index.php
│       └── show.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php
  • ユーザーにヘッダ情報やコンテントなどを返すのはsendメソッドだけに統一 している。
core\Response.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class Response
{
// htmlを格納する変数
    protected $content;

    protected $status_code = 200;
    protected $status_text = 'OK';

// HTTPヘッダの名前と値を配列で格納するプロパティ
// 主なHTTPヘッダとして Locationや Content-Type
    protected $http_headers = array();

    /**
     * レスポンスを送信
     * 
     */
    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;
// 変更 リダイレクト処理する時 不要なため
        if(!is_null($this->content)){
            echo $this->content;
        }

// 別途追加
// ファイル書き込みとかDB操作 を レスポンス後に行う制度設計もある
// https://pisuke-code.com/php-send-response-immediately/
// https://www.php.net/manual/en/function.http-response-code.php
// ユーザーへの返却は当然早くなる 
        exit(); // 別途追加 redirect時に確実に処理を終了させるため。
    }

    /**
     * コンテンツを設定
     * content(HTMLなどのユーザーに返す内容を格納させる)プロパティに
     * データをセットするメソッド
     *
     * @param string $content
     */
    public function setContent($content)
    {
        $this->content = $content;
    }

    /**
     * ステータスコードを設定
     * ステータスコードとは レスポンスがどのような状態かをコードで表す
     * ページが存在しない場合は お馴染みの 404コード
     * サーバーエラーを表す 500コード
     * 
     * それらのコードを設定するためのメソッド
     * 下記のサイトが参考になる
     * STEPごとに作る自作MVC WebFramework
     * https://qiita.com/sakunowman/items/b8f206661ab11af68d38#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9
     *
     * @param integer $status_code
     * @param string $status_code
     */
    public function setStatusCode($status_code, $status_text = '')
    {
        $this->status_code = $status_code;
        $this->status_text = $status_text;
    }

    /**
     * HTTPレスポンスヘッダを設定
     *
     * @param string $name
     * @param mixed $value
     */
    public function setHttpHeader($name, $value)
    {
// 最終的な例として header('location: url'); 
// send()で返信される
        $this->http_headers[$name] = $value;
// $name = location, $value = url
    }
}

image.png

  • Applicationクラスに実装する
core\Application.php
<?php
// abstract 使用するには クラスを抽象化 させる必要がある
abstract class Application
{
    protected Router $router;
    protected Request $request;
//+
    protected $response;

    /**
     * アプリケーションの初期化
     */
    protected function initialize()
    {
        $this->router = new Router($this->registerRoutes());
        $this->request    = new Request();
//+
        $this->response   = new Response();
    }


//+
    /**
     * Responseオブジェクトを取得
     *
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }
// 修正
    /**
     * アプリケーションを実行する
     */
    public function run()
    {
        $params = $this->router->resolve($this->request->getPathInfo());
        if ($params === false) {
            echo '下記のアドレスが存在いたしません';
            echo '<br>';
            echo $this->request->getPathInfo();
            exit;
        }

        $controller = $params['controller'];
        $action = $params['action'];

// 変更
// response の content に ビューをセットする
        //$content = $this->runAction($controller, $action, $params);
        $this->runAction($controller, $action, $params);
        
// responseに変更
        // echo $content;
//+
// response の content を 出力する。
        $this->response->send();
    }

// 修正
    /**
     * 指定されたアクションを実行する
     *
     * @param string $controller_name
     * @param string $action
     * @param array $params
     *
     * @throws HttpNotFoundException コントローラが特定できない場合
     */
    public function runAction($controller_name, $action, $params = array())
    {
        $controller_class = ucfirst($controller_name) . 'Controller';

        $controller = $this->findController($controller_class);

        if ($controller === false) {
            echo 'コントローラーが見つかりません';
            exit();
        }
        $content = $controller->run($action, $params);

// responseに変更
        // return $content;
        $this->response->setContent($content);
    }
}
  • controllerクラスにも初期化したresponseクラスを渡す
    public function __construct($application)
    {
        $this->controller_name = strtolower(substr(get_class($this), 0, -10));
        $this->application = $application;
        $this->request = $application->getRequest();
//+
        $this->response    = $application->getResponse();
    }

※確認 普通に作成した status/index画面 show画面が取得できればOK
http://localhost/MiniBlog/
http://localhost/MiniBlog/user/yamadataro/status/2

  • redirect メソッドを実装する
core\Controller.php
//+
    /**
     * 指定されたURLへリダイレクト
     *
     * @param string $url
     */
    protected function redirect($url)
    {
// 実引数 $url = '/' なら
        if (!preg_match('#https?://#', $url)) {
// アンマッチのため ここが実行
            $protocol = $this->request->isSsl() ? 'https://' : 'http://';
            $host = $this->request->getHost();
            $base_url = $this->request->getBaseUrl();
// base_url(フロントコントローラーまでのパスを先頭に追加してくる)
// よって実引数は フロントコントローラー 以下のパスを渡せばOK
            $url = $protocol . $host . $base_url . $url;
        }

        $this->response->setStatusCode(302, 'Found');
        $this->response->setHttpHeader('Location', $url);

// 別途追加 
// return $this->redirect() redirectメソッドには戻り値がないため
// reidrect + exit() しないと気持ち悪いため
        $this->response->send();
    }

データーベースとテーブルの作成

  • データーベースコネクション
  • データーベースを作成する
    • データーベース名
      • mini_blog
sql
CREATE DATABASE mini_blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
  • テーブルを作成する
    • phpMyAdmin-mini_blog
      • users (ユーザー情報)
      • following (user同士の中間テーブル)
      • status (ユーザーが投稿した情報)
sql
CREATE TABLE users(
	id INTEGER AUTO_INCREMENT,
	user_name VARCHAR(20) NOT NULL,
	password VARCHAR(40) NOT NULL,
	created_at DATETIME,
	PRIMARY KEY(id),
	UNIQUE KEY user_name_index(user_name)
)ENGINE = INNODB DEFAULT CHARSET=utf8mb4 ;

CREATE TABLE following(
	user_id INTEGER,
	following_id INTEGER,
PRIMARY KEY(user_id,following_id))ENGINE = INNODB DEFAULT CHARSET=utf8mb4 ;

CREATE TABLE status (
	id INTEGER AUTO_INCREMENT,
	user_id INTEGER NOT NULL,
    body VARCHAR(255),
    created_at DATETIME,
    PRIMARY KEY(id),
    INDEX user_id_index(user_id)
) ENGINE = INNODB DEFAULT CHARSET=utf8mb4 ;

#外部キーの設定
#外部キーを作成することでデータの整合性を担保できます
#開発段階では 設定しない方がいい 非常にめんどくさくなる。
ALTER TABLE following ADD FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE following ADD FOREIGN KEY (following_id) REFERENCES users(id);
ALTER TABLE status ADD FOREIGN KEY (user_id) REFERENCES users(id);
  • テーブル一覧の確認
 SHOW TABLES;
  • テーブルの構造確認
statusテーブルの構造確認
DESC status;

DB接続するためのプロパティとメソッドを集めたクラスを作成する。

  • DbManagerクラス
    • pdoを作成するクラス
    • DbRepositoryの継承クラス(プロジェクトに属するクラス)をインスタンス化するクラス
      • DbRepositoryの継承クラスはDbManagerクラスに隠れる形になる。
  • DbRepository
    • テーブルからデータを取るフレームワークのクラス
  • DbRepositoryの継承するクラスはプロジェクトに属するクラス
    • 具体的なsqlを発行するメソッド群
      • controllerで sql を書かずに このクラスのメソッドでデータを取得する。
    • 配置場所
      • models
    • 名前
      • テーブル名Repository (例. UserRepository)

perfectPHPフレームワーク

  • DbManager クラスが出入口になっている。

👆 コントローラーからは DbManagerクラス を 操作する。

フレームワークを使わない開発 -- 簡易的な開発の時

  • 継承クラスが出入口になると思う。
.
├── controllers (プロジェクトに属するクラス)
│   └── StatusController.php
├── core ( フレームワークのクラス)
│   ├── Application.php
│   ├── Classloader.php
│   ├── Controller.php
│   ├── DbManager.php               (作成)
│   ├── DbRepository.php            (作成)
│   ├── functions.php
│   ├── Request.php
│   ├── Response.php
│   ├── Router.php
│   └── View.php
├── models (プロジェクトに属するクラス)
│   ├── StatusRepository.php        (作成)
│   └── UserRepository.php          (作成)
├── views
│   └── status
│       ├── index.php
│       └── show.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php

DbManagerクラスを作成する

  • pdoを作る
core\DbManager.php
<?php

/**
 * DbManager.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class DbManager
{
    protected $connections = array();

    /**
     * データベースへ接続
     *
     * @param string $name
     * @param array $params
     */
    public function connect($name, $params)
    {
        // array_merge は 同じキー名の時は値を上書きする。
        $params = array_merge(
// このarray()はデフォルト値
            array(
                'dsn' => null,
                'user' => '',
                'password' => '',
                'options' => array(),
            ), $params);

// $params は
//         $this->db_manager->connect('master', 
//->コレ   array(
//            'dsn'      => 'mysql:dbname=mini_blog;host=localhost',
//            'user'     => 'root',
//            'password' => '',
//              👆重複する 'dsn','user','password'の値は上書きする。
//        ));

        $con = new PDO(
            $params['dsn'],
            $params['user'],
            $params['password'],
            $params['options']
        );

        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 作成したオブジェクトは 配列に名前を付けて保存する
        $this->connections[$name] = $con;
    }

    /**
     * コネクションを取得
     *
     * @param string $name
     * @return PDO
     */
    public function getConnection($name = null)
    {
        if (is_null($name)) {
// current関数は配列の先頭のpdoオブジェクトを返す
            return current($this->connections);
        }
// $nameを指定した場合 $name で 登録した pdoオブジェクトがかえる
        return $this->connections[$name];
    }
}
  • Applicationクラスに実装する。
core\Application.php
<?php
abstract class Application
{

//+    
    protected $db_manager;

    /**
     * コンストラクタ
     *
     */
    public function __construct($debug = false)
    {
        $this->setDebugMode($debug);
        $this->initialize();
//+             👆 で フレームワークの初期化が完了する
//      👇 これで 作成するアプリケーションの初期化処理を登録できるメソッドを用意してある。
// DBの初期化処理などをおこなう。
        $this->configure();
    }

    /**
     * アプリケーションの初期化
     */
    protected function initialize()
    {
        $this->router = new Router($this->registerRoutes());
        $this->request    = new Request();
        $this->response   = new Response();
//+        
        $this->db_manager = new DbManager();
    }

//+
    /**
     * アプリケーションの設定
     * パーフェクトPHP p236
     *  個別のアプリケーションで様々な設定ができるように空のメソッドとして定義
     */
    protected function configure()
    {
    }

//+
    /**
     * DbManagerオブジェクトを取得
     *
     * @return DbManager
     */
    public function getDbManager()
    {
        return $this->db_manager;
    }
}
  • アプリケーションクラスでの使用の仕方
MiniBlogApplication.php
<?php

class MiniBlogApplication extends Application
{

//+
    protected function configure()
    {
// DbMaster の protected $connections = array(); プロパティに
// $this->connections[$name] = $con; 名前をつけて保存している。
        $this->db_manager->connect('master', array(
            'dsn'      => 'mysql:dbname=mini_blog;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));
// pdoの接続を確認する
        dd($this->db_manager->getConnection('master'));
    }
}
  • pdoオブジェクトが返っていればOK
    image.png

  • controllerクラスにオブジェクトを渡す

core\Controller.php
    public function __construct($application)
    {
        $this->controller_name = strtolower(substr(get_class($this), 0, -10));
        $this->application = $application;
        $this->request = $application->getRequest();
        $this->response    = $application->getResponse();
//+
        $this->db_manager  = $application->getDbManager();
    }

DbRepositoryクラスを作成する

  • SQLを実行するメソッド(execute,fetch,fetchAll)を記述
    • 継承クラスで具体的な sql と パラメーター を 発行して👆のメソッドからDBにデータを登録できる設計になっている。
core\DbRepository.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
abstract class DbRepository
{
    protected $con;

    /**
     * コンストラクタ
     *
     * @param PDO $con
     */
    public function __construct($con)
    {
        $this->setConnection($con);
    }

    /**
     * コネクションを設定
     *
     * @param PDO $con
     */
    public function setConnection($con)
    {
        $this->con = $con;
    }

// +
    /**
     * クエリを実行
     *
     * @param string $sql
     * @param array $params
     * @return PDOStatement $stmt
     */
    public function execute($sql, $params = array())
    {
        $stmt = $this->con->prepare($sql);
        $stmt->execute($params);

        return $stmt;
    }
// +
    /**
     * クエリを実行し、結果を1行取得
     *
     * @param string $sql
     * @param array $params
     * @return array
     */
    public function fetch($sql, $params = array())
    {
        return $this->execute($sql, $params)->fetch(PDO::FETCH_ASSOC);
    }
// +
    /**
     * クエリを実行し、結果をすべて取得
     *
     * @param string $sql
     * @param array $params
     * @return array
     */
    public function fetchAll($sql, $params = array())
    {
        return $this->execute($sql, $params)->fetchAll(PDO::FETCH_ASSOC);
    }
}
  • DbManagerクラスに 実装する
    • (DbManagerクラスでは) DbRepositoryの継承クラス をインスタンス化する。
core\DbManager.php
<?php

/**
 * DbManager.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class DbManager
{
//+
    protected $repositories = array();

//+
    /**
     * リポジトリを取得
     *
     * @param string $repository_name
     * @return DbRepository
     */
    public function get($repository_name)
    {
// $repositorieの配列のなかに$repository_name 例えば User があるかどうか
        if (!isset($this->repositories[$repository_name])) {
// なければ UserRepositoryクラス名を作成して、
            $repository_class = $repository_name . 'Repository';
// なければ 引数に PDOオブジェクト を 取得して
            $con = $this->getConnection();
// ここで 継承オブジェクトに PDOオブジェクト渡して new してオブジェクトを作成する。
            $repository = new $repository_class($con);
// $repositorieの配列のなかに 保存する
            $this->repositories[$repository_name] = $repository;
        }
// 最後は 指定の 継承オブジェクトを返す
          return $this->repositories[$repository_name];
    }
}

UserRepository(継承)クラスの作成

models\UserRepository.php
<?php
class UserRepository extends DbRepository
{
// +
    public function insert($user_name, $password)
    {
        $password = $this->hashPassword($password);
        $now = new DateTime();

        $sql = "
            INSERT INTO users(user_name, password, created_at)
                VALUES(:user_name, :password, :created_at)
        ";

        $stmt = $this->execute($sql, array(
            ':user_name'  => $user_name,
            ':password'   => $password,
            ':created_at' => $now->format('Y-m-d H:i:s'),
        ));
    }
// +
    public function hashPassword($password)
    {
        return sha1($password . 'SecretKey');
    }
// +
    public function fetchByUserName($user_name)
    {
        $sql = "SELECT * FROM users WHERE user_name = :user_name";

        return $this->fetch($sql, array(':user_name' => $user_name));
    }
}
  • ユーザーを登録して確認
MiniBlogApplication.php
    protected function configure()
    {
        $this->db_manager->connect('master', array(
            'dsn'      => 'mysql:dbname=mini_blog;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));
// 修正
        $name = 'yamada taro';
        $password = 'testtest';
// DbRepositoryの継承オブジェクトの取得
        $userRepo = $this->db_manager->get('user');
        $userRepo->insert($name,$password);
        dd($userRepo->fetchByUserName($name));
    }

image.png

DbRepositoryクラスの継承クラスとpdoのマッチング

  • 全体の流れ例
    • Accountコントローラーのregisterメソッドでusersテーブルにuserを登録する場合
      image.png
      1⃣ DbManegerからUserRepositoryを取得する メソッド
      image.png
      取得する前に
      image.png
      👆 プロパティに UserRepository がないか確認している
      image.png
      なければ UserRepository を作成している
      image.png
      作成したRepository は プロパティに repository名で保存している。
      image.png
      最後に UserRepository を 返している
      image.png
      ようするに 1⃣ の メソッドは
      image.png
      👆 のプロパティの gettersetter になっている。
      2⃣ getConnectionForRepository メソッド
      1⃣ の Repository作成時の $con 変数に pdo オブジェクトを返すメソッド
      image.png
      1.$repository_connection_mapプロパティにrepositoryがあるか確認して
      2.getConnection()をコールして
      3.取得した pdoを 返すメソッド
      image.png
      image.png
      👆のプロパティにレポジトリの名前があるか確認してあれば、その pdoの$name を渡して その pdo オブジェクトを取得しにいくメソッドをコールする
      3⃣ 👇 $connectionsプロパティのゲッター で pdo を返す
      image.png

$repository_connection_mapのセッターがこれ
特定の pdo を指定したい場合につかう
image.png
$connectionsプロパティのセッターは pdo を作成時に 行っている
image.png
作成した pdo は 上記の流れにそって Repository に セットされる。

  • core\DbManager.phpの修正と追記
core\DbManager.php
<?php

/**
 * DbManager.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class DbManager
{
    protected $connections = array();
    protected $repositories = array();
//+
    protected $repository_connection_map = array();

    /**
     * データベースへ接続
     *
     * @param string $name
     * @param array $params
     */
    public function connect($name, $params)
    {
        $params = array_merge(array(
            'dsn'      => null,
            'user'     => '',
            'password' => '',
            'options'  => array(),
        ), $params);

        $con = new PDO(
            $params['dsn'],
            $params['user'],
            $params['password'],
            $params['options']
        );

        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->connections[$name] = $con;
    }

    /**
     * コネクションを取得
     *
     * @string $name
     * @return PDO
     */
    public function getConnection($name = null)
    {
        if (is_null($name)) {
            return current($this->connections);
        }

        return $this->connections[$name];
    }
//+
    /**
     * リポジトリごとのコネクション情報を設定
     *
     * @param string $repository_name
     * @param string $name
     */
    public function setRepositoryConnectionMap($repository_name, $name)
    {
        $this->repository_connection_map[$repository_name] = $name;
    }
//+
    /**
     * 指定されたリポジトリに対応するコネクションを取得
     *
     * @param string $repository_name
     * @return PDO
     */
    public function getConnectionForRepository($repository_name)
    {
        if (isset($this->repository_connection_map[$repository_name])) {
            $name = $this->repository_connection_map[$repository_name];
            $con = $this->getConnection($name);
        } else {
            $con = $this->getConnection();
        }

        return $con;
    }

    /**
     * リポジトリを取得
     *
     * @param string $repository_name
     * @return DbRepository
     */
    public function get($repository_name)
    {
        if (!isset($this->repositories[$repository_name])) {
            $repository_class = $repository_name . 'Repository';

//修正
            //$con = $this->getConnection('master');
            $con = $this->getConnectionForRepository($repository_name);

            $repository = new $repository_class($con);
            $this->repositories[$repository_name] = $repository;
        }

        return $this->repositories[$repository_name];
    }

//+ 接続の解放処理
    /**
     * デストラクタ
     *   デストラクタメソッドは、 特定のオブジェクトを参照するリファレンスがひとつもなくなったときにコールされます。 
     *   あるいは、スクリプトの終了時にも順不同でコールされます。
     *   
     * リポジトリと接続を破棄する
     */
    public function __destruct()
    {
        foreach ($this->repositories as $repository) {
// unset — 指定した変数の割当を解除する
            unset($repository);
        }

        foreach ($this->connections as $con) {
            unset($con);
        }
    }
}
  • アプリケーションクラスでの使用方法
DBが切り替えられているかの確認のためDB
CREATE DATABASE mini_blog_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE users(
	id INTEGER AUTO_INCREMENT,
	user_name VARCHAR(20) NOT NULL,
	password VARCHAR(40) NOT NULL,
	created_at DATETIME,
	PRIMARY KEY(id),
	UNIQUE KEY user_name_index(user_name)
)ENGINE = INNODB DEFAULT CHARSET=utf8mb4 ;        
MiniBlogApplication.php

    protected function configure()
    {
        $this->db_manager->connect('master', array(
            'dsn'      => 'mysql:dbname=mini_blog;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));

        //+
        $this->db_manager->connect('userCon', array(
            'dsn'      => 'mysql:dbname=mini_blog_test;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));
        $this->db_manager->setRepositoryConnectionMap('user','userCon');
        $name = 'yamada taro';
        $password = 'testtest';
// DbRepositoryの継承オブジェクトの取得
        $userRepo = $this->db_manager->get('user'); 
// この時 userRepo の引数に渡される con は userCon になる
        
        $userRepo->insert($name,$password);
        dd($userRepo->fetchByUserName($name));
    }
  • $name = 'yamada taro'; は 2度目などで重複エラーが生じなければDBが切り替えられている。
select * from mini_blog_test.users;
+----+-------------+------------------------------------------+---------------------+
| id | user_name   | password                                 | created_at          |
+----+-------------+------------------------------------------+---------------------+
|  1 | yamada taro | eccedb8d2e6b8749c14388ba3b3292597960febd | 2022-08-23 06:06:31 |
+----+-------------+------------------------------------------+---------------------+
1 row in set (0.00 sec)

Sessionクラスを実装する

  • $_SESSIONクラスのラッパークラス(より使いやすくしたもの)
  • csrf対策やauth認証で使用
core\Session.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class Session
{
    protected static $sessionStarted = false;

    /**
     * コンストラクタ
     * セッションを自動的に開始する
     */
    public function __construct()
    {
        if (!self::$sessionStarted) {
            session_start();

            self::$sessionStarted = true;
        }
    }

    /**
     * セッションに値を設定
     *
     * @param string $name
     * @param mixed $value
     */
    public function set($name, $value)
    {
        $_SESSION[$name] = $value;
    }

    /**
     * セッションから値を取得
     *
     * @param string $name
     * @param mixed $default 指定したキーが存在しない場合のデフォルト値
     */
    public function get($name, $default = null)
    {
        if (isset($_SESSION[$name])) {
            return $_SESSION[$name];
        }

        return $default;
    }

    /**
     * セッションから値を削除
     *
     * @param string $name
     */
    public function remove($name)
    {
        unset($_SESSION[$name]);
    }

    /**
     * セッションを空にする
     */
    public function clear()
    {
        $_SESSION = array();
    }
}
  • applicationクラスに実装する
core\Application.php

//+    
    protected $session;

    /**
     * アプリケーションの初期化
     */
    protected function initialize()
    {
        $this->router = new Router($this->registerRoutes());
        $this->request = new Request();
        $this->response = new Response();
        $this->db_manager = new DbManager();
//+        
        $this->session    = new Session();
    }

//+  
    /**
     * Sessionオブジェクトを取得
     *
     * @return Session
     */
    public function getSession()
    {
        return $this->session;
    }

  • controllerクラスにオブジェクトを渡す
core\Controller.php
    public function __construct($application)
    {
        $this->controller_name = strtolower(substr(get_class($this), 0, -10));
        $this->application = $application;
        $this->request = $application->getRequest();
        $this->response    = $application->getResponse();
        $this->db_manager  = $application->getDbManager();
//+
        $this->session     = $application->getSession();
    }

UsersテーブルのCRUD

  • そのまえに
MiniBlogApplication.php
    protected function configure()
    {
        $this->db_manager->connect('master', array(
            'dsn'      => 'mysql:dbname=mini_blog;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));

// 👇 これ全部削除        
        $this->db_manager->connect('userRepo', array(
            'dsn'      => 'mysql:dbname=mini_blog_test;host=localhost',
            'user'     => 'root',
            'password' => '',
        ));
        $this->db_manager->setRepositoryConnectionMap('user','userRepo');
        
        $name = 'yamada taro';
        $password = 'testtest';
// DbRepositoryの継承オブジェクトの取得
        $userRepo = $this->db_manager->get('user');
        $userRepo->insert($name,$password);
        dd($userRepo->fetchByUserName($name));
    }
  • rootの確認
MiniBlogApplication.php
    protected function registerRoutes()
    {
        return array(

             // 省略

            '/account'
                => array('controller' => 'account', 'action' => 'index'),
 //これは完全に削除する '/account/:username'
               // => array('controller' => 'account', 'action' => 'show'),
            '/account/:action'
                => array('controller' => 'account'),
        );
    }
  • user の 登録フォームの作成

    • アクションメソッドの作成
      controllers\AccountController.php
      <?php
      /**
       * @author Katsuhiro Ogawa <fivestar@nequal.jp>
       */
      // コントローラーを継承したクラス
      class AccountController extends Controller
      {
            public function indexAction()
          {
              return $this->render();
          }
      
      // 削除
          // public function showAction($params)
          // {
          //     $name = urldecode($params['username']);
          //     $variables = ['user' => ['name' => $name]];
          //     return $this->render($variables);
          // }
      
      //+
          public function signupAction()
          {
              return $this->render(array(
                  'user_name' => '',
                  'password'  => ''
              ));
          }
      }
      
    • viewの作成
      views\account\signup.php
      <?php $this->setLayoutVar('title', 'アカウント登録') ?>
      
      <h2>アカウント登録</h2>
      
      <form action="<?php echo $base_url; ?>/account/register" method="post">
      
          <?php echo $this->render('account/inputs', array(
              'user_name' => $user_name, 'password' => $password,
          )); ?>
      
          <p>
              <input type="submit" value="登録" />
          </p>
      </form>
      
      <p>
          <a href="<?php echo $base_url ?>/account/signin">ログイン画面へ</a>
      </p>
      
      views\account\inputs.php
      <table>
          <tbody>
              <tr>
                  <th>ユーザID</th>
                  <td>
                      <input type="text" name="user_name" value="<?php echo $this->escape($user_name); ?>" />
                  </td>
              </tr>
              <tr>
                  <th>パスワード</th>
                  <td>
                      <input type="password" name="password" value="<?php echo $this->escape($password); ?>" />
                  </td>
              </tr>
          </tbody>
      </table>
      
  • http://localhost/MiniBlog/account/signup
    image.png

  • Userを登録する

    • アクションメソッドの作成
      controllers\AccountController.php
      // 修正
          public function indexAction()
          {
              $user = $this->session->get('user');
      
              return $this->render(array(
                  'user'       => $user
              ));
          }
      
      //+
          public function registerAction()
          {
      // methodの確認
              if (!$this->request->isPost()) {
              // コントローラーはHTMLを返しておけばいい。その際viewクラスを利用するかしないかの話
                  return '<h1>ページが見つかりません</h1>';
              }
      
              $user_name = $this->request->getPost('user_name');
              $password = $this->request->getPost('password');
      
              $errors = array();
      // フォームのバリデーション
              if (!strlen($user_name)) {
                  $errors[] = 'ユーザIDを入力してください';
              } else if (!preg_match('/^\w{3,20}$/', $user_name)) {
                  $errors[] = 'ユーザIDは半角英数字およびアンダースコアを3 ~ 20 文字以内で入力してください';
              } else if (!$this->db_manager->get('User')->isUniqueUserName($user_name)) {
                  $errors[] = 'ユーザIDは既に使用されています';
              }
      
              if (!strlen($password)) {
                  $errors[] = 'パスワードを入力してください';
              } else if (4 > strlen($password) || strlen($password) > 30) {
                  $errors[] = 'パスワードは4 ~ 30 文字以内で入力してください';
              }
      
              if (count($errors) === 0) {
      // エラーがなかったら
      
                  $this->db_manager->get('User')->insert($user_name, $password);
      // ユーザーを取得して Sessionに保存
                  $user = $this->db_manager->get('User')->fetchByUserName($user_name);
                  $this->session->set('user', $user);
      
                  return $this->redirect('/account');
              }
      
              return $this->render(array(
                  'user_name' => $user_name,
                  'password'  => $password,
                  'errors'    => $errors,
              ), 'signup');
          }
      
    • sqlの追加
      models\UserRepository.php
      //+
          public function isUniqueUserName($user_name)
          {
              $sql = "SELECT COUNT(id) as count FROM users WHERE user_name = :user_name";
      
              $row = $this->fetch($sql, array(':user_name' => $user_name));
              if ($row['count'] === '0') {
                  return true;
              }
      
              return false;
          }
      
    • error 表示 を追加
      views\account\signup.php
      <?php $this->setLayoutVar('title', 'アカウント登録') ?>
      
      <h2>アカウント登録</h2>
      
      <form action="<?php echo $base_url; ?>/account/register" method="post">
      <!-- + -->
          <?php if (isset($errors) && count($errors) > 0): ?>
              <?php echo $this->render('errors', array('errors' => $errors)); ?>
          <?php endif; ?>
      
          <?php echo $this->render('account/inputs', array(
              'user_name' => $user_name,
              'password' => $password,
          )
          ); ?>
      
          <p>
              <input type="submit" value="登録" />
          </p>
      </form>
      
      <p>
          <a href="<?php echo $base_url ?>/account/signin">ログイン画面へ</a>
      </p>
      
      views\errors.php
      <ul class="error_list">
          <?php foreach ($errors as $error): ?>
          <li><?php echo $this->escape($error); ?></li>
          <?php endforeach; ?>
      </ul>
      
    • indexActionの修正
      views\account\index.php
      <?php $this->setLayoutVar('title', 'アカウント') ?>
      
      <h2>アカウント</h2>
      <p>
          ユーザID:
          <a href="<?php echo $base_url ?>/user/<?php 
          echo $this->escape($user['user_name']); ?>">
              <strong><?php echo $this->escape($user['user_name']); ?></strong>
          </a>
      </p>
      
      <ul>
          <li>
              <a href="<?php echo $base_url; ?>/">ホーム</a>
          </li>
          <li>
              <a href="<?php echo $base_url; ?>/account/signout">ログアウト</a>
          </li>
      </ul>
      
  • バリデーションの確認
    image.png

  • リダイレクトとユーザー登録の確認
    image.png

csrf対策 を 実装する

  • form や fetch(クロスオリジンを除く) 送信する場合 必ず絶対やらなければいけない 対策
  • フレームワークではワンタイムトークン形式(一般的)をとっている。
  • また複数のウィンドウ(10件以下)で別々のtokenを作成してチェックできるよう設計されている。
  • Controller クラスで 実装する。(普通はSessionクラスで実装されることが多い)
core\Controller.php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */

// +
    /**
     * CSRFトークンを生成
     *
     * @param string $form_name
     * @return string $token
     */
    protected function generateCsrfToken($form_name)
    {
// form 毎に 異なる token を作成
        $key = 'csrf_tokens/' . $form_name;

// セッション に 登録した token を 取得
        $tokens = $this->session->get($key, array());
        if (count($tokens) >= 10) {
// 10 件 以上なら
// 配列の前から削除していく
            array_shift($tokens);
        }
// 新しく token を 作成する
        $token = sha1($form_name . session_id() . microtime());
// session に 保存する
        $tokens[] = $token;
        $this->session->set($key, $tokens);
// トークンの 配列を 返す。
        return $token;
    }
// +
    /**
     * CSRFトークンが妥当かチェック
     *
     * @param string $form_name
     * @param string $token
     * @return boolean
     */
    protected function checkCsrfToken($form_name, $token)
    {
        $key = 'csrf_tokens/' . $form_name;
// セッション に 登録した token を 取得
        $tokens = $this->session->get($key, array());

// $tokensの配列の値をリクエスト送信された$token で サーチする
        if (false !== ($pos = array_search($token, $tokens, true))) {

// 一致すれば、その $token を sessionの $tokensから 削除
            unset($tokens[$pos]);
            $this->session->set($key, $tokens);
// true を 返す。
            return true;
        }

        return false;
    }
  • CSRFトークン を フォームに 追加
controllers\AccountController.php
// 修正
    public function signupAction()
    {
        return $this->render(array(
            'user_name' => '',
            'password'  => ''
// +
           ,
            '_token'    => $this->generateCsrfToken('account/signup')

        ));
    }
// 修正
    public function registerAction()
    {
        if (!$this->request->isPost()) {
            return '<h1>ページが見つかりません</h1>';
        }
// +
        $token = $this->request->getPost('_token');
// +
        if (!$this->checkCsrfToken('account/signup', $token)) {
            return $this->redirect('/account/signup');
        }

        $user_name = $this->request->getPost('user_name');
        $password = $this->request->getPost('password');

        $errors = array();

        if (!strlen($user_name)) {
            $errors[] = 'ユーザIDを入力してください';
        } else if (!preg_match('/^\w{3,20}$/', $user_name)) {
            $errors[] = 'ユーザIDは半角英数字およびアンダースコアを3 ~ 20 文字以内で入力してください';
        } else if (!$this->db_manager->get('User')->isUniqueUserName($user_name)) {
            $errors[] = 'ユーザIDは既に使用されています';
        }

        if (!strlen($password)) {
            $errors[] = 'パスワードを入力してください';
        } else if (4 > strlen($password) || strlen($password) > 30) {
            $errors[] = 'パスワードは4 ~ 30 文字以内で入力してください';
        }

        if (count($errors) === 0) {
            $this->db_manager->get('User')->insert($user_name, $password);

            $user = $this->db_manager->get('User')->fetchByUserName($user_name);
            $this->session->set('user', $user);

            return $this->redirect('/account');
        }

        return $this->render(array(
            'user_name' => $user_name,
            'password'  => $password,
            'errors'    => $errors,
            // +
            '_token'    => $this->generateCsrfToken('account/signup'),
        ), 'signup');
    }
views\account\signup.php
<?php $this->setLayoutVar('title', 'アカウント登録') ?>

<h2>アカウント登録</h2>

<form action="<?php echo $base_url; ?>/account/register" method="post">
<!-- + -->
    <input type="hidden" name="_token" value="<?php echo $this->escape($_token); ?>" />

    <?php if (isset($errors) && count($errors) > 0): ?>
        <?php echo $this->render('errors', array('errors' => $errors)); ?>
    <?php endif; ?>

    <?php echo $this->render(
        'account/inputs',
        array(
            'user_name' => $user_name,
            'password' => $password,
        )
    ); ?>

    <p>
        <input type="submit" value="登録" />
    </p>
</form>
<p>
    <a href="<?php echo $base_url ?>/account/signin">ログイン画面へ</a>
</p>
  • 開発ツールから
    image.png
  • 直接 値を 空にして 投稿する。
    image.png
  • リダイレクトされればOK
    image.png

例外機能を 実装する。

  • 例外をどこでチャッチするかは大きな問題。
    • メソッドごとに例外を補足すると保守性や維持など著しく非効率になる。
    • 例外が握りつぶされる可能性もある
    •     function func(){
             try{
      
              }catch(Exception $e){} //<- コレ
          }
      
  • perfectPHPでは
    • 全てのアクセスは フロントコントローラーの web/index.php にアクセスされる
    • web\index.php
          $app = new MiniBlogApplication(true);
        // このrun メソッドで全て処理される。 
          $app->run();
      
    • core\Application.php
          /**
           * よって このメソッドで全ての 例外を補足することができる。
           */
          public function run()
          {
              try {
                  $params = $this->router->resolve($this->request->getPathInfo());
                  if ($params === false) {
                      echo '下記のアドレスが存在いたしません';
                      echo '<br>';
                      exit;
                  }
                  $controller = $params['controller'];
                  $action = $params['action'];
                  $this->runAction($controller, $action, $params);
      
           
              } catch (\Exception  $e) {
        //tr{}catch{}構文で ここで全ての例外を補足できる。
                  $e->getMessage();
              }
        // catch 後も ここは 実行 する。
                  $this->response->send();
          }
      
  • perfectPHP では 👆のような制度設計になっている。
  • HttpNotFoundException クラスの作成
    • 404を返すときにスローさせる例外クラス
core\HttpNotFoundException.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class HttpNotFoundException extends Exception {}
  • UnauthorizedActionException クラスの作成
    • ユーザー認証が失敗した時にスローさせる例外クラス
core\UnauthorizedActionException.php
<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class UnauthorizedActionException extends Exception {}

  • Apllication に 実装 させる
// 修正
    public function run()
    {
        try {
            $params = $this->router->resolve($this->request->getPathInfo());
            if ($params === false) {
                // echo '下記のアドレスが存在いたしません';
                // echo '<br>';
                // exit;
// 修正
                throw new HttpNotFoundException('No route found for ' . $this->request->getPathInfo());

            }
            $controller = $params['controller'];
            $action = $params['action'];
            $this->runAction($controller, $action, $params);
        } catch (HttpNotFoundException $e) {
            $this->render404Page($e);
        } catch (UnauthorizedActionException $e) {
            // list($controller, $action) = $this->getLoginAction();
            // $this->runAction($controller, $action);
        }

            $this->response->send();
    }
// 修正
    public function runAction($controller_name, $action, $params = array())
    {
        $controller_class = ucfirst($controller_name) . 'Controller';
        $controller = $this->findController($controller_class);
        if ($controller === false) {
            // echo 'コントローラーが見つかりません';
            // exit();
// 修正
            throw new HttpNotFoundException($controller_class . ' controller is not found.');
        }
// +
    /**
     * 404エラー画面を返す設定
     *
     * @param Exception $e
     */
    protected function render404Page($e)
    {
        $this->response->setStatusCode(404, 'Not Found');
        $message = $this->isDebugMode() ? $e->getMessage() : 'Page not found.';
        $message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');

        $this->response->setContent(<<<EOF
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>404</title>
</head>
<body>
    {$message}
</body>
</html>
EOF
        );
    }
  • controller クラスに 実装させる
core\Controller.php
    /**
     * コントローラーのアクションメソッドを 文字列で 実行 させるメソッド
     *
     * @param string $action
     * @param array $params
     * @return string レスポンスとして返すコンテンツ
     *
     */
    public function run($action, $params = array())
    {
        $this->action_name = $action;

        $action_method = $action . 'Action';
        if (!method_exists($this, $action_method)) {
            //     echo "ファイルが存在しません";
            //     exit();
// 修正
            $this->forward404();
        }
        $content = $this->$action_method($params);
        return $content;
    }

// +
     /**
     * 404エラー画面を出力
     *
     * @throws HttpNotFoundException
     */
    protected function forward404()
    {
        throw new HttpNotFoundException('Forwarded 404 page from '
            . $this->controller_name . '/' . $this->action_name);
    }

認証システムを実装する。

  • セッションによる認証機能の実装
    public function registerAction()
    {

//+ 認証済みの場合 index に リダイレクト させる。
        if ($this->session->isAuthenticated()) {
            return $this->redirect('/account');
        }

        if (!$this->request->isPost()) {
            return '<h1>ページが見つかりません</h1>';
        }
        $token = $this->request->getPost('_token');
        if (!$this->checkCsrfToken('account/signup', $token)) {
            return $this->redirect('/account/signup');
        }
        $user_name = $this->request->getPost('user_name');
        $password = $this->request->getPost('password');
        $errors = array();
        if (!strlen($user_name)) {
            $errors[] = 'ユーザIDを入力してください';
        } else if (!preg_match('/^\w{3,20}$/', $user_name)) {
            $errors[] = 'ユーザIDは半角英数字およびアンダースコアを3 ~ 20 文字以内で入力してください';
        } else if (!$this->db_manager->get('User')->isUniqueUserName($user_name)) {
            $errors[] = 'ユーザIDは既に使用されています';
        }
        if (!strlen($password)) {
            $errors[] = 'パスワードを入力してください';
        } else if (4 > strlen($password) || strlen($password) > 30) {
            $errors[] = 'パスワードは4 ~ 30 文字以内で入力してください';
        }
        if (count($errors) === 0) {
            $this->db_manager->get('User')->insert($user_name, $password);
            $user = $this->db_manager->get('User')->fetchByUserName($user_name);
            $this->session->set('user', $user);

// + ユーザー登録成功時
            $this->session->setAuthenticated(true);

            return $this->redirect('/account');
        }

        return $this->render(array(
            'user_name' => $user_name,
            'password'  => $password,
            'errors'    => $errors,
            '_token'    => $this->generateCsrfToken('account/signup'),
        ), 'signup');
    }
core\Session.php
//+
    protected static $sessionIdRegenerated = false;

//+
    /**
     * セッションIDを再生成する
     *
     * @param boolean $destroy trueの場合は古いセッションを破棄する
     * 理由がない限り true を必ずセットする
     */
    public function regenerate($destroy = true)
    {
        if (!self::$sessionIdRegenerated) {
            session_regenerate_id($destroy);

            self::$sessionIdRegenerated = true;
        }
    }

//+
    /**
     * 認証状態を設定
     *
     * @param boolean
     */
    public function setAuthenticated($bool)
    {
// session_regenerate_id をしてから
        $this->regenerate();
// session に ユーザー情報を設置するのが基本
        $this->set('_authenticated', (bool)$bool);
    }

//+
    /**
     * 認証済みか判定
     *
     * @return boolean
     */
    public function isAuthenticated()
    {
        return $this->get('_authenticated', false);
    }
  • 認証が必要なアクションメソッド を コントローラーのプロパティ で登録
  • リクエストがあった場合 プロパティをチェックして 認証が必要なアクションかどうかを判断
  • 認証が必要なアクションで認証がなかったらログインサイトに転送
core\Controller.php
//  👇 個々のコントローラーで 具体的に 認証が必要な アクション名を登録する
    protected $auth_actions = array();
//  $auth_actions = true に 変更した場合、
//    コントローラーの全てのアクションメソッドで認証が必要となる。

// core\Controller.php
    public function run($action, $params = array())
    {
        $this->action_name = $action;

        $action_method = $action . 'Action';
        if (!method_exists($this, $action_method)) {
            $this->forward404();
        }

//+
// needsAuthentication($action)  $actionは認証が必要なアクションかどうか
// isAuthenticated() 認証しているかどうか
        if ($this->needsAuthentication($action) && !$this->session->isAuthenticated()) {
// 必要なサイト かつ 未認証なら 例外をーをスローする。
            throw new UnauthorizedActionException();
        }

        $content = $this->$action_method($params);
        return $content;
    }

//+
    /**
     * 指定されたアクションが認証済みでないとアクセスできないか判定
     *
     * @param string $action
     * @return boolean
     */
    protected function needsAuthentication($action)
    {
// $auth_actionsプロパティが true の時
        if ($this->auth_actions === true
// または $auth_actions が配列 かつ 値の中に 実行する$action 名が 存在する時
            || (is_array($this->auth_actions) && in_array($action, $this->auth_actions))
        ) {
// true を 返す // 認証が必要だと判定される。
            return true;
        }
// false の時 認証が不要だと判定される。
        return false;
    }
  • 未認証で例外スローされた場合
core\Application.php
//+
    abstract function getLoginAction();
    
    public function run()
    {
        try {
            $params = $this->router->resolve($this->request->getPathInfo());
            if ($params === false) {
                throw new HttpNotFoundException('No route found for ' . $this->request->getPathInfo());
            }

            $controller = $params['controller'];
            $action = $params['action'];

            $this->runAction($controller, $action, $params);
        } catch (HttpNotFoundException $e) {
            $this->render404Page($e);
        } catch (UnauthorizedActionException $e) {
// +
// controller と action を 取得して
            list($controller, $action) = $this->getLoginAction();
// 取得した controller の action を実行する。
            $this->runAction($controller, $action);
        }
        
        $this->response->send();
    }
  • login_action プロパティの登録
MiniBlogApplication.php
//+
   public function getLoginAction(){
      return ['account','signin'];
//   👆 アカウントコントローラーのsigninアクションを登録する
//  未認証で例外が飛んだ場合このサイトにアクセスされるようになる。
   }
  • ログインフォームの作成

    • アクションメソッドの作成
      controllers\AccountController.php
          public function signinAction()
          {
      // 認証済みの場合 リダイレクト        
              if ($this->session->isAuthenticated()) {
                  return $this->redirect('/account');
              }
      
              return $this->render(array(
                  'user_name' => '',
                  'password'  => '',
                  '_token'    => $this->generateCsrfToken('account/signin'),
              ));
          }
      
    • 画面の作成
      views\account\signin.php
      <?php $this->setLayoutVar('title', 'ログイン') ?>
      
      <h2>ログイン</h2>
      
      <p>
          <a href="<?php echo $base_url; ?>/account/signup">新規ユーザ登録</a>
      </p>
      
      <form action="<?php echo $base_url; ?>/account/authenticate" method="post">
          <input type="hidden" name="_token" value="<?php echo $this->escape($_token); ?>" />
      
          <?php if (isset($errors) && count($errors) > 0): ?>
              <?php echo $this->render('errors', array('errors' => $errors)); ?>
          <?php endif; ?>
      
          <?php echo $this->render('account/inputs', array(
              'user_name' => $user_name, 'password' => $password,
          )); ?>
      
          <p>
              <input type="submit" value="ログイン" />
          </p>
      </form>
      <p>
          <a href="<?php echo $base_url ?>/account/signup">register画面へ</a>
      </p>
      
    • http://localhost/MiniBlog/account/signin
      image.png
  • ログイン機能作成

controllers\AccountController.php
//+
    public function authenticateAction()
    {
// 認証済みなら
        if ($this->session->isAuthenticated()) {
            return $this->redirect('/account');
        }
// method の確認
        if (!$this->request->isPost()) {
            $this->forward404();
        }
// tokenのチェック
        $token = $this->request->getPost('_token');
        if (!$this->checkCsrfToken('account/signin', $token)) {
            return $this->redirect('/account/signin');
        }

        $user_name = $this->request->getPost('user_name');
        $password = $this->request->getPost('password');

        $errors = array();
// バリデーション
        if (!strlen($user_name)) {
            $errors[] = 'ユーザIDを入力してください';
        }

        if (!strlen($password)) {
            $errors[] = 'パスワードを入力してください';
        }
// 入力値が OK なら
        if (count($errors) === 0) {
// 認証チェック
            $user_repository = $this->db_manager->get('User');
// user_name から user を取得して
            $user = $user_repository->fetchByUserName($user_name);
// user と password の 確認
            if (!$user
                || ($user['password'] !== $user_repository->hashPassword($password))
            ) {
// false なら
                $errors[] = 'ユーザIDかパスワードが不正です';
            } else {
// true なら 
                $this->session->setAuthenticated(true);
                $this->session->set('user', $user);

                return $this->redirect('/account');
            }
        }
// バリデーション エラー または 認証エラー なら
        return $this->render(array(
            'user_name' => $user_name,
            'password'  => $password,
            'errors'    => $errors,
            '_token'    => $this->generateCsrfToken('account/signin'),
        ), 'signin');
    }
  • ログアウトの実装
controllers\AccountController.php
//+
    public function signoutAction()
    {
        $this->session->clear();
        $this->session->setAuthenticated(false);

         $this->redirect('/account/signin');
    }
  • Accountのindexアクションに認証をつける
controllers\AccountController.php
//+
    protected $auth_actions = array('index');
  • 未認証時ログイン画面に遷移されればOK
    • http://localhost/MiniBlog/account
      image.png

perfectPHPでのフレームワークは完了

その他 最低限あったらいいなと思ったクラス

status の作成

  • コントローラーの作成
controllers\StatusController.php
<?php
/**
 * StatusController.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class StatusController extends Controller
{

}
  • Repository の作成
models\StatusRepository.php
<?php

/**
 * StatusRepository.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class StatusRepository extends DbRepository
{
}

navi の作成

  • view の作成
views\navi.php
<?php if($session->isAuthenticated()): ?>
<ul style="display:flex;column-gap:30px">
    <li><a href="<?php echo $base_url ?>">ホーム</a></li>
    <li><a href="<?php echo $base_url ?>/account">アカウント</a></li>
    <li><a href="<?php echo $base_url ?>/users">ユーザー一覧</a></li>
</ul>
<?php endif ?>
  • navi.php の読み込み
views\layout.php
<!DOCTYPE html>
<head>
    <title><?php echo $title ?></title>
</head>
<body>

<!-- + -->
    <?php echo $this->render('navi') ?>

    <?php echo $_content ?>
</body>
</html>
  • view コントローラーに sessionクラスを渡す。
core\Controller.php
protected function render($variables = array(), $template = null, $layout = 'layout')
{
    $defaults = array(
        'request'  => $this->request,
        'base_url' => $this->request->getBaseUrl(),
// アンコメント
        'session'  => $this->session,
    );
    $view = new View($this->application->getViewDir(), $defaults);

    if (is_null($template)) {
        $template = $this->action_name;
    }
    $path = $this->controller_name . '/' . $template;

    return $view->render($path, $variables,$layout);
}
  • navi が 表示されれば OK http://localhost/MiniBlog/account
    image.png

user 一覧の作成

  • root の追加
MiniBlogApplication.php
protected function registerRoutes()
{
    return array(
        '/'
        => array('controller' => 'status', 'action' => 'index'),
        '/status/post'
        => array('controller' => 'status', 'action' => 'post'),
        // :user_name <= 動的に変更したいパスは ':' 先頭にcolonをつける
        '/user/:user_name'
        // 上は '/user/(?p<user_name>[^/]+)' になる。 
        => array('controller' => 'status', 'action' => 'user'),
        '/user/:user_name/status/:id'
        // 上は '/user/(?p<user_name>[^/]+)/status/(?p<id>[^/]+)' になる。
        => array('controller' => 'status', 'action' => 'show'),
        '/account'
        => array('controller' => 'account', 'action' => 'index'),
        '/account/:action'
        // 上は '/account/(?p<action>[^/]+)' になる
        => array('controller' => 'account'),
        '/follow'
        => array('controller' => 'account', 'action' => 'follow'),
//+
        '/users'
        => array('controller' => 'account', 'action' => 'list'),
    );
}
  • view の 作成
views\account\list.php
<?php $this->setLayoutVar('title', 'ユーザー一覧') ?>

<h2>ユーザー一覧</h2>

<ul>
    <?php foreach($users as $user): ?>
    <li>
        <a href="<?php echo $base_url; ?>/user/<?php echo $user['user_name'] ?>">
        <?php echo $user['user_name'] ?>
        </a>
    </li>
    <?php endforeach ?>
</ul>
  • action メソッドの作成
controllers\AccountController.php
// +
    public function listAction(){
        $users = $this->db_manager->get('User')->all();

        return $this->render([
            'users'=>$users
        ]);
    }
  • sql メソッドの追加
models\UserRepository.php
// +
    public function all(){
        $sql = 'SELECT * FROM users';
        return $this->fetchAll($sql);
    }

image.png

ホーム の作成

  • view の作成
views\status\index.php
<?php $this->setLayoutVar('title', 'ホーム') ?>

<h2>ホーム</h2>

<form action="<?php echo $base_url; ?>/status/post" method="post">
    <input type="hidden" name="_token" value="<?php echo $this->escape($_token); ?>" />

    <?php if (isset($errors) && count($errors) > 0): ?>
    <?php echo $this->render('errors', array('errors' => $errors)) ?>
    <?php endif; ?>

    <textarea name="body" rows="2" cols="60"><?php echo $this->escape($body); ?></textarea>
    <p>
        <input type="submit" value="発言" />
    </p>
</form>

<div id="statuses">
    <?php foreach ($statuses as $status): ?>
    <?php echo $this->render('status/status', array('status' => $status)); ?>
    <?php endforeach; ?>
</div>
views\status\status.php
<div class="status">
    <div class="status_content">
        <a href="<?php echo $base_url; ?>/user/<?php echo $this->escape($status['user_name']); ?>">
            <?php echo $this->escape($status['user_name']); ?>
        </a>
        <?php echo $this->escape($status['body']); ?>
    </div>
    <div>
        <a href="<?php echo $base_url; ?>/user/<?php echo $this->escape($status['user_name']);
        ?>/status/<?php echo $this->escape($status['id']); ?>">
            <?php echo $this->escape($status['created_at']); ?>
        </a>
    </div>
</div>
  • indexAction の 作成
controllers\StatusController.php
// +
    protected $auth_actions = array('index', 'post');

// +
    public function indexAction()
    {
        $user = $this->session->get('user');

//+ user と follower の コメント を 取得
        $statuses = $this->db_manager->get('Status')
            ->fetchAllPersonalArchivesByUserId($user['id']);

        return $this->render(
            array(
                'statuses' => $statuses,
                'body' => '',
                '_token' => $this->generateCsrfToken('status/post'),
            )
        );
    }
  • sql メソッドの追加
models\StatusRepository.php
//+ user と follower の コメント を 取得
    public function fetchAllPersonalArchivesByUserId($user_id)
    {
//perfectphp の sql
        // $sql = "
        //     SELECT a.*, u.user_name
        //     FROM status a
        //         LEFT JOIN users u ON a.user_id = u.id
        //         LEFT JOIN following f ON f.following_id = a.user_id
        //             AND f.user_id = :user_id
        //         WHERE f.user_id = :user_id OR u.id = :user_id
        //         ORDER BY a.created_at DESC
        // ";

// AND f.user_id = :user_id  これ処理が重複している
// WHERE f.user_id = :user_id OR u.id = :user_id 
//        👆 ここで絞っているので不要といことではない。

        $sql = "
            SELECT a.*, u.user_name
            FROM status a
                LEFT JOIN users u ON a.user_id = u.id
                LEFT JOIN following f ON f.following_id = a.user_id
                WHERE f.user_id = :user_id OR u.id = :user_id
                ORDER BY a.created_at DESC
        ";

        return $this->fetchAll($sql, array(':user_id' => $user_id));
    }
  • statusテーブルをベースにテーブルを結合させる。どのカラムで結合させるかは外部キーと主キーの関係で結合させる。
    • しかし、status と followers は user_id を介して結合させている。
      image.png
      image.png
  • 結合した テーブルを where で絞っている
    image.png
  • 抽出するカラムは statusテーブルのカラムとusersテーブルのuser_name
    image.png
  • 最後に status の crated_at で 降順 で並び替え
    image.png
    例えば followers テーブルの下のようなデータを結合した場合
    image.png
    image.png
    結合されたテーブル
    image.png
    image.png

よって AND f.user_id = :user_idfollowing_id を一意にしぼっている
image.png
where で f.user_id = 1 と status.user_id=1 の2つの データを取得していると考えてもよかった
image.png

where in 旬 は PDO では 鬼門

pdo + where + in
$sql = "SELECT following_id FROM following WHERE user_id = :user_id";
$ids = $this->fetchAll($sql, array(':user_id' => $user_id));
array_push($ids,['following_id'=>$user_id]);
$ids = implode(',',array_column($ids,'following_id'));

// これはうまくいかない
$sql = "
SELECT a.*, u.user_name
FROM status a
    LEFT JOIN users u ON a.user_id = u.id
    WHERE a.user_id IN (:ids)
    ORDER BY a.created_at DESC
";
return $this->fetchAll($sql, ['ids'=>$ids]);

// こうする必要がある
// 名前付きプレスホルダーがつかえない
// 要素の数だけ ? が必要になる ('◇')ゞ
$sql = "
SELECT a.*, u.user_name
FROM status a
    LEFT JOIN users u ON a.user_id = u.id
    WHERE a.user_id IN (?,?,?)
    ORDER BY a.created_at DESC
";
return $this->fetchAll($sql, [3,2,1]);
  • だから left join や inner join から データを作成を優先したほうがいい。

コメントを投稿する

  • action の追加
controllers\StatusController.php
    public function postAction()
    {
// メソッドのチェック
        if (!$this->request->isPost()) {
            $this->forward404();
        }

// csrf の チェック
        $token = $this->request->getPost('_token');
        if (!$this->checkCsrfToken('status/post', $token)) {
            // return $this->redirect('/');
            $this->redirect('/');
        }

// 入力値を取得
        $body = $this->request->getPost('body');

// バリデーション
        $errors = array();

        if (!strlen($body)) {
            $errors[] = 'ひとことを入力してください';
        } else if (mb_strlen($body) > 200) {
            $errors[] = 'ひとことは200 文字以内で入力してください';
        }

// バリデーションエラーがないなら
        if (count($errors) === 0) {

            $user = $this->session->get('user');
// 投稿して
            $this->db_manager->get('Status')->insert($user['id'], $body);
// ホームにリダイレクト 多重投稿防止のため
            // return $this->redirect('/');
            $this->redirect('/');
        }

// バリデーションエラーが存在したら
        $user = $this->session->get('user');
        $statuses = $this->db_manager->get('Status')
            ->fetchAllPersonalArchivesByUserId($user['id']);

        return $this->render(
            array(
                'errors' => $errors,
                'body' => $body,
                'statuses' => $statuses,
                '_token' => $this->generateCsrfToken('status/post'),
            ), 'index');
    }
  • sql メソッドの追加
models\StatusRepository.php
// +
    public function insert($user_id, $body)
    {
        $now = new DateTime();

        $sql = "
            INSERT INTO status(user_id, body, created_at)
                VALUES(:user_id, :body, :created_at)
        ";

        $stmt = $this->execute($sql, array(
            ':user_id'    => $user_id,
            ':body'       => $body,
            ':created_at' => $now->format('Y-m-d H:i:s'),
        ));
    }
  • 投稿して表示されればOK
    image.png

投稿の詳細を表示

  • view の作成
views\status\show.php
<?php $this->setLayoutVar('title', $status['user_name']) ?>

<?php echo $this->render('status/status', array('status' => $status)); ?>
  • アクションメソッドの追加
views\status\show.php
    public function showAction($params)
    {
        $status = $this->db_manager->get('Status')
            ->fetchByIdAndUserName($params['id'], $params['user_name']);

        if (!$status) {
            $this->forward404();
        }

        return $this->render(array('status' => $status));
    }
  • sql メソッドの追加
models\StatusRepository.php
// 特定のコメントの取得
    public function fetchByIdAndUserName($id, $user_name)
    {
// stutas の id で 絞っているのに、なんで さらに user_name で絞る必要はない。
// 訂正の依頼中
        $sql = "
            SELECT a.* , u.user_name
                FROM status a
                    LEFT JOIN users u ON u.id = a.user_id
                WHERE a.id = :id
                    AND u.user_name = :user_name
        ";

        return $this->fetch($sql, array(
            ':id'        => $id,
            ':user_name' => $user_name,
        ));
    }

ユーザーのコメント一覧を表示

  • view の作成
views\status\user.php
<?php $this->setLayoutVar('title', $user['user_name']) ?>

<h2><?php echo $this->escape($user['user_name']); ?></h2>

<?php if (!is_null($following)): ?>
<?php if ($following): ?>
<p>フォローしています</p>
<?php else: ?>
<form action="<?php echo $base_url; ?>/follow" method="post">
    <input type="hidden" name="_token" value="<?php echo $this->escape($_token); ?>" />
    <input type="hidden" name="following_name" value="<?php echo $this->escape($user['user_name']); ?>" />

    <input type="submit" value="フォローする" />
</form>
<?php endif; ?>
<?php endif; ?>

<div id="statuses">
    <?php foreach ($statuses as $status): ?>
    <?php echo $this->render('status/status', array('status' => $status)); ?>
    <?php endforeach; ?>
</div>
  • アクションメソッドの追加
controllers\StatusController.php
// +
    public function userAction($params)
    {
// ユーザーの取得
        $user = $this->db_manager->get('User')
            ->fetchByUserName($params['user_name']);
        if (!$user) {
            $this->forward404();
        }
// ユーザーのコメント一覧を取得
        $statuses = $this->db_manager->get('Status')
            ->fetchAllByUserId($user['id']);

// auth が その user を follow しているかの確認
        $following = null;
  // 認証しているか
        if ($this->session->isAuthenticated()) {
  // auth != user か
            $my = $this->session->get('user');
            if ($my['id'] !== $user['id']) {
  // true なら
  // そのユーザーを follow しているかいないかの取得
                $following = $this->db_manager->get('Following')
                    ->isFollowing($my['id'], $user['id']);
            }
        }

        return $this->render(
            array(
                'user' => $user,
                'statuses' => $statuses,
                'following' => $following,
                '_token' => $this->generateCsrfToken('account/follow'),
            )
        );
    }
  • sql メソッドの追加
models\StatusRepository.php
// + 特定のユーザーのコメント一覧の取得
    public function fetchAllByUserId($user_id)
    {
        $sql = "
            SELECT a.*, u.user_name
                FROM status a
                    LEFT JOIN users u ON a.user_id = u.id
                WHERE u.id = :user_id
                ORDER BY a.created_at DESC
        ";

        return $this->fetchAll($sql, array(':user_id' => $user_id));
    }
  • FollowingRepository の 作成
models\FollowingRepository.php
<?php
// +

/**
 * StatusRepository.
 *
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class FollowingRepository extends DbRepository
{
    public function isFollowing($user_id, $following_id)
    {
        $sql = "
            SELECT COUNT(user_id) as count
                FROM following
                WHERE user_id = :user_id
                    AND following_id = :following_id
        ";

        $row = $this->fetch($sql, array(
            ':user_id'      => $user_id,
            ':following_id' => $following_id,
        ));

        if ($row['count'] !== '0') {
            return true;
        }

        return false;
    }
}
  • http://localhost/MiniBlog/user/user_name
    image.png

ユーザーを follow する

  • actionメソッドのつ
controllers\AccountController.php
    public function followAction()
    {
        if (!$this->request->isPost()) {
            $this->forward404();
        }

        $following_name = $this->request->getPost('following_name');
        if (!$following_name) {
            $this->forward404();
        }

        $token = $this->request->getPost('_token');
        if (!$this->checkCsrfToken('account/follow', $token)) {
            return $this->redirect('/user/' . $following_name);
        }

        $follow_user = $this->db_manager->get('User')
            ->fetchByUserName($following_name);
        if (!$follow_user) {
            $this->forward404();
        }

        $user = $this->session->get('user');

        $following_repository = $this->db_manager->get('Following');
        if ($user['id'] !== $follow_user['id'] 
            && !$following_repository->isFollowing($user['id'], $follow_user['id'])
        ) {
            $following_repository->insert($user['id'], $follow_user['id']);
        }

        return $this->redirect('/account');
    }
  • sql メソッドの追加
models\FollowingRepository.php
// +
    public function insert($user_id, $following_id) {
        $sql = "INSERT INTO following VALUES(:user_id, :following_id)";

        $stmt = $this->execute($sql, array(
            ':user_id'      => $user_id,
            ':following_id' => $following_id,
        ));
    }

  • follow できたら OK
    image.png
0
0
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
0
0