3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Devbox触ってみる

Last updated at Posted at 2025-12-31

内部で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...」からインストール済のディストリビューションの中から選択して接続します。)

wsl.png

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/ユーザー名/」を開きます。

openfolder.png

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が作成されます。

/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を実行すると下記の表示になり、ターミナルとしては使用できなくなります。
devbox_services_up.png
(終了はCtrl+C)

サービスを起動してから http://localhost:8081 にアクセスすると、/home/ユーザー名/projects/my-app/devbox.d/web/index.htmlが開きます。

Nginxの動作確認

確認用のphpファイルを作成します。

/home/ユーザー名/projects/my-app/devbox.d/web/index.php
 <?php phpinfo(); ?>

http://localhost:8081/index.php を開いても<?php phpinfo(); ?>がそのまま表示される場合はnginx.confを変更する必要があります。
(nginx.templateから生成するので直接 nginx.conf を編集しない)

/home/ユーザー名/projects/my-app/devbox.d/nginx/nginx.template
 # 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;
     }
 }

↓ 変更

/home/ユーザー名/projects/my-app/devbox.d/nginx/nginx.template
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が表示されます。

phpinfo_nomozaic.png

データベースの準備

データベース作成

init.sqlを新規作成します。

/home/ユーザー名/projects/my-app/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が表示されます。

phpmyadmin_login.png

MariaDBへのログイン(ユーザー:root、パスワードなし)しようとすると下記のように表示されてログインできない場合

パスワードなしログインは設定 (AllowNoPassword 参照) によって禁止されています

phpmyadmin_login_fail.png

パスワードなしログインできるように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の変更

/home/ユーザー名/projects/my-app/devbox.d/web/phpmyadmin/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、パスワードなし)

今度はログインできて、データベースが作成されていることを確認できます。

schema.png

table.png

アプリの作成

ポケモンのデータを登録、更新、削除ができる簡単なアプリを作成してみます。

directory.png

/home/ユーザー名/projects/my-app/devbox.d/web/img/icon_delete.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="#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>
/home/ユーザー名/projects/my-app/devbox.d/web/img/icon_edit.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>
/home/ユーザー名/projects/my-app/devbox.d/web/create.php
<?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>


/home/ユーザー名/projects/my-app/devbox.d/web/delete.php
<?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();
?>


/home/ユーザー名/projects/my-app/devbox.d/web/edit.php
<?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>

/home/ユーザー名/projects/my-app/devbox.d/web/index.php
<?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();
?>
/home/ユーザー名/projects/my-app/devbox.d/web/script.js
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();
      }
    });
  }
});


/home/ユーザー名/projects/my-app/devbox.d/web/style.css
@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;
}

アプリ完成

スクリーンショット 2025-12-31 035343.png

スクリーンショット 2025-12-31 152043.png

ファイル生成

# Dockerfileを生成
devbox generate dockerfile 

# .devcontainer/devcontainer.jsonを生成
devbox generate devcontainer

# .envrcを生成
devbox generate direnv

# devbox-readme.mdを生成
devbox generate readme
3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?