こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。
最近、HTML/CSSとPHPをProgateで勉強中です。
(これで勉強することについて否定的な意見もあるかと思いますし、理由を聞くと「なるほどな」ともなります。
但し、少なくとも私のようなインフラエンジニアがフロントエンド・バックエンドの為プログラミング言語を速習するためには良いのかなぁと思ってます。)
学んでいく中でPHPを使ってHTMLからデータを受け取る方法が出てきましたので、実際にコーディングしてみようと思います。
※当方インフラエンジニアです。フロントエンド関係は勉強中です。なので詳しいことは書けません。自分用のメモ的なサムシングです。
※細かいことはこれから勉強です
環境イメージ
Win11 PCにDockerを稼働できる環境(Docker Desktop+WSL2)を用意。
docker composeでWebコンテナとDBコンテナをデプロイしていきます。
デプロイ後は8080ポートにアクセスしてWebコンテナにアクセスして、データを入力・格納できることを確認します。
構成
.
├── docker-compose.yaml
├── mysql
│ ├── Dockerfile
│ ├── initdb
│ │ └── 001_create_schema.sql
│ └── src
└── php
├── Dockerfile
└── src
├── index.html
├── save.php
└── style.css
コード
docker-compose.yaml
services:
php:
build:
context: ./php
dockerfile: Dockerfile
image: myapp-php:8.4-apache
container_name: myapp-php
ports:
- "8080:80"
volumes:
- ./php/src:/var/www/html
environment:
# PHP から参照する DB 接続情報
DB_HOST: db
DB_NAME: app_db
DB_USER: app_user
DB_PASS: app_pass
DB_CHARSET: utf8mb4
depends_on:
db:
condition: service_healthy
db:
build:
context: ./mysql
dockerfile: Dockerfile
image: myapp-mysql:8.4
container_name: myapp-mysql
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: app_db
MYSQL_USER: app_user
MYSQL_PASSWORD: app_pass
TZ: Asia/Tokyo
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -prootpass || exit 1"]
interval: 10s
timeout: 5s
retries: 10
volumes:
db-data:
php/Dockerfile
# php/Dockerfile
FROM php:8.4-apache
# PDO MySQL 拡張をインストール
# (公式 PHP イメージは docker-php-ext-install で拡張導入できます)
RUN docker-php-ext-install pdo_mysql
mysql/Dockerfile
# mysql/Dockerfile
FROM mysql:8.4
# 初回起動時に実行される初期化 SQL を配置
COPY initdb/*.sql /docker-entrypoint-initdb.d/
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="style.css">
<title>HTML/CSS/PHP/MySQL</title>
</head>
<body>
<header>
<div class="header-container">
<p>HTML/CSSとPHPを連携してDBにデータを格納するサイト</p>
</div>
</header>
<div class="main">
<div class="main-container">
<form action="./save.php" method="post" autocomplete="on" novalidate>
<div class="input-title user-name">ユーザ名</div>
<input type="text" class="user-name-input" name="username" />
<div class="input-title password">パスワード</div>
<input type="password" class="password-input" name="password" />
<div class="input-title favorite-language">好きな言語</div>
<select class="favorite-language-input" name="favorite_language">
<option value="">選択してください</option>
<option value="JavaScript">JavaScript</option>
<option value="PHP">PHP</option>
<option value="Java">Java</option>
<option value="Ruby">Ruby</option>
<option value="Python">Python</option>
<option value="C#">C#</option>
<option value="C++">C++</option>
<option value="Golang">Golang</option>
</select>
<button class="submit-button" type="submit" >SUBMIT</button>
</form>
</div>
</div>
<footer>
<div class="footer-container">
<p>Copyright Since 2025 ©hogehogefugafuga. All rights reserved.</p>
</div>
</footer>
</body>
</html>
style.css
/*全体*/
body{
margin:0;
/* 追加: sticky footer */
min-height: 100vh;
display: flex;
flex-direction: column;
}
/*header*/
.header-container{
height:80px;
line-height: 80px;
background-color: #1800ad;
}
.header-container p{
margin:0;
color:#fff;
font-size:40px;
}
/*main*/
.main{
/* 追加: sticky footer */
flex: 1 0 auto;
}
.main-container{
width:80%;
margin:5% auto;
}
.input-title{
font-size:50px;
}
.user-name-input,
.password-input,
.favorite-language-input{
width:20%;
margin-top:20px;
margin-bottom:20px;
font-size:20px;
}
.submit-button{
display:block;
background-color: #0075FF;
font-weight: bold;
color:#fff;
border-radius: 20px;
padding:10px 20px;
margin-top:30px;
box-shadow: 0 5px 0 0 #0F62A8;
cursor: pointer;
}
.submit-button:active{
box-shadow: none;
position: relative;
top:5px;
}
/*footer*/
.footer-container{
background-color: #545454;
color:#fff;
text-align:right;
height:50px;
line-height: 50px;
}
.footer-container p{
/* 追加: sticky footer */
margin:0 10px;
font-size: 20px;
}
save.php
<?php
declare(strict_types=1);
// エラー表示(開発時のみ)
ini_set('display_errors', '1');
error_reporting(E_ALL);
// POST以外は405
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Method Not Allowed';
exit;
}
// 入力取得・サニタイズ(最低限)
$username = trim((string)($_POST['username'] ?? ''));
$password = (string)($_POST['password'] ?? '');
$favorite = trim((string)($_POST['favorite_language'] ?? ''));
// サーバー側バリデーション
$errors = [];
if ($username === '') $errors[] = 'ユーザ名は必須です。';
if ($password === '') $errors[] = 'パスワードは必須です。';
if ($favorite === '') $errors[] = '好きな言語を選択してください。';
if ($errors) {
http_response_code(422);
header('Content-Type: text/html; charset=UTF-8');
echo '<h1>入力エラー</h1><ul>';
foreach ($errors as $e) echo '<li>' . htmlspecialchars($e, ENT_QUOTES, 'UTF-8') . '</li>';
echo '</ul><p><a href="/">戻る</a></p>';
exit;
}
// パスワードをハッシュ化(ソルトは自動付与)
$pwdHash = password_hash($password, PASSWORD_DEFAULT); // 推奨手法[15]
if ($pwdHash === false) {
http_response_code(500);
echo 'パスワードのハッシュ化に失敗しました。';
exit;
}
// DB接続情報(環境に合わせて変更)
$dsn = 'mysql:host=db;dbname=app_db;charset=utf8mb4';
$user = 'app_user';
$pass = 'app_pass';
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // ネイティブプリペアド
]);
// 追加 INSERT(プリペアドステートメント)
$sql = 'INSERT INTO contacts (username, password_hash, favorite_language)
VALUES (:u, :p, :f)';
$stmt = $pdo->prepare($sql); // プリペアドで安全に実行[3][6]
$stmt->execute([
':u' => $username,
':p' => $pwdHash,
':f' => $favorite,
]);
// 結果表示(必要に応じてリダイレクトでもOK)
header('Content-Type: text/html; charset=UTF-8');
echo '<h1>保存しました</h1>';
echo '<p>ユーザ名:' . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . '</p>';
echo '<p>好きな言語:' . htmlspecialchars($favorite, ENT_QUOTES, 'UTF-8') . '</p>';
echo '<p><a href="/">トップに戻る</a></p>';
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=UTF-8');
echo 'DBエラー: ' . $e->getMessage();
exit;
}
001_create_schema.sql
-- mysql/initdb/001_create_schema.sql
-- データベースを用意(Compose の MYSQL_DATABASE を使う場合でも冪等性のため明示)
CREATE DATABASE IF NOT EXISTS app_db
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
-- 対象DBを選択
USE app_db;
-- アプリ用テーブル
CREATE TABLE IF NOT EXISTS contacts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
favorite_language VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_contacts_username (username) -- ユーザ名の一意制約
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
実行
PS C:\Users\ohtsu\Documents\html_dev\phpでDB連携> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS C:\Users\ohtsu\Documents\html_dev\phpでDB連携> docker compose up --build -d
#1 [internal] load local bake definitions
#1 reading from stdin 1.04kB 0.0s done
#1 DONE 0.0s
#2 [db internal] load build definition from Dockerfile
#2 transferring dockerfile: 185B done
#2 DONE 0.0s
#3 [php internal] load build definition from Dockerfile
#3 transferring dockerfile: 244B 0.0s done
#3 DONE 0.0s
#4 [php internal] load metadata for docker.io/library/php:8.4-apache
#4 DONE 2.2s
#5 [db internal] load metadata for docker.io/library/mysql:8.4
#5 DONE 2.4s
#6 [php internal] load .dockerignore
#6 transferring context: 2B done
#6 DONE 0.0s
#7 [php 1/2] FROM docker.io/library/php:8.4-apache@sha256:da52a29f01e7b0b07c8f6849b2564abf4c3736c490df7971484773fc95212b90
#7 DONE 0.0s
#8 [php 2/2] RUN docker-php-ext-install pdo_mysql
#8 CACHED
#9 [php] exporting to image
#9 exporting layers done
#9 writing image sha256:a1565c58c628455fbced5cfccc98fc98f7ff6a4efc79ce934dd9cf63cc641091 done
#9 naming to docker.io/library/myapp-php:8.4-apache done
#9 DONE 0.0s
#10 [php] resolving provenance for metadata file
#10 DONE 0.0s
#11 [db internal] load .dockerignore
#11 transferring context: 2B done
#11 DONE 0.0s
#12 [db 1/2] FROM docker.io/library/mysql:8.4@sha256:ad394eabaf5d728b88fd79732d94091f2165379e9b43e9748364b8c094f26e14
#12 DONE 0.0s
#13 [db internal] load build context
#13 transferring context: 76B done
#13 DONE 0.0s
#14 [db 2/2] COPY initdb/*.sql /docker-entrypoint-initdb.d/
#14 CACHED
#15 [db] exporting to image
#15 exporting layers done
#15 writing image sha256:d06f37996cfe07cb6e6653cf29e5dd438ae885a5c9f87db683fd2e86e45402c1 done
#15 naming to docker.io/library/myapp-mysql:8.4 done
#15 DONE 0.0s
#16 [db] resolving provenance for metadata file
#16 DONE 0.0s
[+] Running 6/6
✔ myapp-mysql:8.4 Built 0.0s
✔ myapp-php:8.4-apache Built 0.0s
✔ Network phpdb_default Created 0.0s
✔ Volume "phpdb_db-data" Created 0.0s
✔ Container myapp-mysql Healthy 10.8s
✔ Container myapp-php Started 11.1s
PS C:\Users\ohtsu\Documents\html_dev\phpでDB連携>
PS C:\Users\ohtsu\Documents\html_dev\phpでDB連携> docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
myapp-mysql myapp-mysql:8.4 "docker-entrypoint.s…" db 23 seconds ago Up 23 seconds (healthy) 3306/tcp, 33060/tcp
myapp-php myapp-php:8.4-apache "docker-php-entrypoi…" php 23 seconds ago Up 12 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
実際にDBを見てみる
PS C:\Users\ohtsu\Documents\html_dev\phpでDB連携> docker exec -it myapp-mysql mysql -u app_user -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 32
Server version: 8.4.6 MySQL Community Server - GPL
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| app_db |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.01 sec)
mysql> use app_db;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+------------------+
| Tables_in_app_db |
+------------------+
| contacts |
+------------------+
1 row in set (0.01 sec)
mysql> select * from contacts;
+----+----------+--------------------------------------------------------------+-------------------+---------------------+
| id | username | password_hash | favorite_language | created_at |
+----+----------+--------------------------------------------------------------+-------------------+---------------------+
| 1 | test01 | $2y$12$kDvmMHWwW7OtX4Ow7lJSY.Y7w4.WIgscOq5uIxA4wUw9i.N2DnPue | Python | 2025-08-16 16:22:30 |
| 2 | test02 | $2y$12$rhUkbZgBWw39yurxkl.FhOs7w3lEH7NC6e01dyGHVAitFx4REhZea | Java | 2025-08-16 16:22:45 |
| 3 | test03 | $2y$12$AaJzWLK1Kd1ssg7Od7VdGeL4Y6O2FceStKA7L/nAMInZlypF77jgi | PHP | 2025-08-16 16:22:58 |
+----+----------+--------------------------------------------------------------+-------------------+---------------------+
3 rows in set (0.00 sec)