0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHP+MySQLでEC機能を実装した話

Posted at

はじめに

この投稿は下記コーディング部分を解説した内容です。
全体的な仕様や経緯はこちらの記事を読んでください。

約2週間で制作したため、安全対策はほぼできてません。
これから改善していこうと思いますのでお手柔らかに🙏
そして、全てのコードを書いてしまうとセキュリティ的に宜しくないので、雑に説明してるところもあります。
ボリュームが多くなるため各ファイルごとの機能要件、画面設計、ユースケース、設計における拡張性やセキュリティ設計は省略。
重要だと思う箇所のみピックアップして記しておきます。
初心者でもわかりやすいようになるべく図解します。
コードの細かい解説を見たい方は「◯◯画面の詳細説明はこちら」を見てください。

この記事で学べること

  • PHPでおこなうCRUD操作
  • 基本的なSQL文
  • 自動返信メール機能の実装
  • Javascriptで実装するバリデーション
  • よく使う機能の関数化と使用方法

画面構成

商品&スタッフ管理ディレクトリ.jpg
ECシステム画面構成.jpg

プロジェクトフォルダの全体像(index.phpは省略)
全体像.jpg

それでは機能別で書いていきます。


管理者ログイン機能


管理者がログインして商品とスタッフアカウントの管理ができる画面です。
推移図はこんな感じ

管理者ログインフォルダ.jpg

フォルダ概要

ページにはログインフォームが含まれており、入力された情報はログインチェックファイルに渡してデータベースの情報と照合。
間違っていればNG画面へ、合っていれば管理画面TOPへ
ログアウトファイルはheader.phpでリンクさせている。

login画面

スクリーンショット 2024-12-07 15.33.44.png

login.php(body直下)
  <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-sm">
      <?php include '../logo.php'; ?>
      <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login</h2>
    </div>

    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
      <form class="space-y-6" action="staff_login_check.php" method="post">
        <div>
          <label for="staff_name" class="block text-sm/6 font-medium text-gray-900">Staff name</label>
          <div class="mt-2">
            <input id="name" name="staff_name" type="text" autocomplete="username" required class="block w-full rounded-md border-0 py-1.5 px-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
          </div>
        </div>

        <div>
          <div class="flex items-center justify-between">
            <label for="password" class="block text-sm/6 font-medium text-gray-900">Pass word</label>
          </div>
          <div class="mt-2">
            <input id="password" name="password" type="password" autocomplete="password" required class="block w-full rounded-md border-0 py-1.5 px-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
          </div>
        </div>

        <div class="flex w-full justify-between">
          <input type="submit" value="Login" class="w-full rounded-md bg-black px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
        </div>
      </form>

    </div>
  </div>
login画面の詳細説明はこちら

1)フォームの構造とバリデーション

  • action 属性で送信先を指定: staff_login_check.php
  • method="post" を使用してセキュリティを向上
  • ユーザーが空欄で送信した場合、ブラウザによるバリデーションを実行

2 )外部PHPの読み込み

  • logo.php: ロゴを表示
  • 再利用性を高めるために共通コンポーネント化
  • footer.php: フッターを表示
  • 全ページで共通化されたフッターデザインを適用

login check画面

ログインフォームのデータを受け取り、データベース内の認証情報と照合する処理を実装しています。

login_check.php
<?php

  try {
    require_once ('../common/common.php');

    $post=sanitize($_POST);
    $staff_name=$post['staff_name'];
    $staff_pass=$post['password'];

    $staff_pass=md5($staff_pass);

    $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
    $user='root';
    $pass='';
    $dbh=new PDO($dsn,$user,$pass);
    $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

    $sql='SELECT name FROM mst_staff WHERE name=? AND password=?';
    $stmt=$dbh->prepare($sql);
    $data[]=$staff_name;
    $data[]=$staff_pass;
    $stmt->execute($data);

    $dbh=null;

    $rec=$stmt->fetch(PDO::FETCH_ASSOC);

    if($rec==false) {
      header('Location:staff_login_ng.php');
      exit();
    } else {
      session_start();
      $_SESSION['login']=1;
      $_SESSION['staff_code']=$staff_code;
      $_SESSION['staff_name']=$rec['name'];
      header('Location:staff_top.php');
      exit();
    }
  } catch(Exception $e) {
    print'<p class="text-center my-3">ただいま障害により<br/>大変ご迷惑をお掛けしております。</p>';
    print'<a href="staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>';
    $error_message = $e->getMessage();
    echo "<script>console.error('PHP Error: " . addslashes($error_message) . "');</script>";
    exit();
  }
?>
login check画面の詳細説明はこちら

1)サニタイジング関数のインクルード

login_check.php
require_once ('../common/common.php');

入力フォーム内容をチェックして無害化するために、サニタイズ関数をグローバル関数として作成。
それをインクルードしています。(以下省略します)


2 )POSTデータの受け取りとサニタイズ

login_check.php
$post = sanitize($_POST); // インクルードした関数
$staff_name = $post['staff_name'];
$staff_pass = $post['password'];

common.phpの内容はこのようになっている(以下省略)

common.php
function sanitize($before) {
  foreach($before as $key=>$val) {
    $after[$key]=htmlspecialchars($val,ENT_QUOTES,'UTF-8');
  }
  return $after;
}

3 )パスワードのハッシュ化

login_check.php
$staff_pass = md5($staff_pass);

パスワードをハッシュ化して、データベース内での安全性を確保。
md5 は現在セキュリティが弱いため、将来的には password_hash に移行します。


4 )データベース接続

login_check.php
$dsn = 'mysql:dbname=testshop;host=localhost;charset=utf8';
$user = 'root';
$pass = '';
$dbh = new PDO($dsn, $user, $pass);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

データベースへの接続を確立。PDOを使用して安全な接続とエラーハンドリングを実現。


5 )SQL文の準備と実行

login_check.php
$sql = 'SELECT name FROM mst_staff WHERE name=? AND password=?';
$stmt = $dbh->prepare($sql);
$data[] = $staff_name;
$data[] = $staff_pass;
$stmt->execute($data);

プレースホルダー (?) を使ったSQL文で、SQLインジェクションを防止。
prepare と execute を使い、安全にパラメータをバインド。


6 )結果の取得とログイン認証

login_check.php
$rec = $stmt->fetch(PDO::FETCH_ASSOC);

クエリの結果を連想配列として取得。
スタッフの名前とパスワードが一致して、データが存在する場合はログイン成功。


7 )認証結果による処理

  1. ログイン失敗時

    login_check.php
    if ($rec == false) {
      header('Location:staff_login_ng.php');
      exit();
    }
    

    認証に失敗した場合、エラーページ (staff_login_ng.php) にリダイレクト。
    処理を中断するために exit() を使用。

  2. ログイン成功時

    login_check.php
    session_start();
    $_SESSION['login'] = 1;
    $_SESSION['staff_code'] = $staff_code;
    $_SESSION['staff_name'] = $rec['name'];
    header('Location:staff_top.php');
    exit();
    

    セッションを開始して、ログイン状態 ($_SESSION['login'] = 1) を記録。(ログインしているかのフラグ用)
    スタッフの名前とコードをセッションに保存。
    ログイン成功後、トップページ (staff_top.php) にリダイレクト。


8 )エラーハンドリング

login_check.php
catch (Exception $e) {
  print '<p class="text-center my-3">ただいま障害により<br/>大変ご迷惑をお掛けしております。</p>';
  print '<a href="staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>';
  $error_message = $e->getMessage();
  echo "<script>console.error('PHP Error: " . addslashes($error_message) . "');</script>";
  exit();
}

例外発生時にエラーメッセージをユーザーにわかりやすく表示。
念のためにエラーメッセージをブラウザのコンソールにログ出力。

login NG画面

スタッフログインページで認証失敗時に表示されるエラーメッセージ画面です。
スクリーンショット 2024-12-11 19.44.22.png

login_check.php
<?php include '../header.php'; ?>
    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <?php
          include '../logo.php';
          print '<p class="text-center my-3">スタッフコードまたは<br/>パスワードが間違っています</p>';
        ?>
        <a href="staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>
      </div>
    </div>
<?php include '../footer.php'; ?>
login NG画面の詳細説明はこちら

1)ページ全体に共通するヘッダーとフッターを読み込み。

login_check.php
<?php include '../header.php'; ?>
・
・
<?php include '../footer.php'; ?>

2)ページ中央にロゴとエラーメッセージとログイン画面に戻るためのボタン表示。

login_check.php
        <?php
          include '../logo.php';
          print '<p class="text-center my-3">スタッフコードまたは<br/>パスワードが間違っています</p>';
        ?>
        <a href="staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>

login後のTOP画面

TOP画面はこのようになっています。
売上や新規登録などの数値は固定値でいれています。なんのデータとも連動していません。(いつかやりたい)

スクリーンショット 2024-12-11 19.51.46.png

