PHPのMVCフレームワークを作った
お久しぶりです。
今回はプライベートでPHPのMVCフレームワークを作る機会があったので、記録ついでに書いていきます。
とりあえずMVCだけあれば良かったので、routing, Response, Requestあたりは作ってません。
public/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
Apacheの設定ファイルですね。
ここでは、パスと一致するファイルが存在しない場合、アクセスをindex.phpに集約するということが書かれてます。
少し雑ですが、要件としては問題ないのでこれで。
public/index.php
<?php
define('ROOT_PATH', str_replace('public', '', $_SERVER["DOCUMENT_ROOT"]));
require_once(ROOT_PATH.'Controllers/ContactController.php');
use Controllers\ContactController; // pathを取得 $path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); if ($path === '/contact/index') { $controller = new ContactController(); $controller->index(); } if ($path === '/contact/confirm') { $controller = new ContactController(); $controller->confirm(); } if ($path === '/contact/complete') { $controller = new ContactController(); $controller->complete(); }
超絶手作業ですが、routingは今回スコープ外なのでこれで。
例えば、http://localhost:8888/contact/index
でアクセスされたらContactControllerのindexメソッドが呼ばれます。
ちなみに今回は問い合わせフォームを作ってます。
ContactController#index
class ContactController
{
public function index()
{
ViewBase::render('/Contact/index.php');
}
~~~
本当はrequireとかuseを上の方に書いてますが、省略。
indexメソッドにアクセスが来たら対象のviewファイルをrenderするようにしています。
renderの中身は後述しますが、ViewBaseというrender用のクラスを作ってます。
名前適当につけたんですが、一般的なフレームワークはなんて名前付けてるんでしょう。
symfonyはここから先追えなかったのでわかりませんでした。
symfony/AbstractController.php at 6.1 · symfony/symfony · GitHub
ViewBase
class ViewBase
{
public static function render($fileName, $params = [])
{
$filePath = ROOT_PATH.'/Views/'.$fileName;
extract($params);
ob_start();
ob_implicit_flush(0);
require $filePath;
$content = ob_get_clean();
echo $content;
exit;
}
}
Viewsディレクトリにあるファイルを出力してます。
埋め込む変数も渡してextractで作っています。
この辺書いていて思い出したんですが、renderの元を辿るとechoだったなと。
我々はechoから始まったんだった。というのを思い出させられました。
普段フレームワーク使ってると意識しないですよね。
ContactController#confirm
class ContactController
{
public function confirm()
{
// POSTのみ受け付ける
if (!$_POST) {
header('Location: /contact/index');
exit;
}
$contactValidator = (new ContactValidator($_POST));
if ($contactValidator->isValid()) {
ViewBase::render(
'/Contact/confirm.php',
[
'name' => $_POST['name'],
'kana' => $_POST['kana'],
'tel' => $_POST['tel'],
'email' => $_POST['email'],
'body' => $_POST['body'],
]
);
}
ViewBase::render(
'/Contact/index.php',
[
'name' => $_POST['name'],
'kana' => $_POST['kana'],
'tel' => $_POST['tel'],
'email' => $_POST['email'],
'body' => $_POST['body'],
'errors' => $contactValidator->getErrors(),
]
);
}
confirmです。
validatorを挟んで、問題あれば入力値保持したままindexへ。
問題なければconfirm画面を表示
validatorはただのクラスなので特に載せません。
ContactController#complete
class ContactController
{
public function complete()
{
// POSTのみ受け付ける
if (!$_POST) {
header('Location: /contact/index');
exit;
}
if (!$_POST['name']
&& !$_POST['kana']
&& !$_POST['tel']
&& !$_POST['email']
&& !$_POST['body'])
{
header('Location: /contact/index');
exit;
}
$contactValidator = (new ContactValidator($_POST));
if ($contactValidator->isValid()) {
try {
$pdo = (new Database())->getPdo();
$sql = 'INSERT INTO contacts (name, kana, tel, email, body) VALUE (:name, :kana, :tel, :email, :body)';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':name', $_POST['name'], \PDO::PARAM_STR);
$stmt->bindValue(':kana', $_POST['kana'], \PDO::PARAM_STR);
$stmt->bindValue(':tel', $_POST['tel'], \PDO::PARAM_STR);
$stmt->bindValue(':email', $_POST['email'], \PDO::PARAM_STR);
$stmt->bindValue(':body', $_POST['body'], \PDO::PARAM_STR);
$stmt->execute();
// 表示
ViewBase::render('/Contact/complete.php');
} catch (\PDOException $e) {
error_log($e->getMessage());
var_dump($e->getMessage()); //エラー画面へ遷移させるとかする
exit;
}
}
// indexへ戻す
ViewBase::render(
'/Contact/index.php',
[
'name' => $_POST['name'],
'kana' => $_POST['kana'],
'tel' => $_POST['tel'],
'email' => $_POST['email'],
'body' => $_POST['body'],
'errors' => $contactValidator->getErrors(),
]
);
}
DBに接続して保存してます。
Repositoryは作ってないのでsqlとか直書きです。
Models/Database
class Database
{
private $pdo;
public function __construct()
{
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=casteria;charset=utf8';
$user = 'root';
$password = 'root';
$opt = [
// エラーを取得するため
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
// sqlインジェクションを回避するため
\PDO::ATTR_EMULATE_PREPARES => false,
];
$this->pdo = new \PDO($dsn, $user, $password, $opt);
}
public function getPdo()
{
return $this->pdo;
}
}
DB接続です。
簡単ですがここまで作ってみての感想ですが、あらゆる場所で「あのケースも考慮しなきゃダメでは..?」みたいに常に不安になりながら実装してました。
なのでフレームワークには大感謝ですね。PRしてみようかなという気持ちになります。
また、実務に直接関わることはなさそうですが、忘れていた部分も多かったのでやってみてよかったです。
最近競技プログラミングをちょっとだけやっているのですが、そのスキルがこの辺で使えそうだな、と思いました。