完美的PHP微博应用程序

演示网站

但它没有工作。 tā .)

研发环境

image.png

将Web目录进行重写

/MiniBlog/web/フォルダを 事実上の ドキュメントルートにするため

MiniBlog
├── web <- ここ以下のフォルダ以外のアクセスはリライトさせる。
│   ├── css
│   │   └── style.css
│   ├── img
│   │   └── img01.png
│   ├── index.php
│   └── .htaccess
├── .htaccess <-- リライト処理するファイル
└── file.php <-webフォルダ以外のファイルはURLからアクセスできないようにする
    • .htaccessはApacheの設定を変更できるファイル

.htaccess 詳細サイト

MiniBlog 以下のディレクトリすべてに適用される。

url から MiniBlog 以下にアクセスされれば、/MiniBlog/web/ にインナーリダイレクトさせる。

<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 を 隠すことができる。

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ /MiniBlog/web/index.php [QSA,L]
</IfModule>
    リライト機能の動作確認
<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>
body{
    background: green;
}
<h1>file</h1>
image.png
image.png
image.png

创建ClassLoader类

    • クラスファイルを自動で読み込むために必要なプロパティとメソッドを集めたクラス

 

    • php では他ファイルは読み込むことで アクセス することができるようになる。

file1.php
// 配列だけを返すファイル
return [
‘a’ => ‘A’
];

file2.php
$aaa = require ‘file1.php’;

print_r($aaa);
// Array( ‘a’ => ‘A’)

.
├── core
│   └── Classloader.php (作成)
│   └── Application.php (作成)
└── bootstrap.php (作成)
    • オートロードのためのルール

1つのファイルに1つのクラス

ファイル名はクラス名.phpにする

spl_autoload_register関数について

<?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にはアプリケーションを立ち上げるための動作という意味がある
アプリケーションを実行するにあたってオートロードはまず最初に行う処理である
また、オートロードの他に特別に必要な前処理が出てきた場合には、それらを実行するための処理を記述する場所でもある
フレームワークは特定のアプリケーションに依存しない共通処理をまとめたものなので、ルートディレクトリ(階層型ファイル構造の最上階層のディレクトリのこと)の直下に配置するのが望ましい

<?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プロパティに保存して
この クラス名 を オートローダーで取得し読み込んで処理する。

<?php

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

创建前端控制器

.
├── core
│   └── Classloader.php
├── web 
│   ├── index.php   (作成)
│   └── .htaccess   (作成して?のコードをコピペ)
├── .htaccess       (作成して?のコードをコピペ)
└── bootstrap.php
    • 全てのリクエスト(web/index.php)を受けとるファイル。

 

    ファイルの読み込みを一ヵ所で記述できる点でページコントローラーより効率的になる。
<?php
require_once __DIR__.'/../bootstrap.php';
// new したタイミングで オートロードしてくれるため改めて読みこむ処理が不要
$app = new Application();
$app->run();
image.png

创建一个应用程序类

.
├── core
│   ├── Application.php 
│   └── Classloader.php
├── web
│   └── index.php
├── .htaccess
├── bootstrap.php
└── MiniBlogApplication.php (作成)
    • アプリケーションクラス(フロントコントローラーの機能を一部 委譲させている)

デバックモードを設定するメソッド
各クラスの絶対パスを取得するメソッド
全てのクラスを初期化するためのメソッド
メインルーチンを実行するメソッド

ルータークラスから
リクエストに応じた
コントローラークラスを初期化してメソッドを実行
その中でmodelクラスでデータを取得したり
viewクラスでHTMLを作成して
それをレスポンスクラスに渡して
最後にユーザーにレスポンスする。

<?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;
    }
}
    • 継承クラス

具体的な処理は作成するプロジェクトに任せる。

<?php

