【完全ガイド】Local + WordPress + 専用テーブルで作る本格備品管理システム

このガイドでは、プログラミング初心者〜中級者の方を対象に、自分のパソコンで「会社の備品の登録・編集・削除・一覧表示」ができるWebシステムを、基本から応用まで一気通貫で開発する手順を解説します。

このガイドで作るもの

  • システム: 品名、管理者、保管場所、利用状況を管理できる備品管理システム
  • 特徴: WordPressの投稿機能とは完全に分離された、備品専用のデータベーステーブルでデータを管理します。これにより、クリーンで高性能なデータ操作を実現します。
  • 技術:
    • Local: 簡単なWordPress開発環境
    • PHP: データベーステーブルと専用API(データの対話窓口)の作成
    • JavaScript: Webページ上の操作画面(UI)の構築

## ステップ1:開発環境の準備 (Localのインストール)

まず、開発の拠点となるWordPress環境を、自分のパソコンの中に簡単に構築できるツール「Local」を準備します。

  1. Localのダウンロード 公式サイト localwp.com にアクセスし、お使いのOS(Windows/Mac)用のインストーラーをダウンロードして、インストールします。
  2. 新しいサイトの作成
    • Localを起動し、左下の「+」ボタン(Create a new site)をクリックします。
    • サイト名(例: bihin-system)を入力し、「Continue」をクリックします。
    • 環境設定は「Preferred」のままでOKです。「Continue」をクリックします。
    • WordPressの管理者ユーザー名パスワード、メールアドレスを設定します。これらは後でログインに使うので、忘れないようにメモしておきましょう。「Add Site」をクリックすると、数分であなた専用のWordPressサイトがPC内に作成されます。
  3. サイトへのアクセス
    • サイトが作成されると、Localの画面右上に「WP Admin」「Open Site」というボタンが表示されます。
    • WP Admin: WordPressの管理画面にログインするためのボタンです。
    • Open Site: 実際に表示されるサイトを確認するためのボタンです。

これで、開発の土台となる環境準備は完了です。


## ステップ2:PHPで備品用の「データベーステーブル」と「専用API」を作る

ここが今回の核心です。プラグインに頼らず、PHPコードを書いて「データの器(データベーステーブル)」と「データの対話窓口(API)」の両方を作成します。

作業するファイル: この作業は、WordPressのテーマファイル内にある functions.php というファイルで行います。

  • ファイルの開き方: Localのサイト管理画面で、サイト名のすぐ下にあるパス(例: /Users/yourname/Local Sites/bihin-system/app/public/)をクリックしてフォルダを開きます。そこから wp-content > themes > twentytwentyfour (または有効化しているテーマ) の中にある functions.php をテキストエディタで開きます。
  • ⚠️ 注意: このファイルはWordPressの重要な設計図です。編集を間違えるとサイトが表示されなくなることがあるので、慎重に作業し、念のためファイルのコピーを取ってから始めましょう。

2.1 専用テーブルを作成するPHPコード

以下のPHPコードを、functions.php ファイルの一番下にコピー&ペーストしてください。 このコードは、WordPressのデータベースに wp_bihin_master という名前の備品専用テーブルがなければ作成する、という処理です。

/**
 * 備品管理用のカスタムテーブルを作成する
 */
function create_bihin_custom_table() {
    global $wpdb; // WordPressのデータベースを操作するためのグローバル変数
    $table_name = $wpdb->prefix . 'bihin_master'; // テーブル名(wp_bihin_master となる)
    $charset_collate = $wpdb->get_charset_collate();

    // テーブルがなければ作成するSQL文
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        hinmei text NOT NULL,
        kanrisha varchar(255) NOT NULL,
        hokan_basho text,
        riyou_joukyou varchar(55) DEFAULT '保管中' NOT NULL,
        updated_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    // WordPressのデータベースアップグレード用関数を読み込む
    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql ); // SQLを実行してテーブルの状態をチェック・更新
}
// WordPressのテーマが読み込まれた後に一度だけ実行する
add_action( 'after_setup_theme', 'create_bihin_custom_table' );

コードを貼り付けたらファイルを保存し、一度WordPressサイトを表示してください。 これでデータベース内にテーブルが作成されます。

2.2 専用APIを作成するPHPコード