staff_top.php
<?php include '../header.php'; ?>

  <!-- 全体レイアウト -->
  <div class="flex h-screen">
    <!-- サイドメニュー -->
    <aside class="w-64 bg-gray-700 text-white">
      <div class="px-4 py-6">
        <h1 class="text-2xl font-bold">管理者画面</h1>
      </div>
      <nav class="mt-6">
        <a href="../staff/staff_list.php" class="block px-4 py-2 text-gray-300 hover:bg-gray-500 rounded">スタッフ管理</a>
        <a href="../product/pro_list.php" class="block px-4 py-2 text-gray-300 hover:bg-gray-500 rounded">商品管理</a>
      </nav>
    </aside>

    <!-- メインコンテンツ -->
    <main class="flex-1 p-6">
      <!-- ヘッダー -->
      <header class="flex justify-between items-center bg-white shadow-md p-4 rounded-md">
        <h2 class="text-xl font-semibold">ダッシュボード</h2>
        <div class="flex items-center">
          <p class="mr-4">こんにちは <?= $_SESSION['staff_name']; ?> さん</p>
          <button type="button" onclick="location.href='./staff_logout.php'" class="px-4 py-2 bg-gray-900 text-white rounded hover:bg-gray-500">ログアウト</button>
        </div>
      </header>

      <!-- ダッシュボードの内容 -->
      <section class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
        <!-- カード1: スタッフ数 -->
        <div class="bg-white shadow-md rounded-lg p-6">
          <h3 class="text-lg font-medium">スタッフ数</h3>
          <p class="mt-2 text-2xl font-bold text-blue-600">20人</p>
        </div>
        <!-- 以下省略 -->
      </section>
    </main>
  </div>
<?php include '../footer.php'; ?>
login TOP画面の詳細説明はこちら

1)セッションに保存したユーザー名を表示

login_check.php
<p class="mr-4">こんにちは <?= $_SESSION['staff_name']; ?> さん</p>

補足)インクルードしているheader.phpでセッションに接続しています。
未ログイン時はログイン画面へ誘導しています。

header.php(セッション接続箇所)
  session_start();
  session_regenerate_id(true);
  if(isset($_SESSION['login'])==false) {
    print'<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">';
    print'<div class="sm:mx-auto sm:w-full sm:max-w-sm">';
    print '<p class="text-center mb-3">ログインしてください</p>';
    print'<a href="./staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">ログイン画面へ</a>';
    print'</div>';
    print'</div>';
    exit();
  }

logout画面

TOP画面にあるログアウトボタンを押したときの画面
スクリーンショット 2024-12-11 20.07.54.png

logout.php
<?php

  session_start();
  $_SESSION=array();
  if(isset($_COOKIE[session_name()])==true) {
    setcookie(session_name(),'',time()-42000,'/');
  }
  session_destroy();

?>

<!DOCTYPE html>
<html class="h-full bg-white">
  <head>
    <!-- 省略 -->
  </head>
  <body class="h-full">
    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <p class="text-center mb-3">ログアウトしました</p>
        <a href="./staff_login.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">ログイン画面へ</a>
      </div>
    </div>
  </body>
</html>
logout画面の詳細説明はこちら

1)セッションデータのクリア

logout.php
$_SESSION = array();

現在のセッション変数に保存されているすべてのデータをクリアする。


2)クッキーの削除
未ログイン時はログイン画面へ誘導しています。

logout.php
if (isset($_COOKIE[session_name()]) == true) {
  setcookie(session_name(), '', time() - 42000, '/');
}

セッションIDを保持しているクッキーを削除する。
session_name() は現在のセッション名を取得する関数で、そのクッキーを削除するために使用。
time() - 42000 は有効期限を過去に設定することでクッキーを無効化。


3)セッションの破棄
未ログイン時はログイン画面へ誘導しています。

logout.php
session_destroy();

サーバー上のセッションデータを削除し、セッションの再利用を防ぐ。


スタッフ一覧表示・追加・編集・削除


スタッフアカウントの追加と編集、削除ができる画面です。
推移図はこのようになっています。

スタッフ管理画面.jpg

フォルダ概要

管理者ログイン画面からTOPへ推移し、メニューボタンにある「スタッフ管理」ボタンを押すことでアクセスできる。
アクセスするとスタッフ一覧画面が表示され、参照(詳細表示)、追加、修正、削除ボタンを押すことで各操作が可能になっている。

スタッフ一覧画面

スタッフ管理で一番最初に表示される画面
スクリーンショット 2024-12-12 17.56.35.png

staff_list.php
<?php include '../header.php'; ?>
  <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-sm">
      <?php 
        try {
          $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
          $user='root';
          $pass='';
          $dbh=new PDO($dsn,$user,$pass);
          $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
          $sql='SELECT code,name FROM mst_staff WHERE 1';
          $stmt=$dbh->prepare($sql);
          $stmt->execute();
          $dbh=null;
          print'<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">staff list</h2>';
          print'<form method="post" action="staff_branch.php">';
          print '<div class="space-y-2 my-2">';
          while(true) {
            $rec=$stmt->fetch(PDO::FETCH_ASSOC);
            if($rec==false){
              break;
            }
            print '<div class="flex items-center space-x-3 my-2">';
            print '<input id="staff'.$rec['code'].'" type="radio" name="staffcode" value="'.$rec['code'].'" class="peer" />';
            print '<label for="staff'.$rec['code'].'" class="peer-checked:text-sky-500 cursor-pointer">'.$rec['name'].'</label>';
            print '</div>';
          }
          print '</div>';
          print '<div class="flex justify-between pt-4">';
          print'<input type="submit" value="参照" name="disp" class="w-fit px-6 rounded-md bg-sky-500 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-100 hover:text-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-100">';
          print'<input type="submit" value="追加" name="add" class="w-fit px-6 rounded-md bg-indigo-500 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-100 hover:text-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-100">';
          print'<input type="submit" value="修正" name="edit" class="w-fit px-6 rounded-md bg-black py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">';
          print'<input type="submit" value="削除" name="delete" class="w-fit px-6 rounded-md bg-gray-200 py-1.5 text-sm/6 font-semibold text-black shadow-sm hover:bg-slate-500 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">';
          print '</div>';
          print'</from>';
          print'<a href="../staff_login/staff_top.php" class="mt-5 flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">トップへ戻る</a>';
        }
        catch(Exception $e) {
          print'ただいま障害により大変ご迷惑をお掛けしております。';
          exit();
        }
      ?>
    </div>
  </div>

<?php include '../footer.php'; ?>
スタッフ一覧表示画面の詳細説明はこちら

1)データベース接続とスタッフ一覧の取得

staff_list.php
$dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
$user='root';
$pass='';
$dbh=new PDO($dsn,$user,$pass);
$dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$sql='SELECT code,name FROM mst_staff WHERE 1';
$stmt=$dbh->prepare($sql);
$stmt->execute();
$dbh=null;

テーブル名を変更して接続。WHERE 1 は全てのレコードを取得する条件として使用。
PDO::ATTR_ERRMODE を PDO::ERRMODE_EXCEPTION に設定することで、データベースエラー時に例外をスローし、適切なエラーハンドリングが可能です。


2)スタッフ一覧の表示

staff_list.php
while(true) {
  $rec=$stmt->fetch(PDO::FETCH_ASSOC);
  if($rec==false){
    break;
  }
  print '<div class="flex items-center space-x-3 my-2">';
  print '<input id="staff'.$rec['code'].'" type="radio" name="staffcode" value="'.$rec['code'].'" class="peer" />';
  print '<label for="staff'.$rec['code'].'" class="peer-checked:text-sky-500 cursor-pointer">'.$rec['name'].'</label>';
  print '</div>';
}

取得したスタッフのリストをループで生成し、各スタッフを選択可能なラジオボタンとして表示。
fetch(PDO::FETCH_ASSOC)の箇所はクエリ結果を1行ずつ連想配列として取得。取得結果がなくなると false を返すため、ループを終了させています。


3)フォーム内の操作ボタン

staff_list.php
print'<input type="submit" value="参照" name="disp" ...>';
print'<input type="submit" value="追加" name="add" ...>';
print'<input type="submit" value="修正" name="edit" ...>';
print'<input type="submit" value="削除" name="delete" ...>';

各ボタンはそれぞれの操作を表し、フォームの action 属性で指定されたスクリプト (staff_branch.php) に送信される。
ボタンには name 属性でユニークな値(disp, add, edit, delete)が設定され、送信先での操作判定に使用されます。

branch画面

スタッフリスト画面から受け取った値を元に設定されたページへ推移させる機能です。

branch.php
<?php
  session_start();
  session_regenerate_id(true);
  if(isset($_SESSION['login'])==false) {
    print '<p class="text-center mb-3">ログインしてください</p>';
    print'<a href="./staff_login.php">ログイン画面へ</a>';
    exit();
  };
  if(isset($_POST['disp'])==true){
    if(isset($_POST['staffcode'])==false) {
      header('Location:staff_ng.php');
      exit();
    }
    $staff_code=$_POST['staffcode'];
    header('Location:staff_disp.php?staffcode='.$staff_code);
    exit();
  };
  if(isset($_POST['add'])==true) {
    header('Location:staff_add.php');
    exit();
  }
  if(isset($_POST['edit'])==true){
    // print'修正がおされました';
    if(isset($_POST['staffcode'])==false) {
      header('Location:staff_ng.php');
      exit();
    }
    $staff_code=$_POST['staffcode'];
    header('Location:staff_edit.php?staffcode='.$staff_code);
    exit();
  };
  if(isset($_POST['delete'])==true){
    // print'削除がおされました';
    if(isset($_POST['staffcode'])==false) {
      header('Location:staff_ng.php');
      exit();
    }
    $staff_code=$_POST['staffcode'];
    header('Location:staff_delete.php?staffcode='.$staff_code);
    exit();
  };
