内部でNixパッケージマネージャーを利用した、ポータブルで分離された開発環境を作るためのCLIツールのJetifyの「DevBox」を触っていきます。
Microsoftの「Dev Box」とは無関係です。
Devboxを使うと、難しいと言われるNix言語を書かずにJSONで管理できます。
Dockerとの比較
| Devbox | Docker |
|---|---|
| プロセスレベルでの分離 | OSレベルでの分離 |
| シェルを起動するだけ | コンテナ起動が必要 |
(前回記事はこちら Docker触ってみる )
Windows環境、エディタはAntigravityを使っていきます。
WSLとUbuntuの準備
WSLとUbuntuのインストール
# WSLインストール
wsl.exe --install
# Ubuntuのバージョンを指定する場合
wsl.exe --install Ubuntu-24.04
WSLのインストールが終わったらwindowsを再起動します。
WSLとUbuntuの確認
# WSLのインストール確認(-l は --list、-v は --verbose の略)
wsl -l -v
NAME STATE VERSION
* Ubuntu Running 2
| 項目名 | 詳細 |
|---|---|
| NAME | Ubuntu だけであれば、デフォルトの最新LTS (バージョンを指定してインストールした場合はそのバージョン名がUbuntuの後に入る) |
| STATE | Runningなど状態が表示される |
| VERSION | Ubuntuのバージョンではなく、WSLのバージョン |
最初の*はデフォルトで使われるものです。
# バージョン確認
wsl --version
WSL バージョン: 2.6.2.0
カーネル バージョン: 6.6.87.2-1
WSLg バージョン: 1.0.71
MSRDC バージョン: 1.2.6353
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.26100.1-240331-1435.ge-release
Windows バージョン: 10.0.26200.7462
Ubuntuの詳細確認
# Ubuntuのバージョンの確認(Ubuntuのターミナルで実行)
lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04.3 LTS
Release: 24.04
Codename: noble
Ubuntuの削除
不要になったときの削除方法です。
# Ubuntu削除
wsl --unregister Ubuntu
Ubuntuのアカウント作成
Ubuntuのターミナルを開くとUbuntuのユーザー名とパスワードの指定をするように促されるので入力します。
エディタ側の準備
エディタからWSLへの接続
Antigravityの左下の「><」 をクリックし、メニューから「Connect to WSL」を選択して接続します。
(デフォルト指定しているものではなくディストリビューションを選択して接続する場合は「Connect to WSL using distro...」からインストール済のディストリビューションの中から選択して接続します。)
VS CodeやCursorの場合
基本機能ではWSLに接続できないので、先にVS Codeの拡張機能マーケットプレイスで「「WSL」(https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl )をインストールします。
※Remote Development」( https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack )には「WSL」が含まれるのでこちらでもよいです。
WSL内のディレクトリを開く
接続できたら、「Open Folder」で「/home/ユーザー名/」を開きます。
Devboxの準備
Devboxのインストール
curl -fsSL https://get.jetify.com/devbox | bash
Devbox 📦 by Jetify
Instant, easy and predictable development environments.
This script downloads and installs the latest devbox binary.
Confirm Installation Details
Location: /usr/local/bin/devbox
Download URL: https://releases.jetify.com/devbox
? Install devbox to /usr/local/bin (requires sudo)? [Y/n]
Yキー+Enterを押すと/usr/local/bin/にdevboxがインストールされます。
Devboxのインストール確認
# Devboxのバージョン確認
devbox version
0.16.0
アプリ開発の環境構築
Devboxの初期化
# projects/my-appのフォルダを作成して中に移動
mkdir -p ~/projects/my-app
cd ~/projects/my-app
# devbox初期化
devbox init
/home/ユーザー名/projects/my-app/devbox.jsonが作成されます。
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"test": [
"echo \"Error: no test specified\" && exit 1"
]
}
}
}
Nixのパッケージのインストール
今回インストールするパッケージ
| パッケージ | 詳細 |
|---|---|
| Nginx | Webサーバー(ポート8080などで待機) |
| PHP-FPM | PHPを実行するプログラム |
| MariaDB | データベース |
#パッケージの追加
devbox add php nginx mariadb
Nixがインストールされていない場合はNixのインストールが始まり/nix(ルート直下)にインストールされます。
Nixのインストールが完了すると、自動的に先ほど指定した PHP, NginX, MariaDBのダウンロードが始まります。
| 項目 | ポート番号/場所 | 備考 |
|---|---|---|
| Nginxポート | 8081 | ブラウザでlocalhost:8081でアクセス |
| PHP-FPMポート | 8082 | NginxがPHPを処理する際に使う |
| Webルート | devbox.d/web/ | ここにindex.phpなどを置く |
| MySQLソケット | .devbox/virtenv/mariadb/run/mysql.sock | パスワードなしrootで接続可 |
Nixを使えるようにする
# パスを通す(ターミナルを一度閉じて開き直してもOK)
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
Nixが認識されているか確認します。
# nixのバージョン確認
nix-shell --version
nix-shell (Determinate Nix 3.15.1) 2.33.0
# Devboxのシェルを起動
devbox shell
Devboxのサービス起動
Devboxには、NginxやMariaDBなどのバックグラウンドプロセスを簡単に管理する機能があります。
以下のコマンドで現在のステータスを確認したり、起動したりできます。
# サービスの確認(devboxのshellで実行)
devbox services ls
Consider joining MariaDB's strong and vibrant community:
https://mariadb.org/get-involved/
No services currently running. Run `devbox services up` to start them:
mariadb
mariadb_logs
nginx
nginx-access
nginx-error
php-fpm
# すべてのサービス(Nginx, MariaDB等)を起動
devbox services up
# すべてのサービス(Nginx, MariaDB等)をバックグラウンド起動
devbox services start
# すべてのサービスを停止
devbox services stop
devbox services upを実行すると下記の表示になり、ターミナルとしては使用できなくなります。