次に、作成したテーブルとJavaScriptが対話するための「専用API」を作ります。このコードも、先ほどのコードの下に続けて functions.php に貼り付けてください。

  • GET /items : 備品一覧を取得
  • POST /items : 新しい備品を登録
  • POST /items/{ID} : 既存の備品を更新
  • DELETE /items/{ID} : 備品を削除
/**
 * 備品管理用のカスタムAPIエンドポイントを作成する
 */
add_action( 'rest_api_init', function () {
    $namespace = 'bihin-api/v1'; // APIのグループ名

    // 備品一覧を取得 (GET)
    register_rest_route( $namespace, '/items', [
        'methods'  => 'GET',
        'callback' => 'get_all_bihin_items',
        'permission_callback' => '__return_true', // 誰でも閲覧可能
    ]);

    // 備品を新規登録 (POST)
    register_rest_route( $namespace, '/items', [
        'methods'  => 'POST',
        'callback' => 'create_bihin_item',
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' ); // ログインユーザーのみ許可
        },
    ]);

    // 備品を更新 (POST)
    register_rest_route( $namespace, '/items/(?P<id>\d+)', [
        'methods'  => 'POST',
        'callback' => 'update_bihin_item',
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
    ]);

    // 備品を削除 (DELETE)
    register_rest_route( $namespace, '/items/(?P<id>\d+)', [
        'methods'  => 'DELETE',
        'callback' => 'delete_bihin_item',
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
    ]);
});

// GET /items の処理
function get_all_bihin_items() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'bihin_master';
    $results = $wpdb->get_results( "SELECT * FROM $table_name ORDER BY id DESC" );
    return new WP_REST_Response( $results, 200 );
}

// POST /items の処理
function create_bihin_item( $request ) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'bihin_master';
    $params = $request->get_json_params(); // 送信されたデータを取得

    $wpdb->insert( $table_name, [
        'hinmei'        => sanitize_text_field( $params['hinmei'] ),
        'kanrisha'      => sanitize_text_field( $params['kanrisha'] ),
        'hokan_basho'   => sanitize_text_field( $params['hokan_basho'] ),
        'riyou_joukyou' => sanitize_text_field( $params['riyou_joukyou'] ),
        'updated_at'    => current_time( 'mysql' ),
    ]);
    return new WP_REST_Response( ['message' => '登録しました'], 201 );
}

// POST /items/{id} の処理
function update_bihin_item( $request ) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'bihin_master';
    $id = (int) $request['id'];
    $params = $request->get_json_params();

    $wpdb->update( $table_name, [
        'hinmei'        => sanitize_text_field( $params['hinmei'] ),
        'kanrisha'      => sanitize_text_field( $params['kanrisha'] ),
        'hokan_basho'   => sanitize_text_field( $params['hokan_basho'] ),
        'riyou_joukyou' => sanitize_text_field( $params['riyou_joukyou'] ),
        'updated_at'    => current_time( 'mysql' ),
    ], ['id' => $id] );
    return new WP_REST_Response( ['message' => '更新しました'], 200 );
}

// DELETE /items/{id} の処理
function delete_bihin_item( $request ) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'bihin_master';
    $id = (int) $request['id'];
    
    $wpdb->delete( $table_name, ['id' => $id] );
    return new WP_REST_Response( ['message' => '削除しました'], 200 );
}

ここまでで、システムの裏側(バックエンド)は完成です。


## ステップ3:操作画面の作成 (フロントエンド)

次に、ユーザーがブラウザで操作する画面を作ります。この作業は2段階に分かれます。

3.1 functions.php:JavaScriptへ設定を渡す

まず、PHPからJavaScriptへ安全に情報を渡すための準備をします。 functions.phpファイルの一番下に、以下のコードを追加して保存してください。このコードが、ページのヘッダーに必要なJavaScript変数を自動的に埋め込んでくれます。

/**
 * フロントエンドのJavaScriptにAPI設定を渡す
 */
function my_script_vars() {
    // '備品管理システム'というタイトルの固定ページの時だけ、このスクリプトを出力する
    if ( is_page('備品管理システム') ) {
        ?>
        <script>
            // APIのURLと認証キーをJavaScriptのグローバル変数として定義
            const WPApiSettings = {
                root: "<?php echo esc_url_raw( rest_url() ); ?>",
                nonce: "<?php echo wp_create_nonce( 'wp_rest' ); ?>"
            };
            const API_ENDPOINT = WPApiSettings.root + 'bihin-api/v1/items';
        </script>
        <?php
    }
}
add_action( 'wp_head', 'my_script_vars' );