?>
branch画面の詳細説明はこちら

1)セッションの安全性向上

branch.php
session_start();
session_regenerate_id(true);

session_regenerate_id(true) を使用して、既存のセッションIDを新しいものに置き換える。(セッション固定化攻撃を防ぐため)
true オプションにより、古いセッションデータを削除し安全性を向上。


2)ログイン状態の確認

branch.php
if(isset($_SESSION['login'])==false) {
  print '<p class="text-center mb-3">ログインしてください</p>';
  print'<a href="./staff_login.php">ログイン画面へ</a>';
  exit();
};

ログインしていない場合は処理を中断し、ログイン画面へ誘導。
$_SESSION['login'] が未設定の場合にエラーメッセージを表示。


3)各操作の処理分岐
参照

branch.php
        print'<input type="submit" value="参照" name="disp" ...>';
        print'<input type="submit" value="追加" name="add" ...>';
        print'<input type="submit" value="修正" name="edit" ...>';
        print'<input type="submit" value="削除" name="delete" ...>';

「参照」ボタンが押された場合に、選択されたスタッフの詳細画面へ遷移。

追加

branch.php
    if(isset($_POST['add'])==true) {
      header('Location:staff_add.php');
      exit();
    }

「追加」ボタンが押された場合に、新規スタッフ追加画面 (add.php) へ遷移。

修正

branch.php
    if(isset($_POST['edit'])==true){
      if(isset($_POST['staffcode'])==false) {
        header('Location:staff_ng.php');
        exit();
      }
      $staff_code=$_POST['staffcode'];
      header('Location:staff_edit.php?staffcode='.$staff_code);
      exit();
    }

「修正」ボタンが押された場合に、選択されたスタッフの編集画面へ遷移。
$_POST['staffcode'] が未設定であればエラー画面 (staff_ng.php) へ遷移する。
スタッフコードをクエリパラメータとして付与し、staff_edit.php へリダイレクト。

削除

branch.php
    if(isset($_POST['delete'])==true){
      if(isset($_POST['staffcode'])==false) {
        header('Location:staff_ng.php');
        exit();
      }
      $staff_code=$_POST['staffcode'];
      header('Location:staff_delete.php?staffcode='.$staff_code);
      exit();
    }

「削除」ボタンが押された場合に、選択されたスタッフの削除確認画面へ遷移。

staff_ng画面

スタッフコードが必要な操作のとき、スタッフ名の選択がなかった場合のエラーメッセージ

staff_ng.php
<?php include '../header.php'; ?>
    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <?php 
            print '<p class="text-center mb-3">スタッフが選択されていません。</p>';
        ?>
        <a href="staff_list.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>
      </div>
    </div>
<?php include '../footer.php'; ?>

disp(スタッフ詳細表示)画面

スタッフ名を選択して詳細を確認する画面

スクリーンショット 2024-12-12 20.00.22.png

disp.php
<?php include '../header.php'; ?>

<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
  <div class="sm:mx-auto sm:w-full sm:max-w-sm">
    <?php 
    try {
      error_reporting(E_ALL);
      ini_set('display_errors', 1);
      $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
      $user='root';
      $pass='';
      $dbh=new PDO($dsn,$user,$pass);
      $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
      $staff_code=$_GET['staffcode'];

      $sql='SELECT name FROM mst_staff WHERE code=?';
      $stmt=$dbh->prepare($sql);
      $data[]=$staff_code;
      $stmt->execute($data);
      $rec=$stmt->fetch(PDO::FETCH_ASSOC);
      $staff_name=$rec['name'];
      $dbh=null;
    }
    catch(Exception $e) {
      print'ただいま障害により大変ご迷惑をお掛けしております。';
      exit();
    }
    ?>
    <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Staff data</h2>
  </div>
  <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
    <div class="flex justify-center">
      <p class="pr-2">Staff code:</p>
      <?php print $staff_code; ?>
    </div>
    <div class="flex justify-center">
      <p class="pr-2">Staff name:</p>
      <?php print $staff_name; ?>
    </div>
    <div class="flex w-full justify-between pt-5">
      <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
    </div>
  </div>
</div>

<?php include '../footer.php'; ?>

データベースから情報を取得して表示させているだけです。

add(スタッフ追加)画面

スタッフをデータベースに追加できる画面です
スクリーンショット 2024-12-12 20.04.05.png

add.php
<?php include '../header.php'; ?>

  <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-sm">
      <?php include '../logo.php'; ?>
      <!-- <img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="石田商店"> -->
      <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">add your account</h2>
    </div>

    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
      <form class="space-y-6" action="staff_add_check.php" method="POST" onsubmit="return validatePasswords()">
        <div>
          <label for="staff_name" class="block text-sm/6 font-medium text-gray-900">Staff name</label>
          <div class="mt-2">
            <input id="name" name="staff_name" type="text" autocomplete="username" required class="block w-full rounded-md border-0 py-1.5 px-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
          </div>
        </div>

        <div>
          <div class="flex items-center justify-between">
            <label for="password" class="block text-sm/6 font-medium text-gray-900">Pass word</label>
          </div>
          <div class="mt-2">
            <input id="password" name="password" type="password" autocomplete="new-password" required class="block w-full rounded-md border-0 py-1.5 px-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
          </div>
        </div>

        <div>
          <div>
            <label for="password2" class="block text-sm/6 font-medium text-gray-900">Please enter your password again</label>
            <label id="error-message" for="password2" class="block text-xs font-medium text-red-900"></label>
          </div>
          <div class="mt-2">
            <input id="password2" name="password2" type="password" autocomplete="new-password" required class="block w-full rounded-md border-0 py-1.5 px-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
          </div>
        </div>

        <div class="flex w-full justify-between">
          <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
          <input type="submit" value="Next" class="w-14 rounded-md bg-black px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
        </div>
      </form>

    </div>
  </div>
  <script>
    function validatePasswords() {
      const password = document.getElementById('password').value;
      const password2 = document.getElementById('password2').value;
      const errorMessage = document.getElementById('error-message');
      
      if (password !== password2) {
        errorMessage.textContent = 'パスワードが一致しません。もう一度入力してください。';
        return false;
      }
      
      errorMessage.textContent = '';
      return true;
    }
  </script>
<?php include '../footer.php'; ?>
add画面の詳細説明はこちら

1)入力フォーム
スタッフ名、パスワード、確認用パスワードをフォームで設置してます。
それぞれidとnameを設定し、POSTデータで識別可能にしています。


2)JavaScriptによるパスワード一致確認

add.php
function validatePasswords() {
  const password = document.getElementById('password').value;
  const password2 = document.getElementById('password2').value;
  const errorMessage = document.getElementById('error-message');
  
  if (password !== password2) {
    errorMessage.textContent = 'パスワードが一致しません。もう一度入力してください。';
    return false;
  }
  
  errorMessage.textContent = '';
  return true;
}

フォーム送信時に、パスワードと再入力が一致しているかを検証。
一致しない場合に送信を中止し、エラーメッセージを表示。

add_check画面

POSTデータから値を取得して表示する画面
スクリーンショット 2024-12-12 20.22.06.png

add.php
<?php include '../header.php'; ?>

    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <?php include 'logo.php'; ?>
        <!-- <img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="石田商店"> -->
        <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">add your account</h2>
      </div>

      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
        <?php 
        require_once ('../common/common.php');

        $post=sanitize($_POST);
        $staff_name=$post['staff_name'];
        $staff_pass=$post['password'];

        $staff_pass=md5($staff_pass); // 暗号化
        ?>
        <form class="space-y-6" action="staff_add_done.php" method="POST">
          <?php if(!empty($staff_name)&&!empty($staff_pass)) : ?>
            <div>
              <label for="staff_name" class="block text-sm/6 font-medium text-gray-900">Staff name</label>
              <div class="mt-2">
                <input id="name" name="staff_name" type="hidden" value="<?= $staff_name; ?>" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
                <p><?= $staff_name; ?></p>
              </div>
            </div>

            <div>
              <div class="flex items-center justify-between">
                <label for="password" class="block text-sm/6 font-medium text-gray-900">Pass word</label>
              </div>
              <div class="mt-2">
                <input id="password" name="password" type="hidden" value="<?= $staff_pass; ?>" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
                <p>入力したパスワード</p>
              </div>
            </div>

            <div class="flex w-full justify-between">
              <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
              <input type="submit" value="Register" class="w-20 rounded-md bg-black px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
            </div>

            <?php else :?>
              <p>エラーが発生しました。前の画面からやり直してください。</p>
              <div>
                <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
              </div>

          <?php endif; ?>
        </form>
      </div>
    </div>

<?php include '../footer.php'; ?>

add_done画面

追加完了画面
スクリーンショット 2024-12-12 20.22.40.png

