デモサイト
開発環境
-
xampp
-
アプリケーション名
- MiniBlog
-
配置場所
- xamppのhtdocsの直下
-
ヴァーチャルホストの追加
- ヴァーチャルホストの追加は追加せず
localhost
でいく- パーフェクトPHPでは ヴァーチャルホストの追加している
http://Mini-blog.localhost
- パーフェクトPHPでは ヴァーチャルホストの追加している
- ヴァーチャルホストの追加は追加せず
-
ドキュメントルート
- ドキュメントルートは変更せず アプリケーションが入っているディレクトリにはアクセスできないように
.htaccess
で制限する。- xampp の
htdocs
のまま。 - パーフェクトPHPではドキュメントルートを変更をしている
-
htdocs/MiniBlog/web
がドキュメントルートになる。
-
- xampp の
- ドキュメントルートは変更せず アプリケーションが入っているディレクトリにはアクセスできないように
-
MVCモデルによる役割の分離
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/ にインナーリダイレクトさせる。
<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>
- MiniBlogにアクセスすると
-
/MiniBlog/web/index.php
ファイルにインナーリダイレクト
されindex.php
がレスポンスされる。
- 内部のスタイルも
- このurlにインナーリダイレクトされる。
- 画像も同様にインナーリダイレクトされる。
- 赤文字のパスがそのまま後方参照されている。
- web ディレクトリにリライトされるとオレンジの処理によってファイルが存在する場合はリライトせずそのままファイルが取得される。
- file1.php を 指定した場合
-
/MiniBlog/web/index.php
が レスポンスされる。
- web ディレクトリには
file1.php
が存在しないため、/MiniBlog/web/index.php
にインナーリダイレクトされるため
-
.htaccess
の 適用順位
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();
アプリケーションクラスの作成
.
├── 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
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関数の作成
- 便利な関数たちを記述
- これはパーフェクト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クラスの作成の前に
- 動的ルーティングにするため正規表現を使用しているための最低限の正規表現の解説
正規表現について
- URLの正規表現のデリミタは
'/'
ではなく'#'
を使う- urlを正規表現する時の注意点
- ようするに URLの区切り文字であるこれ
/
はメタ文字の/
と区別する必要がないためエスケープする必要がなくなる-
\/
こうではなく普通に/
これでOKになる。
-
- preg_match('#^'.
$request_url
.'$#',$url
,$matches
); - バリデーションで行頭と行末を表すメタ文字は
^
と$
ではなく\A
と\z
のほうがセキュリティ的にはいいらしい- 正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう;
-
^abcd$
- 行頭と行末のメタ文字でパターンを囲むと対象文字列の全ての文字列がパターンと一致する必要がある。
- 👆の場合対象文字列が
abcd
の時だけ一致する。
- 動的ルーティングに対応するため正規表現のパターンには名前付きキャプチャを利用する
- パターン例 :
'#^user/(?P<username>[^/]+)$#'
だった場合-
(?P<username>[^/]+)
<- この部分が名前付きキャプチャ -
()
の後方参照 と 一緒につかう- 後方参照 とは
- ()がないパターン :
#^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'),
);
}
}
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でコントローラーとアクションの配列を返すメソッド
<?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);
}
}
※確認 http://localhost/MiniBlog/
- 本来 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
- パーフェクトPHPでは xamppの ドキュメントルートを
-
取得したいのはフロントコントローラー以下のパス
-
$_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
※確認
urlにフロントコントローラーまでのフルパスがない 1
http://localhost/MiniBlog/user/yamadataro
※確認
urlにフロントコントローラーまでのフルパスがない 2
http://localhost/MiniBlog/account
※確認
urlにフロントコントローラーまでのフルパスがない 3
http://localhost/MiniBlog/account/signup
-
その他の パターンに一致するパスを確認できれば OK
-
その他リクエストのメソッドの追加
/**
* @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クラスの作成
.
├── 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でもアクセス
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 にアクセス
- 結果何も出力されない
-
$content = ob_get_clean();
- 保持したデータを取得し変数に代入する
- データ 代入された 変数を
echo
することで 出力できる。
<?php
ob_start();
// デフォルトだとバッファの上限が来ると自動で出力されてしまうので、無効化しておきます。
ob_implicit_flush(0);
echo '今日の天気は';
?>
<h1>曇りのちはれ</h1>
<?php
// 作成したファイルを変数コンテントにいれる。
$content = ob_get_clean();
// + それを出力する
echo $content;
<?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>
sample/1.ph
がecho
される流れは上と全く同じです。
sample/2.php
について
-
sample/1.php
は バッファされているためsample/2.php
はsample/1.php
のなかでバッファされる。 -
最後に
sample/1.php
のデータがob_get_clean();
関数で取得しその結果をecho
して出力される。
-
👇の
layout.php
は ことなる 経過 で 保持され 出力される。
layout ファイルを読み込む場合
// 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;
}
-
view クラスを Controller クラス に 実装する。
- コントローラクラスにも同名の
render
メソッドがあるため混同しなよう注意 - コントローラーからテンプレートファイルや、layout ファイルを指定できるような制度設計になっている。
- コントローラクラスにも同名の
-
データの流れを理解するために 先に継承クラスの使い方を見る
<?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>
※確認 indexActionメソッドにアクセス
http://localhost/MiniBlog/account
※確認 showActionメソッドにアクセス
http://localhost/MiniBlog/account/山田太郎
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メソッドだけに統一 している。
<?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
}
}
- 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();
}
※確認 普通に作成した status/index画面 show画面が取得できればOK
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
- データーベースを作成する
- データーベース名
- mini_blog
- データーベース名
CREATE DATABASE mini_blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
- テーブルを作成する
-
phpMyAdmin-mini_blog
- users (ユーザー情報)
- following (user同士の中間テーブル)
- status (ユーザーが投稿した情報)
-
phpMyAdmin-mini_blog
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)
- 具体的なsqlを発行するメソッド群
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を作る
<?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'));
}
}
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の継承クラス
をインスタンス化する。
- (DbManagerクラスでは)
<?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));
}
DbRepositoryクラスの継承クラスとpdoのマッチング
- 全体の流れ例
- Accountコントローラーのregisterメソッドでusersテーブルにuserを登録する場合
1⃣ DbManegerからUserRepositoryを取得する メソッド
取得する前に
👆 プロパティに UserRepository がないか確認している
なければ UserRepository を作成している
作成したRepository は プロパティに repository名で保存している。
最後に UserRepository を 返している
ようするに 1⃣ の メソッドは
👆 のプロパティのgetter
とsetter
になっている。
2⃣ getConnectionForRepository メソッド
1⃣ の Repository作成時の$con
変数に pdo オブジェクトを返すメソッド
1.$repository_connection_map
プロパティにrepositoryがあるか確認して
2.getConnection()をコールして
3.取得した pdoを 返すメソッド
👆のプロパティにレポジトリの名前があるか確認してあれば、その pdoの$name を渡して その pdo オブジェクトを取得しにいくメソッドをコールする
3⃣ 👇$connections
プロパティのゲッター で pdo を返す
- Accountコントローラーのregisterメソッドでusersテーブルにuserを登録する場合
$repository_connection_map
のセッターがこれ
特定の pdo を指定したい場合につかう
$connections
プロパティのセッターは pdo を作成時に 行っている
作成した pdo は 上記の流れにそって Repository に セットされる。
- 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();
}
UsersテーブルのCRUD
- そのまえに
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'),
);
}
-
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>
- アクションメソッドの作成
-
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>
- アクションメソッドの作成
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>
例外機能を 実装する。
- 例外をどこでチャッチするかは大きな問題。
- メソッドごとに例外を補足すると保守性や維持など著しく非効率になる。
- 例外が握りつぶされる可能性もある
-
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を返すときにスローさせる例外クラス
<?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);
}
- 適当なURLでアクセスしてみる
No route found for /gogo
認証システムを実装する。
- セッションによる認証機能の実装
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アクションを登録する
// 未認証で例外が飛んだ場合このサイトにアクセスされるようになる。
}
-
ログインフォームの作成
- アクションメソッドの作成
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
- アクションメソッドの作成
-
ログイン機能作成
//+
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');
perfectPHPでのフレームワークは完了
その他 最低限あったらいいなと思ったクラス
- バリデーションクラス
- 技術書典5『はじめてのLaravel』が参考になる
- イメージクラス
- メールクラス
- ログクラス
など - DIコンテナークラス
status の作成
- コントローラーの作成
<?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);
}
user 一覧の作成
- 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);
}
ホーム の作成
- 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));
}
- statusテーブルをベースにテーブルを結合させる。どのカラムで結合させるかは外部キーと主キーの関係で結合させる。
- 結合した テーブルを
where
で絞っている
- 抽出するカラムは statusテーブルのカラムとusersテーブルのuser_name
- 最後に status の crated_at で 降順 で並び替え
例えば followers テーブルの下のようなデータを結合した場合
結合されたテーブル
よって AND f.user_id = :user_id
は following_id
を一意にしぼっている
where で f.user_id = 1 と status.user_id=1 の2つの データを取得していると考えてもよかった
where in 旬 は 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'),
));
}
投稿の詳細を表示
- 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;
}
}
ユーザーを follow する
- 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,
));
}