【ポイント】 is_page('備品管理システム') の部分は、次のステップで作成する固定ページのタイトルと完全に一致させてください。

3.2 固定ページ:HTMLとJavaScriptで画面を組み立てる

  1. 固定ページの作成 WordPress管理画面の左メニュー「固定ページ」>「新規追加」をクリックし、タイトルを「備品管理システム」にします。
  2. コードの貼り付け 本文の入力エリアで、右上の「+」ボタンから「カスタムHTML」ブロックを選択します。そして、以下のコードを全てコピーして貼り付けてください。
<div id="bihin-form-container">
    <h2 id="form-title">新しい備品を登録</h2>
    <form id="bihin-form">
        <input type="hidden" id="bihin-id" value="">
        <div class="form-group">
            <label for="hinmei">品名:</label>
            <input type="text" id="hinmei" required>
        </div>
        <div class="form-group">
            <label for="kanrisha">管理者:</label>
            <input type="text" id="kanrisha" required>
        </div>
        <div class="form-group">
            <label for="hokan-basho">保管場所:</label>
            <input type="text" id="hokan-basho" required>
        </div>
        <div class="form-group">
            <label for="riyou-joukyou">利用状況:</label>
            <select id="riyou-joukyou">
                <option value="保管中">保管中</option>
                <option value="貸出中">貸出中</option>
                <option value="修理・点検中">修理・点検中</option>
            </select>
        </div>
        <div class="form-actions">
            <button type="submit" id="submit-button">登録する</button>
            <button type="button" id="cancel-edit-button" style="display:none;">キャンセル</button>
        </div>
    </form>
</div>
<hr>
<h2>備品一覧</h2>
<table id="bihin-list-table">
    <thead>
        <tr>
            <th>品名</th>
            <th>管理者</th>
            <th>保管場所</th>
            <th>利用状況</th>
            <th>最終更新</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody id="bihin-list-body"></tbody>
</table>