class MiniBlogApplication extends Application
{
// 継承クラスでは必ずgetRootDir()が記述する必要がある
    public function getRootDir() // 階層型ファイル構造の最上階層のディレクトリのこと
    {
// 自身がいるディレクトリの絶対パスを返す。
//MiniBlogApplication.phpのbasename(ファイル名)を除いたパスがかえる。
       return dirname(__FILE__); 
    }
}
    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

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'

创建一个辅助函数。

    • 便利な関数たちを記述

これはパーフェクトphpからではなく技術書典5『はじめてのLaravel から拝借しました

<?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では 関数は必ずグローバル空間に配置されるためファイルを読みこめば、そのままアクセスが可能になる。

<?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类之前

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

 

    phpの正規表現についての詳細サイト

关于正则表达式

    • URLの正規表現のデリミタは’/’ではなく’#’を使う

urlを正規表現する時の注意点
ようするに URLの区切り文字であるこれ / はメタ文字の/と区別する必要がないためエスケープする必要がなくなる

\/こうではなく普通に / これでOKになる。

preg_match(‘#^’.$request_url.’$#’, $url, $matches);
バリデーションで行頭と行末を表すメタ文字は^と$ではなく\Aと\zのほうがセキュリティ的にはいいらしい

正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう;

^abcd$

行頭と行末のメタ文字でパターンを囲むと対象文字列の全ての文字列がパターンと一致する必要がある。
?の場合対象文字列が abcd の時だけ一致する。

動的ルーティングに対応するため正規表現のパターンには名前付きキャプチャを利用する
パターン例 : ‘#^user/(?P[^/]+)$#’だった場合

(?P[^/]+) <- この部分が名前付きキャプチャ

()の後方参照 と 一緒につかう

後方参照 とは

()がないパターン : #^user/[^/]+$#

()があるパターン : #^user/([^/]+)$#

^user/ 行頭はuser/で始まりその後の行末までの文字列は

[^/]+$ 行末までの1文字以上の文字列なかに/が含まれていないならマッチする。

<?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

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);
Array
(
// 一致したら [0] には user/文字列 がかえる
    [0] => user/山田太郎  

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

注册根目录的方法

    • ルートはアプリケーション固有の情報のためMiniBlogApplicationクラスに実装

larabel でいうと routes/web.php ファイルにあたる

<?php
abstract class Application
{

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

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

<?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

创建一个路由器类 (CrouterClass)。

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[^/]+)に変換するメソッド

resolve()

リクエストされたURLでコントローラーとアクションの配列を返すメソッド

<?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类中实施实现。

<?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);
    }
}
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に応じて場合分けしてフロントコントローラーのパスを削除している。

<?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クラスに実装する
<?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。

※确认
网址中没有到前端控制器的完整路径 1 http://localhost/MiniBlog/user/yamadataro

※确认
url没有前端控制器的完整路径 2 http://localhost/MiniBlog/account
※确认
url没有前端控制器的完整路径 3 http://localhost/MiniBlog/account/signup