(終了はCtrl+C)
サービスを起動してから http://localhost:8081 にアクセスすると、/home/ユーザー名/projects/my-app/devbox.d/web/index.htmlが開きます。
Nginxの動作確認
確認用のphpファイルを作成します。
<?php phpinfo(); ?>
http://localhost:8081/index.php を開いても<?php phpinfo(); ?>がそのまま表示される場合はnginx.confを変更する必要があります。
(nginx.templateから生成するので直接 nginx.conf を編集しない)
# The nginx.conf in this folder is automatically generated from nginx.template
# To modify your NGINX config, edit the nginx.template file
events {}
http{
server {
listen $NGINX_WEB_PORT;
listen [::]:$NGINX_WEB_PORT;
server_name $NGINX_WEB_SERVER_NAME;
root $NGINX_WEB_ROOT;
error_log error.log error;
access_log access.log;
client_body_temp_path temp/client_body;
proxy_temp_path temp/proxy;
fastcgi_temp_path temp/fastcgi;
uwsgi_temp_path temp/uwsgi;
scgi_temp_path temp/scgi;
index index.html;
server_tokens off;
}
}
↓ 変更
events {}
http {
# CSS や画像ファイルがただのテキストとして扱われないようにする
types {
text/html html htm shtml;
text/css css;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
image/png png;
image/x-icon ico;
application/json json;
}
default_type application/octet-stream;
server {
listen $NGINX_WEB_PORT;
server_name $NGINX_WEB_SERVER_NAME;
root $NGINX_WEB_ROOT;
error_log error.log error;
access_log access.log;
# http://localhost:8081/ にアクセスしたときにindex.phpを探しに行くようにする
index index.php index.html;
location / {
# ディレクトリ内の実ファイル(phpmyadminなど)を優先して探す
try_files $uri $uri/ /index.php?$query_string;
}
# PHPファイルへのリクエストを PHP-FPM(8082番ポート)に渡すようにする
location ~ \.php$ {
fastcgi_pass 127.0.0.1:8082;
include fastcgi.conf;
# $realpath_rootは「今の絶対パス」を自動で入れてくれる
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
}
}
templateからconfを再生成するためにNginXをリスタート
# すべてのサービス(Nginx, MariaDB等)を起動 終了はCtrl+C
devbox services up
# すべてのサービス(Nginx, MariaDB等)をバックグラウンド起動
devbox services start
# すべてのサービスを停止
devbox services stop
http://localhost:8081/index.php を開くとphpinfoが表示されます。
データベースの準備
データベース作成
init.sqlを新規作成します。
-- データベースmydbが存在しない場合は作成
CREATE DATABASE IF NOT EXISTS mydb;
-- データベースを使用
USE mydb;
-- テーブルmytableが存在しない場合は作成
CREATE TABLE IF NOT EXISTS mytable (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
hp INT NOT NULL, -- HP
attack INT NOT NULL, -- こうげき
defense INT NOT NULL, -- ぼうぎょ
sp_attack INT NOT NULL, -- とくこう
sp_defense INT NOT NULL, -- とくぼう
speed INT NOT NULL -- すばやさ
);
-- 再実行時のデータ重複を防ぐためにテーブルを空にする
TRUNCATE TABLE mytable;
-- ポケモンデータを挿入
INSERT INTO mytable (name, hp, attack, defense, sp_attack, sp_defense, speed) VALUES
('ピカチュウ', 35, 55, 40, 50, 50, 90),
('フシギダネ', 45, 49, 49, 65, 65, 45),
('ヒトカゲ', 39, 52, 43, 60, 50, 65),
('ゼニガメ', 44, 48, 65, 50, 64, 43),
('ミュウツー', 106, 110, 90, 154, 90, 130);
init.sqlを実行します。
mariadb -u root < init.sql
データベースの確認
# すべてのサービス(Nginx, MariaDB等)を起動 終了はCtrl+C
devbox services up
# すべてのサービス(Nginx, MariaDB等)をバックグラウンド起動
devbox services start
# すべてのサービスを停止
devbox services stop
MariaDBにログインして確認する場合
# データベースmydbを指定してMariaDBにログインする
mariadb -u root mydb
# テーブルmytableのスキーマを確認
DESC mytable;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| hp | int(11) | NO | | NULL | |
| attack | int(11) | NO | | NULL | |
| defense | int(11) | NO | | NULL | |
| sp_attack | int(11) | NO | | NULL | |
| sp_defense | int(11) | NO | | NULL | |
| speed | int(11) | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
# テーブルmytableのスキーマをより詳細に確認
SHOW FULL COLUMNS FROM mytable;
+------------+--------------+--------------------+------+-----+---------+----------------+---------------------------------+---------+
| Field | Type | Collation | Null | Key | Default | Extra | Privileges | Comment |
+------------+--------------+--------------------+------+-----+---------+----------------+---------------------------------+---------+
| id | int(11) | NULL | NO | PRI | NULL | auto_increment | select,insert,update,references | |
| name | varchar(255) | utf8mb4_unicode_ci | NO | | NULL | | select,insert,update,references | |
| hp | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
| attack | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
| defense | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
| sp_attack | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
| sp_defense | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
| speed | int(11) | NULL | NO | | NULL | | select,insert,update,references | |
+------------+--------------+--------------------+------+-----+---------+----------------+---------------------------------+---------+
# テーブルmytableのデータを確認
SELECT * FROM mytable LIMIT 5;
+----+-----------------+-----+--------+---------+-----------+------------+-------+
| id | name | hp | attack | defense | sp_attack | sp_defense | speed |
+----+-----------------+-----+--------+---------+-----------+------------+-------+
| 1 | ピカチュウ | 35 | 55 | 40 | 50 | 50 | 90 |
| 2 | フシギダネ | 45 | 49 | 49 | 65 | 65 | 45 |
| 3 | ヒトカゲ | 39 | 52 | 43 | 60 | 50 | 65 |
| 4 | ゼニガメ | 44 | 48 | 65 | 50 | 64 | 43 |
| 5 | ミュウツー | 106 | 110 | 90 | 154 | 90 | 130 |
+----+-----------------+-----+--------+---------+-----------+------------+-------+
# MariaDBからログアウト
exit
phpMyAdminで確認する場合
phpMyAdminのインストール
# projects/my-app/devbox.d/webへ移動
cd ~/projects/my-app/devbox.d/web
# ダウンロード
curl -O https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz
# 解凍
tar -xzf phpMyAdmin-5.2.3-all-languages.tar.gz
# 名前を使いやすく変更
mv phpMyAdmin-5.2.3-all-languages phpmyadmin
# 不要になったファイルを削除
rm phpMyAdmin-5.2.3-all-languages.tar.gz
# すべてのサービス(Nginx, MariaDB等)を起動 終了はCtrl+C
devbox services up
# すべてのサービス(Nginx, MariaDB等)をバックグラウンド起動
devbox services start
# すべてのサービスを停止
devbox services stop
phpMyAdminにログインする
http://localhost:8081/phpmyadmin/ にアクセスするとphpmyadminが表示されます。
MariaDBへのログイン(ユーザー:root、パスワードなし)しようとすると下記のように表示されてログインできない場合
パスワードなしログインは設定 (AllowNoPassword 参照) によって禁止されています
パスワードなしログインできるようにconfig.inc.phpを作成して、値を変更します。
# /projects/my-app/devbox.d/web/phpmyadmin/に移動
cd ~/projects/my-app/devbox.d/web/phpmyadmin/
# config.sample.inc.phpをconfig.inc.phpとしてコピー
cp config.sample.inc.php config.inc.php
config.inc.phpの変更
$cfg['Servers'][$i]['host'] = 'localhost';
↓
$cfg['Servers'][$i]['host'] = '127.0.0.1';
$cfg['Servers'][$i]['AllowNoPassword'] = false;
↓
$cfg['Servers'][$i]['AllowNoPassword'] = true;
| 記述 | 詳細 |
|---|---|
| localhost と書いた場合 | PHPは「Unixドメインソケット」という、特定のファイル(例: /tmp/mysql.sock)を介して通信しようとする Devbox環境ではこのファイルの場所が標準と異なるため、見つからずにエラーになる |
| 127.0.0.1 と書いた場合 | PHPはネットワーク(TCP/IP)を使って、ポート 3306 番で待ち構えているデータベースに直接話しかけるのでソケットファイルの場所を知らなくても繋がる |
phpMyAdminでデータベースを確認する
# すべてのサービス(Nginx, MariaDB等)を起動 終了はCtrl+C
devbox services up
# すべてのサービス(Nginx, MariaDB等)をバックグラウンド起動
devbox services start
# すべてのサービスを停止
devbox services stop
http://localhost:8081/phpmyadmin/ にアクセスするとphpmyadminが表示されます。
MariaDBへのログイン(ユーザー:root、パスワードなし)
今度はログインできて、データベースが作成されていることを確認できます。
アプリの作成
ポケモンのデータを登録、更新、削除ができる簡単なアプリを作成してみます。
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 200 200" fill="#ef4444"><path d="M114.055 26.47h-28.11v3.687H75.75l2.698-7.473a9.062 9.062 0 0 1 8.508-5.975h26.088a9.062 9.062 0 0 1 8.508 5.975l2.698 7.473h-10.195V26.47ZM153.098 71.368l-8.242 111.924H55.144L46.902 71.368h106.196ZM100 173.498c.093.005.187.007.282.007 3.197-.002 5.828-2.633 5.83-5.83V87.636c-.002-3.197-2.633-5.828-5.83-5.83-.095 0-.189.002-.282.007a5.255 5.255 0 0 0-.282-.007c-3.197.002-5.828 2.633-5.83 5.83v80.039c.002 3.197 2.633 5.828 5.83 5.83.095 0 .189-.002.282-.007ZM76.258 87.32c-.175-3.193-2.945-5.677-6.138-5.505-3.193.175-5.677 2.944-5.506 6.137l4.338 80.038c.174 3.193 2.944 5.678 6.137 5.507 3.194-.174 5.679-2.944 5.507-6.138L76.258 87.32Zm47.484 0-4.338 80.039c-.172 3.194 2.313 5.964 5.507 6.138 3.193.171 5.963-2.314 6.137-5.507l4.338-80.038c.171-3.193-2.313-5.962-5.506-6.137-3.193-.172-5.963 2.312-6.138 5.505ZM157.436 41.435l8.894 19.738H33.67l8.894-19.738h114.872Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 200 200" fill="#3b82f6"><path d="M157.084 75.877 123.94 42.731l16.388-16.388 33.146 33.145zM35.734 130.936l-.552.552-8.655 42.17 42.353-9.576-33.146-33.146ZM74.773 158.189l-33.146-33.146 76.42-76.42 33.145 33.146z"/></svg>
<?php
$mysqli = new mysqli("127.0.0.1", "root", "", "mydb");
if ($mysqli->connect_error) {
die("Connect Error (" . $mysqli->connect_errno . ") " . $mysqli->connect_error);
}
$errors = [];
$form_data = [
'name' => '', 'hp' => '', 'attack' => '', 'defense' => '',
'sp_attack' => '', 'sp_defense' => '', 'speed' => ''
];
$labels = [
'name' => 'なまえ', 'hp' => 'HP', 'attack' => 'こうげき', 'defense' => 'ぼうぎょ',
'sp_attack' => 'とくこう', 'sp_defense' => 'とくぼう', 'speed' => 'すばやさ'
];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$form_data = [
'name' => $_POST['name'],
'hp' => $_POST['hp'],
'attack' => $_POST['attack'],
'defense' => $_POST['defense'],
'sp_attack' => $_POST['sp_attack'],
'sp_defense' => $_POST['sp_defense'],
'speed' => $_POST['speed']
];
// バリデーション
if (empty(trim($form_data['name']))) {
$errors[] = "なまえを入力してください。";
}
foreach (['hp', 'attack', 'defense', 'sp_attack', 'sp_defense', 'speed'] as $key) {
$val = $form_data[$key];
if ($val === '') {
$errors[] = $labels[$key] . "を入力してください。";
} elseif (!is_numeric($val) || $val < 0 || $val > 255) {
$errors[] = $labels[$key] . "は0から255の間で入力してください。";
}
}
if (empty($errors)) {
$stmt = $mysqli->prepare("INSERT INTO mytable (name, hp, attack, defense, sp_attack, sp_defense, speed) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("siiiiii", $form_data['name'], $form_data['hp'], $form_data['attack'], $form_data['defense'], $form_data['sp_attack'], $form_data['sp_defense'], $form_data['speed']);
if ($stmt->execute()) {
$stmt->close();
$mysqli->close();
header("Location: index.php");
exit();
} else {
$errors[] = "データベースエラーが発生しました: " . $stmt->error;
}
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ポケモン追加</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<h1>ポケモン追加</h1>
<p>
<a href="index.php" class="link-primary"><< 一覧に戻る</a>
</p>
<form method="post">
<table>
<tr>
<th>なまえ<span class="badge-required">必須</span></th>
<td><input type="text" name="name" value="<?php echo htmlspecialchars($form_data['name']); ?>" required autofocus maxlength="6"></td>
</tr>
<tr>
<th>HP<span class="badge-required">必須</span></th>
<td><input type="number" name="hp" value="<?php echo htmlspecialchars($form_data['hp']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>こうげき<span class="badge-required">必須</span></th>
<td><input type="number" name="attack" value="<?php echo htmlspecialchars($form_data['attack']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>ぼうぎょ<span class="badge-required">必須</span></th>
<td><input type="number" name="defense" value="<?php echo htmlspecialchars($form_data['defense']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>とくこう<span class="badge-required">必須</span></th>
<td><input type="number" name="sp_attack" value="<?php echo htmlspecialchars($form_data['sp_attack']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>とくぼう<span class="badge-required">必須</span></th>
<td><input type="number" name="sp_defense" value="<?php echo htmlspecialchars($form_data['sp_defense']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>すばやさ<span class="badge-required">必須</span></th>
<td><input type="number" name="speed" value="<?php echo htmlspecialchars($form_data['speed']); ?>" min="0" max="255" required></td>
</tr>
</table>
<br>
<button type="submit" class="button button-primary">追加</button>
<?php if (!empty($errors)): ?>
<div class="error-message">
<?php foreach ($errors as $error): ?>
<div><?php echo htmlspecialchars($error); ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</form>
</body>
</html>
<?php
$mysqli = new mysqli("127.0.0.1", "root", "", "mydb");
if ($mysqli->connect_error) {
die("Connect Error (" . $mysqli->connect_errno . ") " . $mysqli->connect_error);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ids'])) {
$ids = $_POST['ids'];
if (is_array($ids)) {
// プレースホルダーを動的に生成
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$types = str_repeat('i', count($ids));
$stmt = $mysqli->prepare("DELETE FROM mytable WHERE id IN ($placeholders)");
$stmt->bind_param($types, ...$ids);
$stmt->execute();
$stmt->close();
}
} elseif (isset($_GET['id'])) {
$id = $_GET['id'];
$stmt = $mysqli->prepare("DELETE FROM mytable WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$stmt->close();
}
$mysqli->close();
header("Location: index.php");
exit();
?>
<?php
$mysqli = new mysqli("127.0.0.1", "root", "", "mydb");
if ($mysqli->connect_error) {
die("Connect Error (" . $mysqli->connect_errno . ") " . $mysqli->connect_error);
}
$id = isset($_GET['id']) ? $_GET['id'] : (isset($_POST['id']) ? $_POST['id'] : null);
$errors = [];
$form_data = null;
$labels = [
'name' => 'なまえ', 'hp' => 'HP', 'attack' => 'こうげき', 'defense' => 'ぼうぎょ',
'sp_attack' => 'とくこう', 'sp_defense' => 'とくぼう', 'speed' => 'すばやさ'
];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$form_data = [
'id' => $_POST['id'],
'name' => $_POST['name'],
'hp' => $_POST['hp'],
'attack' => $_POST['attack'],
'defense' => $_POST['defense'],
'sp_attack' => $_POST['sp_attack'],
'sp_defense' => $_POST['sp_defense'],
'speed' => $_POST['speed']
];
// バリデーション
if (empty(trim($form_data['name']))) {
$errors[] = "なまえを入力してください。";
}
foreach (['hp', 'attack', 'defense', 'sp_attack', 'sp_defense', 'speed'] as $key) {
$val = $form_data[$key];
if ($val === '') {
$errors[] = $labels[$key] . "を入力してください。";
} elseif (!is_numeric($val) || $val < 0 || $val > 255) {
$errors[] = $labels[$key] . "は0から255の間で入力してください。";
}
}
if (empty($errors)) {
$stmt = $mysqli->prepare("UPDATE mytable SET name=?, hp=?, attack=?, defense=?, sp_attack=?, sp_defense=?, speed=? WHERE id=?");
$stmt->bind_param("siiiiiii", $form_data['name'], $form_data['hp'], $form_data['attack'], $form_data['defense'], $form_data['sp_attack'], $form_data['sp_defense'], $form_data['speed'], $form_data['id']);
if ($stmt->execute()) {
$stmt->close();
$mysqli->close();
header("Location: index.php");
exit();
} else {
$errors[] = "データベースエラーが発生しました: " . $stmt->error;
}
}
} elseif ($id) {
$stmt = $mysqli->prepare("SELECT * FROM mytable WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$form_data = $result->fetch_assoc();
$stmt->close();
}
if (!$form_data) {
die("Data not found");
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ポケモン編集</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<h1>ポケモン編集</h1>
<p>
<a href="index.php" class="link-primary"><< 一覧に戻る</a>
</p>
<form method="post">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($form_data['id']); ?>">
<table>
<tr>
<th>なまえ<span class="badge-required">必須</span></th>
<td><input type="text" name="name" value="<?php echo htmlspecialchars($form_data['name']); ?>" required autofocus maxlength="6"></td>
</tr>
<tr>
<th>HP<span class="badge-required">必須</span></th>
<td><input type="number" name="hp" value="<?php echo htmlspecialchars($form_data['hp']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>こうげき<span class="badge-required">必須</span></th>
<td><input type="number" name="attack" value="<?php echo htmlspecialchars($form_data['attack']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>ぼうぎょ<span class="badge-required">必須</span></th>
<td><input type="number" name="defense" value="<?php echo htmlspecialchars($form_data['defense']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>とくこう<span class="badge-required">必須</span></th>
<td><input type="number" name="sp_attack" value="<?php echo htmlspecialchars($form_data['sp_attack']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>とくぼう<span class="badge-required">必須</span></th>
<td><input type="number" name="sp_defense" value="<?php echo htmlspecialchars($form_data['sp_defense']); ?>" min="0" max="255" required></td>
</tr>
<tr>
<th>すばやさ<span class="badge-required">必須</span></th>
<td><input type="number" name="speed" value="<?php echo htmlspecialchars($form_data['speed']); ?>" min="0" max="255" required></td>
</tr>
</table>
<br>
<button type="submit" class="button button-primary">更新</button>
<?php if (!empty($errors)): ?>
<div class="error-message">
<?php foreach ($errors as $error): ?>
<div><?php echo htmlspecialchars($error); ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</form>
</body>
</html>
<?php
$mysqli = new mysqli("127.0.0.1", "root", "", "mydb");
if ($mysqli->connect_error) {
die("Connect Error (" . $mysqli->connect_errno . ") " . $mysqli->connect_error);
}
$sql = "SELECT * FROM mytable";
$result = $mysqli->query($sql);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ポケモンデータ</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>ポケモン能力値一覧</h1>
<form action="delete.php" method="POST" id="bulk-delete-form" data-js-bulk-delete-form>
<div class="action-bar">
<p><a href="create.php" class="button button-primary">新規追加</a></p>
<?php if ($result->num_rows > 0): ?>
<button type="submit" class="button button-danger" data-js-bulk-delete-btn>選択した項目を削除</button>
<?php endif; ?>
</div>
<?php if ($result->num_rows > 0): ?>
<table>
<tr>
<th><input type="checkbox" id="select-all" data-js-select-all></th>
<th>ID</th>
<th>なまえ</th>
<th>HP</th>
<th>こうげき</th>
<th>ぼうぎょ</th>
<th>とくこう</th>
<th>とくぼう</th>
<th>すばやさ</th>
<th>操作</th>
</tr>
<?php
while($row = $result->fetch_assoc()) {
echo "<tr>";
echo "<td><input type='checkbox' name='ids[]' value='" . $row["id"] . "' data-js-checkbox-item></td>";
echo "<td class='text-right'>" . $row["id"] . "</td>";
echo "<td>" . htmlspecialchars($row["name"]) . "</td>";
echo "<td class='text-right'>" . $row["hp"] . "</td>";
echo "<td class='text-right'>" . $row["attack"] . "</td>";
echo "<td class='text-right'>" . $row["defense"] . "</td>";
echo "<td class='text-right'>" . $row["sp_attack"] . "</td>";
echo "<td class='text-right'>" . $row["sp_defense"] . "</td>";
echo "<td class='text-right'>" . $row["speed"] . "</td>";
echo "<td>";
echo "<a href='edit.php?id=" . $row["id"] . "'><img src='/img/icon_edit.svg' class='action-icon'> </a>";
echo "<a href='delete.php?id=" . $row["id"] . "' data-js-confirm-single><img src='/img/icon_delete.svg' class='action-icon'></a>";
echo "</td>";
echo "</tr>";
}
?>
</table>
<?php else: ?>
<p>登録されているポケモンがいません。「新規追加」ボタンから登録してください。</p>
<?php endif; ?>
</form>
<script src="script.js"></script>
</body>
</html>
<?php
$mysqli->close();
?>
document.addEventListener('DOMContentLoaded', function () {
//input type="text"の制御
const numberInputs = document.querySelectorAll('input[type="text"]');
//6桁まで入力可能にする
numberInputs.forEach(function (input) {
input.addEventListener('input', function () {
if (this.value.length > 6) {
this.value = this.value.slice(0, 6);
}
});
});
//tdの制御
const tds = document.querySelectorAll('td');
//tdをクリックすると子要素のinputにフォーカスする
tds.forEach(function (td) {
td.addEventListener('click', function () {
const input = this.querySelector('input');
if (input) {
input.focus();
}
});
});
});
//input type="number"の制御
document.addEventListener('DOMContentLoaded', function () {
const numberInputs = document.querySelectorAll('input[type="number"]');
//3桁まで入力可能にする
numberInputs.forEach(function (input) {
input.addEventListener('input', function () {
if (this.value.length > 3) {
this.value = this.value.slice(0, 3);
}
});
//数字以外を入力できなくする
input.addEventListener('keydown', function (e) {
if (['e', 'E', '+', '-'].includes(e.key)) {
e.preventDefault();
}
});
//数字以外をペーストできなくする
input.addEventListener('paste', function (e) {
const paste = (e.clipboardData || window.clipboardData).getData('text');
if (/[eE\+\-]/.test(paste)) {
e.preventDefault();
}
});
});
});
// 削除操作の制御
document.addEventListener('DOMContentLoaded', function () {
// 個別削除の確認
const deleteLinks = document.querySelectorAll('[data-js-confirm-single]');
deleteLinks.forEach(function (link) {
link.addEventListener('click', function (e) {
if (!confirm('本当に削除しますか?')) {
e.preventDefault();
}
});
});
// 全選択チェックボックス
const selectAll = document.querySelector('[data-js-select-all]');
const checkboxes = document.querySelectorAll('[data-js-checkbox-item]');
if (selectAll) {
selectAll.addEventListener('change', function () {
checkboxes.forEach(function (checkbox) {
checkbox.checked = selectAll.checked;
});
});
}
// 一括削除ボタンの確認
const bulkDeleteButton = document.querySelector('[data-js-bulk-delete-btn]');
if (bulkDeleteButton) {
bulkDeleteButton.addEventListener('click', function (e) {
const checkedCount = document.querySelectorAll('[data-js-checkbox-item]:checked').length;
if (checkedCount === 0) {
alert('削除する項目を選択してください。');
e.preventDefault();
return;
}
if (!confirm('選択した ' + checkedCount + ' 件のデータを本当に削除しますか?')) {
e.preventDefault();
}
});
}
});
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
/* ==========================================================================
Base
========================================================================== */
body {
padding: 0 20px;
font-family: 'Noto Sans JP', sans-serif;
}
h1 {
font-size: 1.5rem;
}
a {
text-decoration: none;
}
a:link,
a:visited {
color: black;
}
input {
width: 100%;
}
.link-primary {
color: #3b82f6 !important;
/* Force override base black link color */
}
.link-primary:hover {
text-decoration: underline;
}
/* ==========================================================================
Layout
========================================================================== */
.action-bar {
margin-bottom: 1rem;
}
.action-bar p {
display: inline-block;
margin-right: 1rem;
}
/* ==========================================================================
Components
========================================================================== */
/* Buttons */
.button {
border: none;
padding: 5px 15px;
cursor: pointer;
border-radius: 4px;
color: white;
display: inline-block;
font-size: 0.9rem;
transition: opacity 0.3s;
}
.button:hover {
opacity: 0.8;
}
.button-primary {
background-color: #3b82f6;
}
.button-danger {
background-color: #ef4444;
}
/* Link as Button overrides */
a.button {
text-decoration: none;
color: white;
}
/* Action Icons */
.action-icon {
width: 1.25rem;
vertical-align: middle;
transition: opacity 0.3s;
}
.action-icon:hover {
opacity: 0.6;
}
/* Badges */
.badge-required {
background-color: #ef4444;
color: white;
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
vertical-align: middle;
font-weight: bold;
line-height: 1;
display: inline-block;
margin-top: -2px;
}
/* Data Table & Inputs */
table {
border-collapse: collapse;
width: min-content;
}
th,
td {
border: 1px solid #ddd;
padding: 1rem;
text-align: left;
text-box-edge: cap alphabetic;
text-box-trim: trim-both;
white-space: nowrap;
width: min-content;
}
th {
background-color: #f2f2f2;
font-weight: normal;
}
/* Inputs in table */
input[type="text"],
input[type="number"] {
border: none;
}
input[type="text"]:focus,
input[type="number"]:focus {
outline: none;
}
input[type="text"] {
width: 4.1rem;
}
input[type="number"] {
text-align: right;
width: 3rem;
}
input[type="checkbox"] {
margin-left: 0;
}
/* Cell specific controls */
td:has(input[type="text"]),
td:has(input[type="number"]) {
padding: 1rem;
}
td:has(input[type="number"]) {
padding-left: 0;
padding-right: 0;
text-align: right;
}
/* Focus highlighting for cells */
td:has(input[type="text"]:focus),
td:has(input[type="number"]:focus) {
outline: 2px solid #3b82f6;
outline-offset: 0px;
}
/* ==========================================================================
Utilities
========================================================================== */
.text-right {
text-align: right;
}
.error-message {
color: #ef4444;
background-color: #fee2e2;
border: 1px solid #fecaca;
padding: 1rem;
margin-top: 1rem;
border-radius: 4px;
font-weight: bold;
display: block;
width: fit-content;
line-height: 1.6;
}
アプリ完成
ファイル生成
# Dockerfileを生成
devbox generate dockerfile
# .devcontainer/devcontainer.jsonを生成
devbox generate devcontainer
# .envrcを生成
devbox generate direnv
# devbox-readme.mdを生成
devbox generate readme









