PHPのMVCフレームワークを作った

お久しぶりです。

今回はプライベートでPHPMVCフレームワークを作る機会があったので、記録ついでに書いていきます。

 

とりあえず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してみようかなという気持ちになります。

 

また、実務に直接関わることはなさそうですが、忘れていた部分も多かったのでやってみてよかったです。

最近競技プログラミングをちょっとだけやっているのですが、そのスキルがこの辺で使えそうだな、と思いました。