image.png
/**
 * @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类

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
// 修正
    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);
    }
}
    • コントローラークラス (フレームワーク)

コントローラーで共通する処理をまとめている。
それぞれのコントローラーはこのクラスを継承することで重複する処理を削減できる。

<?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に必要な情報を制御して返すためのコントローラークラス

<?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 に記述する

個々のプロジェクトはフレームワークの汎用的なコードを利用しながら記述していけるようになる。

<?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。

image.png

创建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(); で 取得し 変数に 代入できる。

<?php
// 実行
ob_start();
// デフォルトだとバッファの上限が来ると自動で出力されてしまうので、無効化しておきます。
ob_implicit_flush(0);

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

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

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

访问 http://localhost/MiniBlog/test.php

image.png
<?php
ob_start();
// デフォルトだとバッファの上限が来ると自動で出力されてしまうので、無効化しておきます。
ob_implicit_flush(0);

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

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

<?php
// 作成したファイルを変数コンテントにいれる。
$content = ob_get_clean();
// + それを出力する
echo $content;
image.png
<?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');
    読みこむファイルを作成
<h1>sample/1.php のファイル</h1>
<p><?php echo $this->escape('<script>alert("今日は涼しいです。")</script>') ?></p>

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

下述是对原文的中文本地化释义:

1. 对于sample/1.ph,echo的流程与上述完全相同。
2. 关于sample/2.php。

image.png

如果要加载布局文件

// render メソッドの引数 パス、変数、layoutファイルのパス にすることで、
// 呼び出し時に、データ渡したり、、layoutファイルを指定することができる。
    public function render($path,$variables,$layout=null){}
// 呼び出し時に layoutファイルパスを指定する。
    $view->render('sample/1.php',array(),'sample/layout.php');
    views クラスを修正する
<?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');
    }
}
<?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());
<h1>sample/2.php のファイル</h1>

layoutファイル を指定して実行する

$view = new View();
// sample/1.php
$view->render('sample/1.php',array(),'sample/layout.php');
// $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;
    }
// $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
<?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);
    }
}
// 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メソッドと混同しないように注意。

<?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 クラスの動作確認

まずは コントローラーを作成する。

<?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ファイルを作成する

<?php
$this->setLayoutVar('title', 'userのindex.php');
?>
<h1>userのindex.php</h1>
<?php
$this->setLayoutVar('title', 'statusのshow.php');
?>
<h1>ユーザーの名前: <?php echo $user['name'] ?></h1>
<!DOCTYPE html>
<head>
    <title><?php echo $title ?></title>
</head>
<body>
    <?php echo $_content ?>
</body>
</html>
image.png

创建Response类

    • HTTPヘッダやHTML を ユーザーに 返すためのクラス

phpでは header関数でいつでも HTTPヘッダ情報をユーザーに送信できる。
header関数 の ラッパークラス

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メソッドだけに統一 している。
<?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クラスに実装する
<?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();
    }

如果能够正确获取通过常规方式创建的状态/索引界面和展示界面,那就可以了。
http://localhost/MiniBlog/
http://localhost/MiniBlog/user/yamadataro/status/2

    redirect メソッドを実装する
//+
    /**
     * 指定された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();
    }

创建数据库和表

    • データーベースコネクション

mysql

phpmyadmin

データーベースを作成する

データーベース名

mini_blog

CREATE DATABASE mini_blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
    • テーブルを作成する

phpMyAdmin-mini_blog

users (ユーザー情報)
following (user同士の中間テーブル)
status (ユーザーが投稿した情報)

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;
    テーブルの構造確認
DESC status;

创建一个类,该类收集了用于DB连接的属性和方法。

    • DbManagerクラス

pdoを作成するクラス
DbRepositoryの継承クラス(プロジェクトに属するクラス)をインスタンス化するクラス

DbRepositoryの継承クラスはDbManagerクラスに隠れる形になる。

DbRepository

テーブルからデータを取るフレームワークのクラス

DbRepositoryの継承するクラスはプロジェクトに属するクラス

具体的なsqlを発行するメソッド群

controllerで sql を書かずに このクラスのメソッドでデータを取得する。

配置場所

models

名前

テーブル名Repository (例. UserRepository)

完美的PHP框架

    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を作る
<?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クラスに実装する。
<?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;
    }
}
    アプリケーションクラスでの使用の仕方
<?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'));
    }
}
image.png
    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にデータを登録できる設計になっている。

<?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の継承クラス をインスタンス化する。

<?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的类。

<?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));
    }
}
    ユーザーを登録して確認
    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的匹配。

image.png
image.png
    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);
        }
    }
}
    アプリケーションクラスでの使用方法
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 ;        

    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認証で使用
<?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クラスに実装する

//+    
    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クラスにオブジェクトを渡す
    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();
    }

用户表的增删改查操作

    そのまえに
    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の確認
    protected function registerRoutes()
    {
        return array(

             // 省略

            '/account'
                => array('controller' => 'account', 'action' => 'index'),
 //これは完全に削除する '/account/:username'
               // => array('controller' => 'account', 'action' => 'show'),
            '/account/:action'
                => array('controller' => 'account'),
        );
    }
image.png

实施防止跨站请求伪造(CSRF)的措施。

    • form や fetch(クロスオリジンを除く) 送信する場合 必ず絶対やらなければいけない 対策

 

    • フレームワークではワンタイムトークン形式(一般的)をとっている。

 

    • また複数のウィンドウ(10件以下)で別々のtokenを作成してチェックできるよう設計されている。

 

    Controller クラスで 実装する。(普通はSessionクラスで実装されることが多い)
/**
 * @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トークン を フォームに 追加
// 修正
    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');
    }
<?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

实施例外功能。

    • 例外をどこでチャッチするかは大きな問題。

メソッドごとに例外を補足すると保守性や維持など著しく非効率になる。
例外が握りつぶされる可能性もある

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 ‘
‘;
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を返すときにスローさせる例外クラス

<?php
/**
 * @author Katsuhiro Ogawa <fivestar@nequal.jp>
 */
