簡易的なメモアプリ
開発環境
- M1Mac
- Docker
- PHP 8.0
- MySQL 8.0
- composer
目的
MVCモデルを使用して簡易的なアプリケーションを実装してみる
MVCモデルについての説明はこちらをご覧ください。
機能
- ログイン機能
- CRUD機能
ディレクトリ構造
src/
├── controller/
│ ├── LoginController.php
│ ├── SingupController.php
│ └── RecordController.php
├── views/
│ ├── login
│ │ ├── login_form.php
│ │ ├── login_page.php
│ │ ├── delete_users_page.php
│ │ ├── edit_users_form.php
│ │ └── mypage_form.php
│ ├── record/
│ │ ├── recordlist.php
│ │ ├── record_form.php
│ │ └── edit_form.php
│ ├── signup/
│ │ ├── signedup.php
│ │ └── signup_form.php
│ └── layout.php
├── models/
│ ├── UserLogic.php
│ └── listLogin.php
├── web/
│ └── index.php
├── core/
│ ├── Controller.php
│ ├── Response.php
│ ├── Router.php
│ ├── View.php
│ └── setupDB.php
├── Application.php
└── bootstrap.php
MVCモデルに必要なファイルだけ抜粋しています。
データベース構成
テーブル「ログイン」の構成
id: ユニークな整数ID(主キー)
userid: ユーザーID(文字列、ユニーク)
email: メールアドレス(任意)
pass: パスワード(暗号化された文字列を想定)
主キー (id, userid): 「id」と「userid」の両方を主キーとして設定
テーブル「レコード」の構成
id: ユニークな整数ID
userid: ユーザーID(必須)
training_day: トレーニング日(DATE型)
training_name: トレーニングの名前(文字列、主キーの一部)
weight_score: 重量やスコアを記録する整数
count: 回数を記録する整数
主キー (id, training_name): id と training_name の複合主キー
この2つのテーブルを結合して実装します。
ここから本格的に実装していきます。
実装
メイン処理を実装
まずはApplication.phpから実装していきます。
Application.phpの役割はアプリケーション全体の管理です。
主に、アプリケーションの実行とルーティング(URL)の登録です。始まりはApplicationクラス内のrunメソッドから始まります。(runメソッドの実行はweb/index.phpで行います)
なので、メインの処理の流れはrunメソッドです。
コンストラクターでRouterクラスとResponseクラスを作成します。
Routerクラスはパスの対応関係の処理を担います。
例えば、登録してあるルーティング名にないパスを指定した場合は、エラー処理をします。
ResponseクラスではHTTPレスポンスを管理および送信します。
class Application
{
private $router;
protected $response;
public function __construct()
{
$this->router = new Router($this->registerRoutes());
$this->response = new Response();
}
public function run()
{
try{
//名前解決
$params = $this->router->resolve($this->getPathInfo());
if (!$params){
throw new HTTPNotFoundException();
}
$controller = $params['controller'];//制御するクラス名
$action = $params['action'];//コントローラーの中のメソッド名
$this->runAction($controller,$action);
}catch(HTTPNotFoundException){
$this->render404Page();
}
$this->response->send();
}
private function runAction($controllerName,$action)
{
//クラスの名前を作成する(最初の文字を大文字にする 例: LoginController)
$controllerClass = ucfirst($controllerName) . 'Controller';
if(!class_exists($controllerClass)){
throw new HTTPNotFoundException();
}
//コントローラーのクラスを作成(LoginController)
$controller = new $controllerClass();
//コントローラークラス内のメソッドを実行
$content = $controller->run($action);
$this->response->setContent($content);
}
private function registerRoutes()
{
//ルーティングの登録
//URLのパス => コントローラー名,実行するメソッド名
return [
'/' => ['controller' => 'login','action' => 'index'],
'/login' => ['controller' => 'login','action' => 'login'],
'/logout' => ['controller' => 'login','action' => 'logout'],
'/edit' => ['controller' => 'login','action' => 'edit'],
'/update' => ['controller' => 'login','action' => 'update'],
'/delete' => ['controller' => 'login','action' => 'delete'],
'/mypage' => ['controller' => 'login','action' => 'mypage'],
'/signup' => ['controller' => 'signup','action' => 'index'],
'/signup/register' => ['controller' => 'signup','action' => 'register'],
'/record' => ['controller' => 'record','action' => 'index'],
'/record/create' => ['controller' => 'record','action' => 'create'],
'/record/register' => ['controller' => 'record','action' => 'register'],
'/record/edit' => ['controller' => 'record','action' => 'edit'],
'/record/update' => ['controller' => 'record','action' => 'update'],
'/record/delete' => ['controller' => 'record','action' => 'delete'],
];
}
private function getPathInfo()
{
return $_SERVER['REQUEST_URI'];
}
private function render404Page()
{
$this->response->setStatusCode(404,'Not Found');
$this->response->setContent(
<<<EOF
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/vendor/twbs/bootstrap/dist/css/bootstrap.css">
<title>404</title>
</head>
<body>
<h1>
404 Page Not Found
</h1>
</body>
</html>
EOF
);
}
}
Router.phpとResponse.phpを実装していきます。
<?php
class Router
{
//ルーティングの情報
private $routes;
public function __construct($routes)
{
$this->routes = $routes;
}
public function resolve($pathInfo)
{
//パスの一致を判定
foreach($this->routes as $path => $pattern){
if($path === $pathInfo){
return $pattern;
}
}
return false;
}
}
<?php
//目的:サーバからクライアント(ブラウザ)へデータを送信
Class Response
{
protected $content;
protected $statusCode;
protected $statusText;
public function send()
{
//HTTP/1.1 200 OK
header('HTTP/1.1' . $this->statusCode . ' ' . $this->statusText);
echo $this->content;
}
//HTMLデータなど
public function setContent($content)
{
$this->content = $content;
}
//statusCode = 404など,statusText = Page Not Found など
public function setStatusCode($statusCode,$statusText)
{
$this->statusCode = $statusCode;
$this->statusText = $statusText;
}
}
HTTPについては後ほど別記事で解説します。
Application.phpとその周りの実装をしたので、Application.phpを実行します。
実行するファイルはweb/index.phpです。
<?php
require '../bootstrap.php';
require '../Application.php';
$app = new Application();
$app->run();
参考
bootstrap.phpは各種ファイルを自動で読み込む命令が書かれています。
オートローディングという自動でファイルを読み込むための機能を使用しています。
これを使用することで、requireを毎回ファイルごとに書く必要がなくなります。
今回は、/coreと/modelsと/controllerにオートローディングの設定をしています。
Controllerの実装
メインの処理ができたら、コントローラーの処理を実装していきます。
ルーティングではコントローラ名とメソッド名の情報を取得しました。
Controllerの目的
- View
- model
に対してルーティングで受け取ったデータをそれぞれに送受信などを行います。
いきなりLoginContollerクラスを作成せずに、継承を利用してコントローラーの役割を明記していきます。
Controllerクラスを作成します。
Controllerクラスではrunとrenderメソッドを記述します。
runメソッドではルーティングで指定したメソッド実行するための処理をします。renderではViewに対しての処理を行います。
<?php
class Controller
{
protected $actionName;
public function run($action)
{
$this->actionName = $action;
if(!method_exists($this,$action)){
throw new HTTPNotFoundException();
}
$content = $this->$action();
return $content;
}
//継承先で呼び出せるようにprotectedにしています。
//この処理はviewsの中にあるhtmlを表示します。
//$variables=[htmlで使う変数],$template=html名
protected function render($variables = [],$template = null,$layout = 'layout')
{
//ViewクラスについてView編で説明します。
$view = new View(__DIR__ . '/../views');
if(is_null($template)){
$template = $this->actionName;
}
$controllerName = strtolower(substr(get_class($this),0,-10));
$path = $controllerName . '/' . $template;
return $view->render($path,$variables,$layout);
}
}
各種コントローラーを実装します。
func.phpではTokenやhtmlspecialcharsの実装をしています。
ログインや会員登録などの実装の説明は割愛します。
3つのコントローラーの構成は基本的に同じため、まとめて説明します。
コンストラクタでmodelsのロジックを呼び出します。これはそれぞれのコントローラー内のメソッドで使用するためです。
そして、各種コントローラーからControllerクラスで定義したrenderメソッドを呼び出します。
renderメソッドの挙動を解説します。
LoginController.phpのindexメソッドの返り値を例とします。
return render(['csrf_token'=>$csrf_token,'error'=>[]],login_page)
このrenderの引数は2つあります。
1つ目は配列としてHTMLの中で使う変数を定義しています。
例$csrf_token,$error
2つ目は、表示するHTMLページを指定しています。このHTMLはviewsディレクトリに置いてあります。
<?php
require_once '../func.php';
class LoginController extends Controller
{
private $user_logic;
public function __construct()
{
$this->user_logic = new UserLogic();
}
public function index()
{
session_start();
$csrf_token = Token();
$result = $this->user_logic->checkLogin();
if($result) {
return $this->mypage();
}
return $this->render([
'csrf_token' => $csrf_token,
'error' => []
],'login_page');
}
public function login()
{
session_start();
$token = filter_input(INPUT_POST, 'csrf_token');
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
exit('不正なリクエスト');
}
unset($_SESSION['csrf_token']);
$loginData = [
'userid' => h($_POST['userid']),
'password' => h($_POST['pass']),
];
$err = [];
if(!$loginData['userid'] = filter_input(INPUT_POST, 'userid')) {
$err['userid'] = 'ユーザーIDを記入してください。';
}
if(!$loginData['password'] = filter_input(INPUT_POST, 'pass')) {
$err['password'] = 'パスワードを記入してください。';
}
if (count($err) > 0) {
return $this->render([
'error' => $err
],'login_page');
}
// ログイン成功時の処理
$result = $this->user_logic->login($loginData);
// ログイン失敗時の処理
if (!$result) {
$err[] = $_SESSION['msg'];
return $this->render([
'error' => $err
],'login_page');
}else{
return $this->mypage();
}
}
public function logout()
{
session_start();
$result = UserLogic::logout();
if($result){
$_SESSION['msg'] = 'ログアウトしました。';
}else{
$_SESSION['msg'] = 'ログアウトできませんでした。もう一度やり直してください。';
}
$msg = $_SESSION['msg'];
return $this->render([
'title' => 'ログアウト',
'msg' => $_SESSION['msg']
],'logout_form');
}
public function edit()
{
session_start();
$csrf_token = Token();
if(!isset($_SESSION['login_user'])){
$_SESSION['err'] = 'ログインをもう一度やり直してください';
}
$err =[];
$err[] = $_SESSION;
$login_user = $_SESSION['login_user'];
return $this->render([
'title' => '編集',
'login_user' => $login_user,
'csrf_token' => $csrf_token,
'error' => $err
],'edit_users_form');
}
public function update()
{
session_start();
$login_user = $_SESSION['login_user'];
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('不正なリクエストです。');
}
unset($_SESSION['csrf_token']);
$current_userid = $login_user['userid'];
if($_SERVER["REQUEST_METHOD"] == "POST"){
$updateUsers = [
'user_id' => h($_POST['userid']),
'mail' => h($_POST['mail']),
];
//パスワードのバリデーションチェックをしない
$validatePass = false;
$err = $this->user_logic->validateUsers($updateUsers,$validatePass);
$err = $this->user_logic->checkDuplicate($updateUsers);
$err=[];
if (count($err) > 0) {
// エラーがあった場合は戻す
$_SESSION = $err;
header('Location:/mypage');
return;
}else{
$result = $this->user_logic->updateUser($updateUsers,$current_userid);
if ($result == true){
return $this->mypage();
}
}
}
}
public function mypage()
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$result = UserLogic::checkLogin();
if(!$result){
$_SESSION['msg'] = 'ユーザを登録してログインしてください';
return $this->render([
'title' => 'マイページ',
'msg' => $_SESSION['msg'],
'result' => $result
],'mypage_form');
}else{
$login_user = $_SESSION['login_user'];
return $this->render([
'title' => 'マイページ',
'login_user' => $login_user,
'result' => $result
],'mypage_form');
}
}
public function delete()
{
session_start();
$login_user = $_SESSION['login_user'];
$user_id = $login_user['userid'];
$result = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $this->user_logic->deleteUser($user_id);
}
return $this->render([
'title' => '削除',
'login_user' => $login_user,
'result' => $result
],'delete_users_page');
}
}
<?php
class RecordController extends Controller
{
private $recordLogic;
private array $err = [];
public function __construct()
{
$this->recordLogic = new listLogic();
}
public function index()
{
if (session_status() == PHP_SESSION_NONE) {
// セッションは有効で、開始していないとき
session_start();
}
if(!isset($_SESSION['login_user'])){
$_SESSION['msg'] = 'ログインもしくは会員登録をしてください';
header('Location:/');
exit;
}else{
$login_user = $_SESSION['login_user'];
$user_id = $login_user['userid'];
$results = $this->recordLogic->selectTable($user_id);
return $this->render([
'title' => 'トレーニングリスト',
'results' => $results,
],'recordlist');
}
}
public function create()
{
if (session_status() == PHP_SESSION_NONE) {
// セッションは有効で、開始していないとき
session_start();
}
$csrf_token = Token();
if(!isset($_SESSION['login_user'])){
$_SESSION['msg'] = 'ログインもしくは会員登録をしてください';
header('Location:/');
exit;
}else{
return $this->render([
'title' => 'トレーニングレコード',
'error' => $this->err,
'csrf_token' => $csrf_token,
],'record_form');
}
}
public function register()
{
session_start();
$token = filter_input(INPUT_POST, 'csrf_token');
if (!isset($_POST['csrf_token']) || $token !== $_SESSION['csrf_token']) {
// トークンが一致しなかった場合
die('不正なリクエストです。');
}
unset($_SESSION['csrf_token']);
$login_user = $_SESSION['login_user'];
$user_id = $login_user['userid'];
if($_SERVER["REQUEST_METHOD"] == "POST"){
$records = [
'date' => h($_POST['date']),
'name' => h($_POST['name']),
'weight' => h($_POST['weight']),
'count' => h($_POST['rep']),
];
$this->err = $this->recordLogic->validateRecord($records);
if (count($this->err) > 0) {
return $this->render([
'title' => 'トレーニングレコード',
'error' => $this->err
],'record_form');
}
$result = $this->recordLogic->recordTable($records,$user_id);
if ($result == true){
return $this->index();
}else{
$this->err[] ='レコード処理に失敗しました。' ;
return $this->render([
'title' => 'トレーニングレコード',
'error' => $this->err
],'record_form');
}
}
}
public function edit()
{
session_start();
if(!isset($_SESSION['login_user'])){
return $this->create();
}
$csrf_token = Token();
$id = $_POST['id'];
return $this->render([
'title' => '編集',
'id' => $id,
'csrf_token' => $csrf_token,
],'edit_form');
}
public function update()
{
session_start();
$token = filter_input(INPUT_POST, 'csrf_token');
if (!isset($_POST['csrf_token']) || $token !== $_SESSION['csrf_token']) {
// トークンが一致しなかった場合
die('不正なリクエストです。');
}
unset($_SESSION['csrf_token']);
$id = $_POST['id'];
if($_SERVER["REQUEST_METHOD"] == "POST"){
$updateRecords = [
'date' => h($_POST['date']),
'name' => h($_POST['name']),
'weight' => h($_POST['weight']),
'count' => h($_POST['rep']),
];
$this->err = $this->recordLogic->validateRecord($updateRecords);
if (count($this->err) > 0) {
return $this->update();
}else{
$result = $this->recordLogic->updateTable($id,$updateRecords);
}
}
if ($result == true){
return $this->index();
}
}
public function delete()
{
$id = $_POST['id'];
$result = $this->recordLogic->deleteTable($id);
if ($result == true){
return $this->index();
}
}
}
<?php
require_once __DIR__ . '/../func.php';
class SignupController extends Controller
{
private $user_logic;
public function __construct()
{
$this->user_logic = new UserLogic();
}
public function index()
{
session_start();
$csrf_token = Token();
$err =[];
$err [] = $_SESSION;
return $this->render([
'csrf_token' => $csrf_token,
'error' => $err
],'signup_form');
}
public function register()
{
session_start();
$token = filter_input(INPUT_POST, 'csrf_token');
//トークンがない、もしくは一致しない場合、処理を中止
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
exit('不正なリクエスト');
}
unset($_SESSION['csrf_token']);
$usersData = [
'user_id' => h($_POST['userid']),
'mail' => h($_POST['mail']),
'pass' => password_hash(h($_POST['pass']), PASSWORD_DEFAULT),
'confirmpass' => password_hash(h($_POST['confirmpass']), PASSWORD_DEFAULT),
];
// $err = [];
$err = $this->user_logic->validateUsers($usersData);
$result = $this->user_logic->checkDuplicate($usersData);
if($result === false){
$err [] ='このユーザーIDは既に使われております。。もう一度やり直してください。';
}
if(count($err) === 0){
//ユーザを登録する処理
$hasCreated = $this->user_logic->createUser($usersData);
if(!$hasCreated){
$err[] = '登録に失敗しました';
}
}
return $this->render([
'error' => $err
],'signedup');
}
}
以上、Controllerの実装でした。
間違いなどがあれば教えていただけると助かります!
次はModelについて解説します!