add_done.php
<?php include '../header.php'; ?>
    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <?php 
          try {
            require_once ('../common/common.php');

            $post=sanitize($_POST);
            $staff_name=$post['staff_name'];
            $staff_pass=$post['password'];

            $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
            $user='root';
            $pass='';
            $dbh=new PDO($dsn,$user,$pass);
            $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
    
            $sql='INSERT INTO mst_staff(name,password) VALUES(?,?)';
            $stmt=$dbh->prepare($sql);
            $data[]=$staff_name;
            $data[]=$staff_pass;
            $stmt->execute($data);
    
            $dbh=null;
    
            // HTMLを挿入する
            print '<p class="text-center mb-3">"'.$staff_name.'"さんを登録しました。</p>';

          }
          catch(Exception $e) {
            print '<p class="text-center mb-3">ただいま障害により<br/>大変ご迷惑をお掛けしております。</p>';
            print '<a href="staff_add.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>';
            $error_message = $e->getMessage();
            echo "<script>console.error('PHP Error: " . addslashes($error_message) . "');</script>";
            exit();
          }
        ?>
        <a href="staff_list.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>
      </div>
    </div>
<?php include '../footer.php'; ?>

edit(スタッフ修正)画面

スタッフ一覧でスタッフ名を選択して、データベースにある情報を変更できます。
スクリーンショット 2024-12-12 20.26.07.png

edit.php
<?php include '../header.php'; ?>

<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
  <div class="sm:mx-auto sm:w-full sm:max-w-sm">
    <?php 
    try {
      error_reporting(E_ALL);
      ini_set('display_errors', 1);
      $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
      $user='root';
      $pass='';
      $dbh=new PDO($dsn,$user,$pass);
      $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
      $staff_code=$_GET['staffcode'];

      $sql='SELECT name FROM mst_staff WHERE code=?';
      $stmt=$dbh->prepare($sql);
      $data[]=$staff_code;
      $stmt->execute($data);
      $rec=$stmt->fetch(PDO::FETCH_ASSOC);
      $staff_name=$rec['name'];
      $dbh=null;
    }
    catch(Exception $e) {
      print'ただいま障害により大変ご迷惑をお掛けしております。';
      exit();
    }
    ?>
    <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit staff</h2>
  </div>
  <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
    <div class="flex justify-center">
      <p class="pr-2">Staff code:</p>
      <?php print $staff_code; ?>
    </div>
    <form action="staff_edit_check.php" method="post" class="flex flex-col">
      <input type="hidden" name="code" value="<?php print $staff_code; ?>" class="w-auto ">
      <label for="name" class="px-3">Staff name</label>
      <input type="text" name="name" value="<?php print $staff_name; ?>" required class="border border-gray-300 border-2 rounded w-auto px-3 py-2 outline-gray-700">
      <label for="password" class="px-3 pt-2">Password</label>
      <input type="password" name="password" required class="border border-gray-300 border-2 rounded w-auto px-3 py-2 outline-gray-700">
      <label for="password2" class="px-3 pt-2">One more agein password</label>
      <input type="password" name="password2" required class="border border-gray-300 border-2 rounded w-auto px-3 py-2 outline-gray-700">
      <div class="flex w-full justify-between pt-5">
        <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
        <input type="submit" value="Edit" class="w-14 rounded-md bg-black px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
      </div>
    </form>
  </div>
</div>

<?php include '../footer.php'; ?>

他の画面と同じくDBから取得して表示させています。
パスワードも入力しなければならないところが仕様センスないので、いつか改造したい。

edit_scheckとedit_done画面

こちらもadd(追加)画面と同じ構造で作成しているため割愛。
edit_done画面はSQLの書き方が違うだけです。
$sql='UPDATE mst_staff SET name=?,password=? WHERE code=?';

delete(削除)画面

スタッフアカウントを削除できます。
スクリーンショット 2024-12-12 20.40.46.png
パスワード認証が必要なのかと思いきや、ただ入力フォームがあるだけの無意味な箱です(笑)
実装期間的に次に進んだので断念しました。
ここも改善したい点です。

delete.php
<?php include '../header.php'; ?>

<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
  <div class="sm:mx-auto sm:w-full sm:max-w-sm">
    <?php 
    try {
      error_reporting(E_ALL);
      ini_set('display_errors', 1);
      $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
      $user='root';
      $pass='';
      $dbh=new PDO($dsn,$user,$pass);
      $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
      $staff_code=$_GET['staffcode'];

      $sql='SELECT name FROM mst_staff WHERE code=?';
      $stmt=$dbh->prepare($sql);
      $data[]=$staff_code;
      $stmt->execute($data);
      $rec=$stmt->fetch(PDO::FETCH_ASSOC);
      $staff_name=$rec['name'];
      $dbh=null;
    }
    catch(Exception $e) {
      print'ただいま障害により大変ご迷惑をお掛けしております。';
      exit();
    }
    ?>
    <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Delete staff</h2>
  </div>
  <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
    <div class="flex justify-center">
      <p class="pr-2">Staff code:</p>
      <?php print $staff_code; ?>
    </div>
    <div class="flex justify-center">
      <p class="pr-2">Staff name:</p>
      <?php print $staff_name; ?>
    </div>
    <p class="text-center">パスワードを入力してください</p><br/>
    <form action="staff_delete_done.php" method="post" class="flex flex-col">
      <input type="hidden" name="code" value="<?php print $staff_code; ?>" class="w-auto ">
      <input type="hidden" name="staff_name" value="<?php print $staff_name; ?>" class="w-auto ">
      <label for="password" class="px-3 pt-2">Password</label>
      <input type="password" name="password" required class="border border-gray-300 border-2 rounded w-auto px-3 py-2 outline-gray-700">
      <label for="password2" class="px-3 pt-2">One more agein password</label>
      <input type="password" name="password2" required class="border border-gray-300 border-2 rounded w-auto px-3 py-2 outline-gray-700">
      <div class="flex w-full justify-between pt-5">
        <button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">Back</button>
        <input type="submit" value="Delete" class="w-17 rounded-md bg-black px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
      </div>
    </form>
  </div>
</div>

<?php include '../footer.php'; ?>

delete_done画面

こちらもaddやedit画面と同様、SQLの書き方が違うだけです。
画面構成は同じなので割愛します。
$sql='DELETE FROM mst_staff WHERE code=?';


商品一覧表示・追加・編集・削除


商品を管理する画面です。
内容や構造はスタッフ管理システムと同じですので、SQLやDB名を変えて実装しています。
コードはほとんど同じなので表示画面のみ紹介しておきます。

推移図
商品管理機能.jpg

フォルダ概要

管理者ログイン画面からTOPへ推移し、メニューボタンにある「商品管理」ボタンを押すことでアクセスできる。
アクセスすると商品一覧が表示され、参照(詳細表示)、追加、修正、削除ボタンを押すことで各操作が可能になっている。

product_list(商品一覧)画面

スクリーンショット 2024-12-13 19.13.30.png

product_branchとpro_ng

スタッフ機能のbranchとng画面と同内容

pro_disp(商品参照)画面

スクリーンショット 2024-12-13 19.15.01.png

pro_add(商品追加)画面

スタッフ追加時とは違いファイル(画像)が追加できるようになっています。
スクリーンショット 2024-12-13 19.19.38.png

pro_add_check画面

特記すべきは画像の読み込み部分、move_uploaded_file()を使ってフォルダに格納しています。
格納した画像ファイルを呼び出して使用しています。
スクリーンショット 2024-12-13 19.22.44.png

pro_add_done画面

スクリーンショット 2024-12-13 19.25.22.png

pro_edit(商品編集)画面

スクリーンショット 2024-12-13 19.26.57.png

pro_edit_check画面

スクリーンショット 2024-12-13 19.28.44.png

pro_edit_done画面

スタッフ管理で作成した同機能のコードをコピペして作成しています。
スクリーンショット 2024-12-13 19.29.38.png

pro_delete(商品削除)画面

スクリーンショット 2024-12-13 19.31.53.png

pro_delete_done画面

スクリーンショット 2024-12-13 19.32.58.png

商品管理はさくさく作業できました。
次はいよいよショップサイトの構成です。


カート、購入手続き、お問い合わせ、会員登録機能


ECサイトに必要な機能を1つのフォルダにまとめちゃいました。
もっと大規模なサイトだと機能ごとに分けるべきですが、あくまでフルスタック開発を学ぶためなので悪しからず。
ECシステム画面構成.jpg

フォルダ概要

トップは商品一覧画面、商品の詳細を見ることでカートに入れられる。

shop_list画面

商品一覧画面、ECサイトのホーム画面でもある。
我ながら画面デザインは気に入っている。
スクリーンショット 2025-01-02 21.38.43.png