<style>
    #bihin-form-container { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
    .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
    .form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
    .form-actions button { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; }
    #submit-button { background-color: #0073aa; color: white; } #cancel-edit-button { background-color: #999; color: white; }
    #bihin-list-table { width: 100%; border-collapse: collapse; }
    #bihin-list-table th, #bihin-list-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    #bihin-list-table th { background-color: #f2f2f2; }
    .edit-btn, .delete-btn { margin-right: 5px; padding: 5px 10px; cursor: pointer; color: white; border: none; border-radius: 3px; }
    .edit-btn { background-color: #2271b1; } .delete-btn { background-color: #d63638; }
</style>

<script>
document.addEventListener('DOMContentLoaded', function() {
    // API設定は、functions.phpからwp_head経由で提供されます

    const form = document.getElementById('bihin-form');
    const formTitle = document.getElementById('form-title');
    const bihinIdField = document.getElementById('bihin-id');
    const hinmeiField = document.getElementById('hinmei');
    const kanrishaField = document.getElementById('kanrisha');
    const hokanBashoField = document.getElementById('hokan-basho');
    const riyouJoukyouField = document.getElementById('riyou-joukyou');
    const submitButton = document.getElementById('submit-button');
    const cancelEditButton = document.getElementById('cancel-edit-button');
    const bihinListBody = document.getElementById('bihin-list-body');

    // ■ 備品リストを取得して表示
    async function fetchBihinList() {
        bihinListBody.innerHTML = '<tr><td colspan="6">読み込み中...</td></tr>';
        try {
            const response = await fetch(API_ENDPOINT);
            if (!response.ok) throw new Error('データ取得失敗');
            const bihins = await response.json();
            
            bihinListBody.innerHTML = '';
            bihins.forEach(bihin => {
                const row = `
                    <tr data-id="${bihin.id}">
                        <td>${bihin.hinmei}</td>
                        <td>${bihin.kanrisha}</td>
                        <td>${bihin.hokan_basho}</td>
                        <td>${bihin.riyou_joukyou}</td>
                        <td>${new Date(bihin.updated_at).toLocaleString('ja-JP')}</td>
                        <td>
                            <button class="edit-btn" data-id="${bihin.id}">編集</button>
                            <button class="delete-btn" data-id="${bihin.id}">削除</button>
                        </td>
                    </tr>
                `;
                bihinListBody.insertAdjacentHTML('beforeend', row);
            });
        } catch (error) {
            bihinListBody.innerHTML = `<tr><td colspan="6">${error.message}</td></tr>`;
        }
    }

    // ■ フォーム送信時の処理
    form.addEventListener('submit', async function(event) {
        event.preventDefault();
        const bihinId = bihinIdField.value;
        const isEditing = bihinId !== '';

        const data = {
            hinmei: hinmeiField.value,
            kanrisha: kanrishaField.value,
            hokan_basho: hokanBashoField.value,
            riyou_joukyou: riyouJoukyouField.value
        };
        
        const url = isEditing ? `${API_ENDPOINT}/${bihinId}` : API_ENDPOINT;

        try {
            const response = await fetch(url, {
                method: 'POST', // 更新もPOSTで行う
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': WPApiSettings.nonce
                },
                body: JSON.stringify(data)
            });
            if (!response.ok) throw new Error('処理に失敗しました。');
            const result = await response.json();
            alert(result.message);
            resetForm();
            fetchBihinList();
        } catch (error) {
            alert(error.message);
        }
    });

    // ■ 編集・削除ボタンの処理
    bihinListBody.addEventListener('click', async function(event) {
        const target = event.target;
        const id = target.dataset.id;

        // 削除
        if (target.classList.contains('delete-btn')) {
            if (!confirm('本当にこの備品を削除しますか?')) return;
            try {
                const response = await fetch(`${API_ENDPOINT}/${id}`, {
                    method: 'DELETE',
                    headers: { 'X-WP-Nonce': WPApiSettings.nonce }
                });
                if (!response.ok) throw new Error('削除失敗');
                const result = await response.json();
                alert(result.message);
                fetchBihinList();
            } catch(error) {
                alert(error.message);
            }
        }
        // 編集
        if (target.classList.contains('edit-btn')) {
            const row = target.closest('tr');
            bihinIdField.value = id;
            hinmeiField.value = row.cells[0].textContent;
            kanrishaField.value = row.cells[1].textContent;
            hokanBashoField.value = row.cells[2].textContent;
            riyouJoukyouField.value = row.cells[3].textContent;
            formTitle.textContent = '備品を編集';
            submitButton.textContent = '更新する';
            cancelEditButton.style.display = 'inline-block';
            window.scrollTo(0, 0);
        }
    });
    
    // ■ キャンセル処理
    cancelEditButton.addEventListener('click', resetForm);
    function resetForm() {
        form.reset();
        bihinIdField.value = '';
        formTitle.textContent = '新しい備品を登録';
        submitButton.textContent = '登録する';
        cancelEditButton.style.display = 'none';
    }

    // ★ 初期表示
    fetchBihinList();
});
</script>
  1. 保存して確認 右上の「公開」ボタンをクリックし、「ページを表示」して動作を確認します。フォームが表示され、備品の登録、編集、削除ができれば成功です。

## ステップ4:他のPCやスマホからアクセスする (LAN内共有)

【参考】以下の方法は動画と異なります。

Localには、作成したサイトを一時的にインターネットに公開し、同じネットワーク(LAN)にいる他の人や自分のスマホからアクセスできるようにする「Live Link」という便利な機能があります。

  1. Live Linkを有効にする Localのサイト管理画面の下部にある「Live Link」という文字の横の「Enable」ボタンをクリックします。
  2. URLを共有 有効になると、xxxxx.loca.lt のようなURLが発行されます。このURLに、同じWi-Fiに接続しているPCやスマートフォンのブラウザからアクセスしてみてください。
    • 注意: この機能は、あなたがLocalを起動している間だけ有効です。

これで、部署やチーム内で簡易的にシステムを共有することも可能です。


## まとめと次のステップ

お疲れ様でした!これで、あなただけの本格的な備品管理システムが完成しました。

専用テーブル方式のメリット:

  • WordPressの投稿機能とは独立しているため、データ構造を完全に自由に設計できる。
  • データが純粋なため、将来的に大量のデータを扱う場合でもパフォーマンス上有利になることがある。

この経験は、単なるWordPressユーザーから、一歩進んだ開発者になるための重要なステップです。ここからさらに、検索機能CSVエクスポート機能などをPHPで追加していくことで、より高度なシステム開発に挑戦できます。