class HttpNotFoundException extends Exception {}
    • UnauthorizedActionException クラスの作成

ユーザー認証が失敗した時にスローさせる例外クラス

<?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 クラスに 実装させる
    /**
     * コントローラーのアクションメソッドを 文字列で 実行 させるメソッド
     *
     * @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);
    }
image.png

实施认证系统。

    セッションによる認証機能の実装
    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');
    }
//+
    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);
    }
    • 認証が必要なアクションメソッド を コントローラーのプロパティ で登録

 

    • リクエストがあった場合 プロパティをチェックして 認証が必要なアクションかどうかを判断

 

    認証が必要なアクションで認証がなかったらログインサイトに転送
//  ? 個々のコントローラーで 具体的に 認証が必要な アクション名を登録する
    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;
    }
    未認証で例外スローされた場合
//+
    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 プロパティの登録
//+
   public function getLoginAction(){
      return ['account','signin'];
//   ? アカウントコントローラーのsigninアクションを登録する
//  未認証で例外が飛んだ場合このサイトにアクセスされるようになる。
   }
image.png
//+
    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');
    }
    ログアウトの実装
//+
    public function signoutAction()
    {
        $this->session->clear();
        $this->session->setAuthenticated(false);

         $this->redirect('/account/signin');
    }
    Accountのindexアクションに認証をつける
//+
    protected $auth_actions = array('index');
image.png

在perfectPHP中,框架已经完成。

希望拥有的其他基本课程

    • バリデーションクラス

技術書典5『はじめてのLaravel』が参考になる

イメージクラス
メールクラス
ログクラス
など
DIコンテナークラス

创建状态

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

}
    Repository の作成
<?php

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

创建Navi

    view の作成
<?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 の読み込み
<!DOCTYPE html>
<head>
    <title><?php echo $title ?></title>
</head>
<body>

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

    <?php echo $_content ?>
</body>
</html>
    view コントローラーに sessionクラスを渡す。
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);
}
image.png

创建用户列表

    root の追加
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 の 作成
<?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 メソッドの作成
// +
    public function listAction(){
        $users = $this->db_manager->get('User')->all();

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

创建主页

    view の作成
<?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>
<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 の 作成
// +
    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 メソッドの追加
//+ 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));
    }
image.png
image.png

在中国,“旬”意为一两个星期。在PDO中,“鬼门”是指邪门。

$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 の追加
    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 メソッドの追加
// +
    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'),
        ));
    }
image.png

显示投稿细节

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

<?php echo $this->render('status/status', array('status' => $status)); ?>
    アクションメソッドの追加
    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 メソッドの追加
// 特定のコメントの取得
    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 の作成
<?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>
    アクションメソッドの追加
// +
    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 メソッドの追加
// + 特定のユーザーのコメント一覧の取得
    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 の 作成
<?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;
    }
}
image.png

追踪用户

    actionメソッドのつ
    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 メソッドの追加
// +
    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,
        ));
    }

image.png
广告
将在 10 秒后关闭
bannerAds