shop_list.php
<main class="relative container mx-auto px-6 py-10 xl:w-full">
  <div class="flex flex-col xl:flex-row gap-6">
    <div>
      <h2 class="text-3xl font_primary mb-8">チャフロ茶</h2>
      <div class="bg-white rounded-lg shadow-md overflow-hidden">
        <div class="w-full">
          <img src="../img/mv-chafuro.png" alt="チャフロ茶'" class="w-full object-contain sm:object-cover">
        </div>
        <div class="p-4">
          <p class="text-lg font_primary">
            チャフロ茶は、特許取得の焙煎技法で引き出したポリフェノール「チャフロサイド」が豊富なお茶。1つ1つ手作業で丁寧に作られ、内側からゆっくりと健やかな身体へ導きます。
          </p>
        </div>
      </div>
    </div>
    <div>
      <h2 class="text-3xl font_primary mb-8">茶葉一覧</h2>
      <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-3 gap-6">
        <?php 
          try {
            $dsn='mysql:dbname=testshop;host=localhost;charset=utf8';
            $user='root';
            $pass='';
            $dbh=new PDO($dsn,$user,$pass);
            $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

            $sql='SELECT code,name,price,gazou,product_tagline FROM mst_product WHERE 1';
            $stmt=$dbh->prepare($sql);
            $stmt->execute();
            $dbh=null;

            while(true) {
              $rec=$stmt->fetch(PDO::FETCH_ASSOC);
              if($rec==false){
                break;
              }
              print '<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">';
              print '<img src="../product/gazou/'.$rec['gazou'].'" alt="'.$rec['name'].'" class="w-full h-50 object-contain sm:object-cover">';
              print '<div class="p-4">';
              print '<h3 class="text-lg font_primary">#'.$rec['code'].'&nbsp;'.$rec['name'].'</h3>';
              print '<p class="text-sm text-gray-400 mt-1">'.$rec['product_tagline'].'</p>';
              print '<p class="text-xl font-bold text-gray-400 mt-2">'.number_format($rec['price']).'円</p>';
              print '<a href="shop_product.php?procode='.$rec['code'].'" class="block w-full mt-4 back_ground_kinari font_accent py-2 text-center rounded-md hover_color_kin hover:text-white transition">購入する</a>';
              print '</div>';
              print '</div>';
            }
          }
          catch(Exception $e) {
            print'ただいま障害により大変ご迷惑をお掛けしております。';
            exit();
          }
        ?>
      </div>
    </div>
  </div>
</main>

コードはこれまでと同じく、データベースから取得して表示しているだけです。
タグごとprintしているところがナンセンスだったなと反省。

product(商品詳細)画面

「購入する」ボタンをクリックした飛び先です。
商品の詳細画面に推移します。

スクリーンショット 2025-01-02 22.51.49.png
こちらの画面もDBから取得した内容を変数に入れて表示させています。
数量の箇所はFormタグを使用して、次の画面にGETで数量を取得できるようにしています。

cart_in画面

「カートに追加する」ボタンを押した時のphpです。
画面では追加したむねの文字しか表示されません。
スクリーンショット 2025-01-02 22.56.42.png

cart_in.php
<?php 
  try {
    $pro_code = $_GET['procode'];
    $quantity = intval($_GET['quantity']);

    // カート初期化
    if (!isset($_SESSION['cart']) || !is_array($_SESSION['cart'])) {
      $_SESSION['cart'] = [];
    }

    // カートの内容を統一
    $found = false;
    foreach ($_SESSION['cart'] as &$cartItem) {
      if ($cartItem['code'] === $pro_code) {
        $cartItem['quantity'] += $quantity;
        $found = true;
        break;
      }
    }
    // カートに商品がない場合、新規追加
    if (!$found) {
      $_SESSION['cart'][] = ['code' => $pro_code, 'quantity' => $quantity];
    }

    $message = 'カートに追加しました';
  }
  catch (Exception $e) {
    $message = 'ただいま障害により大変ご迷惑をお掛けしております。';
  }
?>
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900"><?= $message; ?></h2>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
  <div class="flex w-full justify-center pt-5">
    <a href="shop_list.php" class="block flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">商品一覧へ戻る</a>
  </div>
</div>
cart_in画面の詳細説明はこちら

1)GETで必要な値を取得
intval()で整数に変換しておく(念の為)

cart_in.php
    $pro_code = $_GET['procode'];
    $quantity = intval($_GET['quantity']);

2)カートを初期化する
セッションにあるcartの値を初期化する
中身なし、もしくは配列でない場合は初期化してしまう。

cart_in.php
    if (!isset($_SESSION['cart']) || !is_array($_SESSION['cart'])) {
      $_SESSION['cart'] = [];
    }

3)カートの中身を同じ形に統一する
foundフラグを立てておき、cartの中身をcartItemに格納する
codeが一致する場合は数量を加算(初めからカートの中身がある場合)
foundフラグで判断してカートの中身があるかを確認。
なければkeyと値を入れていく

cart_in.php
    // カートの内容を統一
    $found = false;
    foreach ($_SESSION['cart'] as &$cartItem) {
      if ($cartItem['code'] === $pro_code) {
        $cartItem['quantity'] += $quantity;
        $found = true;
        break;
      }
    }
    // カートに商品がない場合、新規追加
    if (!$found) {
      $_SESSION['cart'][] = ['code' => $pro_code, 'quantity' => $quantity];
    }

cart_look(カートの中身)画面

追加した商品の一覧画面です。
商品の個数を変更すると合計金額と送料が再計算されます。
変更した際の再計算はJSで実装しました。
画面読み込み時はPHPで合計などを計算させ表示、数量変更したとき(formで実装している箇所)はJSで再計算するというちょっとめんどくさいことをしています。
PHPのイベントリスナーつかえばよかったかも…

スクリーンショット 2025-01-03 11.57.16.png

cart_look.php(try~chatch部分)
<?php 
try {
    error_reporting(E_ALL);
    ini_set('display_errors', 1);

    if (!isset($_SESSION['cart']) || !is_array($_SESSION['cart'])) {
        $_SESSION['cart'] = []; // カートが空の場合に初期化
    }

    // カートデータのフィルタリング
    $cart = array_filter($_SESSION['cart'], function ($item) {
        return is_array($item) && isset($item['code'], $item['quantity']);
    });

    // 商品ごとの数量を計算
    $quantities = [];
    foreach ($cart as $item) {
        $quantities[$item['code']] = ($quantities[$item['code']] ?? 0) + $item['quantity'];
    }
    $unique_cart = array_keys($quantities); // 商品コードの一覧

    $dsn = 'mysql:dbname=testshop;host=localhost;charset=utf8';
    $user = 'root';
    $pass = '';
    $dbh = new PDO($dsn, $user, $pass);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $pro_name = [];
    $pro_price = [];
    $pro_gazou = [];
    $pro_quantity = [];

    foreach ($unique_cart as $val) {
      $sql = 'SELECT code, name, price, gazou FROM mst_product WHERE code = ?';
      $stmt = $dbh->prepare($sql);
      $stmt->execute([$val]);

      $rec = $stmt->fetch(PDO::FETCH_ASSOC);

      $pro_name[] = $rec['name'];
      $pro_price[] = $rec['price'];
      $pro_gazou[] = '<img src="../product/gazou/'.$rec['gazou'].'" class="w-24 h-24 object-cover rounded-md">';
      $pro_quantity[] = $quantities[$val];
    }

    $total_quantity = array_sum($pro_quantity);
    $shipping_fee = ceil($total_quantity / 6) * 300; // 送料計算
    $total_price = array_sum(array_map(function ($price, $quantity) {
      return $price * $quantity;
    }, $pro_price, $pro_quantity));
    $grand_total = $total_price + $shipping_fee;

    $dbh = null;
} catch (Exception $e) {
    echo '<p>ただいま障害により大変ご迷惑をお掛けしております。</p>';
    exit();
}
?>
cart_lookのtry~catch箇所詳細はこちら

1)エラーの発生を未然に防ぐためデータをフィルタリング
配列かつcodeとquantityが含まれているかをフィルタリング

cart_look.php
    $cart = array_filter($_SESSION['cart'], function ($item) {
        return is_array($item) && isset($item['code'], $item['quantity']);
    });

2)商品ごとの数量を計算する
配列を用意して商品コードごとに数量を再計算しています。
商品コードが無ければ0を入れ、存在する場合は数量をそのまま足します。

cart_look.php
    $quantities = [];
    foreach ($cart as $item) {
        $quantities[$item['code']] = ($quantities[$item['code']] ?? 0) + $item['quantity'];
    }
    $unique_cart = array_keys($quantities);

array_keys()でカートに入っている商品コードを一覧で取得しておきます。


3)カートに入っている商品情報をDBから取得

cart_look.php
    foreach ($unique_cart as $val) {
      $sql = 'SELECT code, name, price, gazou FROM mst_product WHERE code = ?';
      $stmt = $dbh->prepare($sql);
      $stmt->execute([$val]);

      $rec = $stmt->fetch(PDO::FETCH_ASSOC);

      $pro_name[] = $rec['name'];
      $pro_price[] = $rec['price'];
      $pro_gazou[] = '<img src="../product/gazou/'.$rec['gazou'].'" class="w-24 h-24 object-cover rounded-md">';
      $pro_quantity[] = $quantities[$val];
    }

4)数量や送料を計算する
画面が読み込まれた時点での数量を計算します。

cart_look.php
    $total_quantity = array_sum($pro_quantity);
    $shipping_fee = ceil($total_quantity / 6) * 300; // 送料計算
    $total_price = array_sum(array_map(function ($price, $quantity) {
      return $price * $quantity;
    }, $pro_price, $pro_quantity));
    $grand_total = $total_price + $shipping_fee;
cart_look.php(表示する部分)
<main class="container mx-auto px-6 py-10">
  <h2 class="text-3xl font-bold text-gray-800 mb-8">カートの中身</h2>
  <?php
    $max = count($cart);
    if($max==0) {
      print '<p>カートに商品が入っていません</p>';
      print '<button type="button" onclick="history.back()" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 mt-3 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</button>';
      exit();
    }else {
  ?>
    <div class="bg-white shadow-md rounded-lg overflow-hidden">
      <!-- 商品リスト -->
      <div class="divide-y divide-gray-200">
        <?php for ($i = 0; $i < count($unique_cart); $i++) { ?>
          <div class="flex items-center px-6 py-4">
            <?= $pro_gazou[$i]; ?>
            <div class="ml-4 flex-1">
                <h3 class="text-lg font-semibold text-gray-700"><?= htmlspecialchars($pro_name[$i]); ?></h3>
                <p class="text-sm text-gray-500">香り高い商品</p>
                <p class="text-xl font-bold text-green-600 mt-2"><?= number_format(htmlspecialchars($pro_price[$i])); ?></p>
            </div>
            <div class="flex items-center space-x-2">
                <input 
                    type="number" 
                    value="<?= htmlspecialchars($pro_quantity[$i]); ?>" 
                    min="1" 
                    class="w-16 border border-gray-300 rounded-md p-2 text-center focus:ring-blue-500 focus:border-blue-500 quantity-input"
                    data-price="<?= $pro_price[$i]; ?>"
                />
                <button class="text-red-600 hover:underline remove-item" data-code="<?= htmlspecialchars($unique_cart[$i]); ?>">削除</button>
            </div>
          </div>
        <?php } ?>
      </div>
    </div>

    <div class="mt-6 bg-white shadow-md rounded-lg p-6">
      <!-- 商品の合計金額 -->
      <div class="flex justify-between items-center">
        <p class="text-lg font-semibold">商品合計金額:</p>
        <p id="total-price" class="text-xl font-bold text-green-600"><?= number_format($total_price); ?></p>
      </div>

      <!-- 送料 -->
      <div class="flex justify-between items-center mt-4">
        <p class="text-lg font-semibold">送料:</p>
        <p id="shipping-fee" class="text-xl font-bold text-green-600"><?= number_format($shipping_fee); ?></p>
      </div>

      <!-- 最終合計金額 -->
      <div class="flex justify-between items-center mt-4">
        <p class="text-lg font-semibold">お支払い金額合計:</p>
        <p id="grand-total" class="text-2xl font-bold text-green-600"><?= number_format($grand_total); ?></p>
      </div>

      <!-- 購入手続きボタン -->
      <div class="mt-6 flex justify-end">
        <?php
        if(isset($_SESSION['login'])==true) {
          print '<a href="./shop_kantan_check.php" class="block bg-gray-600 text-white px-6 py-3 mr-3 rounded-md hover:bg-gray-300 hover:text-black focus:ring-2 focus:ring-green-400 transition">会員かんたん注文</a>';
          print '<a href="./shop_form.php" class="block bg-gray-300 text-black px-6 py-3 rounded-md hover:bg-gray-600 hover:text-white focus:ring-2 focus:ring-green-400 transition">お届け先を変更する</a>';
        } else {
          print '<a href="./shop_form.php" class="block bg-gray-600 text-white px-6 py-3 rounded-md hover:bg-gray-300 hover:text-black focus:ring-2 focus:ring-green-400 transition">購入手続きへ</a>';
        };
        ?>
      </div>
    </div>

  <?php } ?>
</main>

JSの解説は割愛しますが興味のある方は参考にしてください。
エラーハンドリングも含まれています。

scriptはこちら

  document.addEventListener('DOMContentLoaded', function () {
    const removeButtons = document.querySelectorAll('.remove-item');
    const quantityInputs = document.querySelectorAll('.quantity-input');
    const totalPriceElement = document.getElementById('total-price');
    const productRows = document.querySelectorAll('.divide-y > div');
    const hiddenCartData = document.getElementById('cart-data');

    // 商品を削除する関数
    function removeItem(event) {
      const button = event.target;
      const productCode = button.getAttribute('data-code'); // 商品コードを取得
      const row = button.closest('.flex.items-center.px-6.py-4'); // 商品行を取得

      row.remove();
      recalculateTotal();
    }

    // 合計金額を再計算する関数
    function recalculateTotal() {
      let total = 0;
      let totalQuantity = 0;
      const cartData = [];

      document.querySelectorAll('.quantity-input').forEach(input => {
        const quantity = parseInt(input.value, 10) || 0;
        const price = parseFloat(input.dataset.price);
        const productCode = input.closest('.flex.items-center.px-6.py-4')
                                .querySelector('.remove-item')
                                .getAttribute('data-code');
        if (quantity > 0) {
          cartData.push({ code: productCode, quantity: quantity });
        }
        total += quantity * price;
        totalQuantity += quantity;
      });

      // 送料を計算(6個ごとに300円)
      const shippingFee = Math.ceil(totalQuantity / 6) * 300;
      const grandTotal = total + shippingFee;

      // 表示を更新
      document.getElementById('total-price').textContent = `${total.toLocaleString()}円`;
      document.getElementById('shipping-fee').textContent = `${shippingFee.toLocaleString()}円`;
      document.getElementById('grand-total').textContent = `${grandTotal.toLocaleString()}円`;

      // hidden inputにカートデータをJSON形式で設定
      if (hiddenCartData) {
        hiddenCartData.value = JSON.stringify(cartData);
      }

      // サーバーにカート情報を送信
      fetch('./update_cart.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(cartData)
      })
      .then(response => response.json())
      .then(data => {
        if (data.success) {
          console.log('カート情報がサーバーに更新されました');
        } else {
          console.error('カート更新失敗:', data.message);
        }
      })
      .catch(error => {
        console.error('サーバー通信エラー:', error);
      });
    }


    // 削除ボタンにイベントリスナーを追加
    removeButtons.forEach(button => {
      button.addEventListener('click', removeItem);
    });

    // 個数変更時のイベントリスナーも再確認
    quantityInputs.forEach(input => {
      input.addEventListener('input', recalculateTotal);
    });

    // 初期カートデータをhidden inputに設定
    recalculateTotal();
  });

update_cart画面

カートの中で数量を変更した時、セッション内のカート情報を更新するためのAPIです。
再計算が行われるたびに呼び出され、cartセッション内容を再設定します。

update_cart.php
<?php
session_start();

header('Content-Type: application/json');

try {
    // JSONデータを取得
    $data = json_decode(file_get_contents('php://input'), true);

    // データ形式を確認
    if (!is_array($data)) {
        throw new Exception('無効なデータ形式です');
    }

    // セッションのカートデータをクリアしてから再設定
    $_SESSION['cart'] = [];
    foreach ($data as $item) {
        if (
            isset($item['code'], $item['quantity']) &&
            is_string($item['code']) &&
            is_int($item['quantity']) &&
            $item['quantity'] > 0
        ) {
            // 数量分の商品コードを挿入
            $_SESSION['cart'][] = [
                'code' => $item['code'],
                'quantity' => $item['quantity']
            ];
        } else {
            throw new Exception('不正な商品データが含まれています');
        }
    }

    // 成功レスポンス
    echo json_encode(['success' => true]);
} catch (Exception $e) {
    // エラーレスポンス
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
    http_response_code(400); // クライアントエラー
    exit;
}
?>
update_cartの詳細はこちら

1)クライアントに返すデータ形式を設定

header('Content-Type: application/json');

2)JSONデータの取得とデコード

$data = json_decode(file_get_contents('php://input'), true);

json_decode(..., true)
JSON文字列を連想配列に変換します。trueを指定することで、デコード結果が連想配列になります。

file_get_contents('php://input')
HTTPリクエストの生データ(リクエストボディ)を取得します。通常、JSONデータが送られています。


3)データ形式の確認
$dataが配列でなければエラーをスローします。これにより、JSONの形式が正しいか確認します。
確認をする理由はこのシステムを構築していく過程で、何度も形式の違うデータがセッションに保管されてしまい、エラーになったためです。
完成後は特に必要ないですが、この念の為残しておきました。

    if (!is_array($data)) {
        throw new Exception('無効なデータ形式です');
    }

4)カートデータを空にして再設定
一度セッションのカートデータをクリアし、
改めて内容を確認したうえで、JSONデータで受け取った値をセッションに入れていきます。

    $_SESSION['cart'] = [];
    foreach ($data as $item) {
        if (
            isset($item['code'], $item['quantity']) &&
            is_string($item['code']) &&
            is_int($item['quantity']) &&
            $item['quantity'] > 0
        ) {
            // 数量分の商品コードを挿入
            $_SESSION['cart'][] = [
                'code' => $item['code'],
                'quantity' => $item['quantity']
            ];
        } else {
            throw new Exception('不正な商品データが含まれています');
        }
    }

5)成功レスポンスとエラーハンドリングを返しておきます

    echo json_encode(['success' => true]);
} catch (Exception $e) {
    // エラーレスポンス
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
    http_response_code(400); // クライアントエラー
    exit;
}

form画面

名前や住所などを入力する発注画面です。
商品追加画面などで基本的なコード内容に触れているため、全貌は載せませんが、特記しておく箇所のみ紹介します。

スクリーンショット 2025-01-03 20.23.23.png

form.php(会員登録有無の箇所)
    <?php
    if(isset($_SESSION['login'])==false) {
      print '<div>';
      print '<label class="block text-sm font-medium text-gray-700">会員登録</label>';
      print '<div class="mt-2 flex space-x-4">';
      print '<div>';
      print '<input
              type="radio"
              id="register_yes"
              name="register"
              value="yes"
              class="mr-2"
              onclick="toggleRegistrationFields(true)"
            />';
      print '<label for="register_yes">会員登録をする</label>';
      print '</div>';
      print '<div>';
      print '<input
              type="radio"
              id="register_no"
              name="register"
              value="no"
              class="mr-2"
              onclick="toggleRegistrationFields(false)"
              checked
            />';
      print '<label for="register_no">会員登録をしない</label>';
      print '</div>';
      print '</div>';
      print '</div>';
    }
    ?>

ログイン状態を判断させ、登録ボタンを操作しています。
「会員登録をする」場合は入力項目が増えます。

スクリーンショット 2025-01-03 20.30.01.png

ログイン状態では会員登録の催促はありません。

スクリーンショット 2025-01-03 20.35.05.png

form_check画面

注文内容を最終確認する画面です。
商品追加やスタッフ追加画面と内容は同じで、セッションのカート情報から商品情報を取得してます。

スクリーンショット 2025-01-03 20.41.50.png
スクリーンショット 2025-01-03 20.41.56.png

form_done画面

スクリーンショット 2025-01-03 20.49.50.png
注文内容をDBに挿入し、自動返信メールを送信しています。
基本的なCRUD操作で実装できるのと、メールはmd_send_mail()で実装しています。

コードはこちら
form_done.php
<?php include './shop_head.php'; ?>
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
try {
  require_once('../common/common.php');

  $post = sanitize($_POST);

  $name = $post['name'];
  $company = $post['company'];
  $position = $post['position'];
  $email = $post['email'];
  $postal_code = $post['postal_code'];
  $address = $post['address'];
  $phone = $post['phone'];
  $order_date = date('Y-m-d H:i:s');
  $payment_method = 'お振込';
  $register_frag = $post['register_frag'];
  $password = md5($post['password']);
  $gender = $post['gender'];
  $birthdate = $post['birthdate'];
  $total_price = number_format($post['total_price']);
  $shipping_fee = number_format($post['shipping_fee']);
  $grand_total = number_format($post['grand_total']);

  $cart = $_SESSION['cart'] ?? [];

  $quantities = [];
  foreach ($cart as $item) {
    $product_code = $item['code'];
    $quantity = $item['quantity'];
    $quantities[$product_code] = ($quantities[$product_code] ?? 0) + $quantity;
  }

  $dsn = 'mysql:dbname=testshop;host=localhost;charset=utf8';
  $user = 'root';
  $pass = '';
  $dbh = new PDO($dsn, $user, $pass);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

  $dbh->exec('LOCK TABLES orders WRITE, order_details WRITE, mst_product READ, members WRITE');

  // 会員登録する場合はデータベースに挿入
  if($register_frag==true) {
    $sql = 'INSERT INTO members (password, member_name, email, postal, address, tel, gender, birthdate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$password, $name, $email, $postal_code, $address, $phone, $gender, $birthdate]);
    $member_code = $dbh->lastInsertId();
  } else {
    $member_code = '0';
  }

  // 注文情報をデータベースに挿入
  $sql = 'INSERT INTO orders (order_date, code_member, name, company, position, email, postal_code, address, phone) 
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
  $stmt = $dbh->prepare($sql);
  $stmt->execute([$order_date, $member_code, $name, $company, $position, $email, $postal_code, $address, $phone]);

  // AUTO_INCREMENT の値を取得して 12 桁にゼロパディング
  $order_id = $dbh->lastInsertId();
  $order_number = str_pad($order_id, 12, '0', STR_PAD_LEFT);

  // 商品情報の取得と計算
  $cart_items = [];
  foreach ($quantities as $product_code => $quantity) {
    $sql = 'SELECT code, name, price FROM mst_product WHERE code = ?';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$product_code]);

    $product = $stmt->fetch(PDO::FETCH_ASSOC);
    if ($product) {
      $cart_items[] = [
        'name' => $product['name'],
        'code' => $product['code'],
        'price' => $product['price'],
        'quantity' => $quantity,
      ];
    }
  }

  // 注文情報の詳細をデータベースに挿入
  // print json_encode($cart_items, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  foreach($cart_items as $item) {
    $sql = 'INSERT INTO order_details (code, product_code, price, quantity) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$order_id, $item['code'], $item['price'], $item['quantity']]);
  }


  $dbh->exec('UNLOCK TABLES');

  // 自動返信メール本文
  $email_body = <<<EOM
  チャフロ茶をご注文いただき、誠にありがとうございます。

  以下の内容でご注文を承りました。
  商品の発送準備が整い次第、改めて発送完了のメールをお送りいたします。
  ご不明な点がございましたら、このメールにご返信ください。

  ──────────────────────────
  ■ ご注文内容
  ──────────────────────────

  【ご注文番号】$order_number
  【ご注文日時】$order_date

  <ご注文商品>\n
  EOM;

  foreach($cart_items as $item) {
    $pro_name = htmlspecialchars($item['name']);
    $quantity = htmlspecialchars($item['quantity']);
    $price = number_format($item['price'] * $item['quantity']);

    $email_body.= <<<EOM
    1. $pro_name
    数量: $quantity
    金額: ¥$price\n\n
    EOM;
  }


  $email_body.= <<<EOM
  ──────────────────────────
  【商品合計金額】¥$total_price
  【送料】¥$shipping_fee
  【お支払い金額合計】¥$grand_total
  ──────────────────────────

  ■ お届け先情報
  ──────────────────────────
  【お名前】$name 様
  【会社名】$company
  【肩書き】$position
  【郵便番号】$postal_code
  【ご住所】$address
  【電話番号】$phone
  ──────────────────────────

  【お支払い方法】$payment_method
  【お振込先】
  代金は以下の口座にお振り込みください。

  銀行名:×××
  支店:×××支店
  番号:×××
  名義:×××

  金額:¥$grand_total
  ──────────────────────────
  入金確認がとれ次第、発送準備をさせていただきます。
  今後とも◯◯商店をよろしくお願い申し上げます。

  ━━━━━━━━━━━━━━━━━━━━
  ◯◯商店
  〒000-0000 住所
  Email: info@syooten.jp
  Website: https://dr-isd-jr.com/
  ━━━━━━━━━━━━━━━━━━━━
  EOM;

  // お客様宛メール送信
  $subject = 'ご注文確認メール - chafurocha';
  $headers = 'From: info@mimple.jp' . "\r\n";
  $headers .= 'Content-Type: text/plain; charset=UTF-8';
  $email_body = html_entity_decode($email_body, ENT_QUOTES, 'UTF-8');
  mb_language('Japanese');
  mb_internal_encoding('UTF-8');
  if (mb_send_mail($email, $subject, $email_body, $headers)) {
    $mail_status = '自動返信メールを送信しました。';
    $_SESSION['cart'] = [];
  } else {
    $mail_status = 'メール送信に失敗しました。管理者にお問い合わせください。';
  }

  // 管理者宛メール送信
  $subject = 'お客様からご注文がありました';
  $headers = 'From: '.$email;
  $headers .= 'Content-Type: text/plain; charset=UTF-8';
  $email_body = html_entity_decode($email_body, ENT_QUOTES, 'UTF-8');
  mb_language('Japanese');
  mb_internal_encoding('UTF-8');
  mb_send_mail('info@dr-isd-jr.com', $subject, $email_body, $headers);

  $dbh = null;
?>
<div class="flex flex-col justify-center px-6 py-12 lg:px-8">
  <div class="sm:mx-auto sm:w-full sm:max-w-lg text-center">
    <h2 class="text-2xl font-bold tracking-tight text-gray-900">ご注文ありがとうございます</h2>
    <p class="mt-4 text-gray-700">お客様のご注文を受け付けました。</p>
  </div>

  <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-lg bg-gray-100 p-6 rounded-lg shadow-md">
    <h3 class="text-lg font-medium text-gray-900">注文内容</h3>
    <div class="mt-4 space-y-4">
      <!-- 注文番号 -->
      <div class="flex justify-between border-b pb-2">
        <span class="font-medium text-gray-700">注文番号:</span>
        <span class="text-gray-900"><?= $order_number; ?></span>
      </div>
      <!-- 注文日時 -->
      <div class="flex justify-between border-b pb-2">
        <span class="font-medium text-gray-700">注文日時:</span>
        <span class="text-gray-900"><?= $order_date; ?></span>
      </div>
      <!-- 合計金額 -->
      <div class="flex justify-between border-b pb-2">
        <span class="font-medium text-gray-700">合計金額:</span>
        <span class="text-gray-900"><?= $grand_total; ?></span>
      </div>
      <!-- 支払い方法 -->
      <div class="flex justify-between">
        <span class="font-medium text-gray-700">お支払い方法:</span>
        <span class="text-gray-900"><?= $payment_method; ?></span>
      </div>
    </div>
  </div>

  <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-lg text-center">
    <p class="text-gray-700">発送の準備が整いましたら、メールにてご連絡いたします。</p>
    <p class="mt-4 text-sm text-gray-500">注文内容の詳細・お支払い方法はご登録のメールアドレスに送信いたします。</p>
    <p class="mt-4 text-sm text-green-500"><?= $mail_status; ?></p>
    <div class="mt-6">
      <a href="shop_list.php" class="inline-block rounded-md bg-gray-600 px-6 py-3 text-white text-sm font-semibold shadow hover:bg-gray-300">
        商品一覧へ戻る
      </a>
    </div>
  </div>
</div>
<?php 
} catch (Exception $e) {
  // エラー内容を表示
  echo '<p>エラーが発生しました:</p>';
  echo '<p>' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . '</p>';
  echo '<a href="shop_list.php" class="flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">戻る</a>';
  exit();
} 
?>
<?php include './shop_foot.php'; ?>

本番環境へUPする際、サーバ側でメール通信を設定する必要があります。

kantan_check画面

こちらの画面は会員登録しているユーザーが商品を注文する際、住所入力などを省略できる機能です。
ユーザー情報を取得して表示させています。
会員ログインをしてカートの中身を見ると、「会員かんたん注文」というボタンが表示されます。

スクリーンショット 2025-01-13 12.06.11.png

押すとDBから取得した会員情報がフォームに挿入された状態で表示されます。

スクリーンショット 2025-01-13 12.08.26.png

kantan_check画面のコードはこちら
kantan_check.php
<?php 

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

include './shop_head.php';

// セッションからカートデータを取得
$cart = $_SESSION['cart'] ?? [];
$member_id = $_SESSION['code'];

// カートが空の場合はエラーメッセージを表示
if (empty($cart)) {
    echo '<p class="text-red-600 text-center mt-6">カートが空です。</p>';
    echo '<div class="text-center"><a href="shop_cartlook.php" class="px-4 py-2 bg-gray-700 text-white rounded-md shadow hover:bg-gray-300">カートに戻る</a></div>';
    include './shop_foot.php';
    exit();
}

$dsn = 'mysql:dbname=testshop;host=localhost;charset=utf8';
$user = 'root';
$pass = '';
$dbh = new PDO($dsn, $user, $pass);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// 商品情報を取得
$cart_grouped = [];
foreach ($cart as $item) {
  // 商品コードと数量を取得
  $product_code = is_array($item) ? $item['code'] : $item;
  $quantity = is_array($item) ? $item['quantity'] : 1;

  if (isset($cart_grouped[$product_code])) {
    $cart_grouped[$product_code]['quantity'] = $quantity;
  } else {
    $sql = 'SELECT code, name, price FROM mst_product WHERE code = ?';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$product_code]);
    $product = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($product) {
      $cart_grouped[$product_code] = [
        'name' => $product['name'],
        'code' => $product['code'],
        'price' => $product['price'],
        'quantity' => $quantity
      ];
    }
  }
}

$sql = 'SELECT member_name,email,postal,address,tel FROM members WHERE member_code = ?';
$stmt = $dbh->prepare($sql);
$stmt->execute([$member_id]);
$members_rec = $stmt->fetch(PDO::FETCH_ASSOC);

$name = $members_rec['member_name'];
$email = $members_rec['email'];
$postal_code = $members_rec['postal'];
$address = $members_rec['address'];
$phone = $members_rec['tel'];

$dbh = null;

// $cart_grouped を配列として整理
$cart_items = array_values($cart_grouped);

// 合計金額を計算
$total_price = array_reduce($cart_items, function ($sum, $item) {
    return $sum + $item['price'] * $item['quantity'];
}, 0);

// 送料の計算 (6個ごとに300円)
$total_quantity = array_reduce($cart_items, function ($sum, $item) {return $sum + $item['quantity'];}, 0);
$shipping_fee = ceil($total_quantity / 6) * 300;

// 最終合計金額
$grand_total = $total_price + $shipping_fee;

?>
<h2 class="mt-10 text-center text-2xl font-bold tracking-tight text-gray-900">お届け先情報確認</h2>
<div class="mt-10 px-3 sm:mx-auto sm:w-full sm:max-w-lg overflow-y-auto">
  <!-- お届け先情報 -->
  <h3 class="text-lg font-medium text-gray-900">お届け先情報</h3>
  <form action="shop_kantan_done.php" method="POST" class="space-y-6">
    <!-- お名前 -->
    <div>
      <label class="block text-sm font-medium text-gray-700">お名前</label>
      <input type="hidden" name="name" value="<?= $name;?>">
      <p class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black"><?= htmlspecialchars($name); ?></p>
    </div>
    <!-- 会社名 -->
    <div>
      <label class="block text-sm font-medium text-gray-700">会社名・屋号 (任意)</label>
      <input type="text" name="company" placeholder="必要であれば入力してください" class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black">
    </div>
    <!-- 肩書き -->
    <div>
      <label class="block text-sm font-medium text-gray-700">肩書き (任意)</label>
      <input type="text" name="position" placeholder="必要であれば入力してください" class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black">
    </div>
    <!-- メールアドレス -->
    <div>
      <label class="block text-sm font-medium text-gray-700">メールアドレス</label>
      <input type="hidden" name="email" value="<?= $email; ?>">
      <p class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black"><?= htmlspecialchars($email); ?></p>
    </div>
    <!-- 郵便番号 -->
    <div>
      <label class="block text-sm font-medium text-gray-700">郵便番号</label>
      <input type="hidden" name="postal_code" value="<?= $postal_code; ?>">
      <p class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black"><?= htmlspecialchars($postal_code); ?></p>
    </div>
    <!-- 住所 -->
    <div>
      <label class="block text-sm font-medium text-gray-700">住所</label>
      <input type="hidden" name="address" value="<?= $address; ?>">
      <p class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black"><?= htmlspecialchars($address); ?></p>
    </div>
    <!-- 電話番号 -->
    <div>
      <label class="block text-sm font-medium text-gray-700">電話番号</label>
      <input type="hidden" name="phone" value="<?= $phone; ?>">
      <p class="w-full mt-1 border border-gray-300 rounded-md p-2 shadow-sm bg-gray-100 text-black"><?= htmlspecialchars($phone); ?></p>
    </div>


    <!-- 注文内容 -->
    <h3 class="text-lg font-medium text-gray-900 mt-6">注文内容</h3>
    <div class="bg-white shadow rounded-lg p-4">
      <?php foreach ($cart_items as $item): ?>
        <div class="flex justify-between border-b py-2">
          <span><?= htmlspecialchars($item['name']); ?> (x<?= htmlspecialchars($item['quantity']); ?>)</span>
          <span><?= number_format($item['price'] * $item['quantity']); ?></span>
        </div>
      <?php endforeach; ?>
      <div class="flex justify-between border-t pt-2 mt-2">
        <span>商品合計</span>
        <span><?= number_format($total_price); ?></span>
        <input type="hidden" name="total_price" value="<?= $total_price; ?>">
      </div>
      <div class="flex justify-between">
        <span>送料</span>
        <span><?= number_format($shipping_fee); ?></span>
        <input type="hidden" name="shipping_fee" value="<?= $shipping_fee; ?>">
      </div>
      <div class="flex justify-between font-bold">
        <span>合計金額</span>
        <span><?= number_format($grand_total); ?></span>
        <input type="hidden" name="grand_total" value="<?= $grand_total; ?>">
      </div>
    </div>

    <!-- ボタン -->
    <div class="flex justify-end space-x-4 mt-6 md:pb-20">
      <button 
        type="button" 
        onclick="history.back()" 
        class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md shadow hover:bg-gray-300"
      >
        戻る
      </button>
      <button 
        type="submit" 
        class="px-4 py-2 bg-gray-700 text-white rounded-md shadow hover:bg-gray-300"
      >
        次へ進む
      </button>
    </div>
  </form>
</div>

<?php include './shop_foot.php'; ?>

kantan_done画面

内容はform_done画面と同じです。

contact画面

お問い合わせページ。使用などはform画面と同じです。
スクリーンショット 2025-01-13 12.14.53.png

member_login画面

会員専用のログイン画面です。使用はスタッフログイン画面と同じです。
スクリーンショット 2025-01-13 12.18.24.png

member_login_check画面

スタッフのlogin_check画面と同じです。

member_logout画面

ログアウト機能です。 こちらもスタッフがログアウトする時と同じ仕様になります。
スクリーンショット 2025-01-13 12.18.16.png

logout画面のコードはこちら ```php:member_logout.php session_start();
$_SESSION=array();
if(isset($_COOKIE[session_name()])==true) {
setcookie(session_name(),'',time()-42000,'/');
}
session_destroy();

?>

chafurocha

ログアウトしました

ホームへ
```

clear_cart画面

こちらは構築中のセッション情報をクリアするために作成した機能です。

clear_cart.php
<?php

  session_start();
  $_SESSION=array();
  if(isset($_COOKIE[session_name()])==true) {
    setcookie(session_name(),'',time()-42000,'/');
  }
  session_destroy();

?>

<!DOCTYPE html>
<html class="h-full bg-white">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>石田商店</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      tailwind.config = {
        module.exports = {
          plugins: {
            require('@tailwindcss/forms'),
          }
        }
      }
  </script>
  </head>
  <body class="h-full">
    <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <p class="text-center mb-3">カートを空にしました</p>
      </div>
      <div class="flex w-full justify-center pt-5">
        <a href="shop_list.php" class="block flex justify-center rounded-md bg-slate-100 px-3 py-1.5 text-sm/6 font-semibold text-slate-800 shadow-sm hover:bg-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">商品一覧へ戻る</a>
      </div>
    </div>
  </body>
</html>

最後に

反省点や書き直したい箇所も多々ありますが、納期を決めて自分で要件定義し、構築していけたことには満足しています。
暇あれば修正していきたい。育児大変だからたぶんしない(笑)
そしてこの長々とした記事を最後まで書いた自分を承認しようと思います。
お付き合いいただきありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?