adminer の独自プラグインをつくる方法

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、これは個人で違ってくると思いますが、開発者の皆さんはそれぞれ「お気に入りの開発ツール」みたいなものが決まっていると思います。

例えば、私の場合でしたらIDEはほぼphpstormのみを使っていたりしますが、VSCodeも人気ですし、この辺は好みが分かれるところじゃないでしょうか。

そして、今回ご紹介するのがデータベースの操作がウェブ上でできる

adminer(あどまいなー)

のお話です。

adminer とはウェブ上からMySQLなどのデータベース管理ができるツールで、とてもシンプル&軽量なのが気に入ってずっと使っています。(PhpMyAdminのシンプル版といった位置づけですかね 🤔)

しかし、そんな大好きなadminerでも「あー、これできたらいいのにな」という部分が出てきたりもします。

そこで❗

今回はadminerのプラグインを独自につくる方法をご紹介したいと思います。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「英語の発音アプリで
『あ、間違えた』って言ったら、
正解になったんですが…😅」

開発環境: adminer 4.8.1

今回やりたいこと

いつも私が開発をしていると遭遇する「あー、これできたらいいのにな」は複数カラムを一気にコピーしたいときです。

例えば、以下のような流れです。

  1. 開発中に「あれ、あのカラム名なんだっけ🤔」となる
  2. adminer でチェックする
  3. 必要な複数カラムを選択反転させて「Ctrl + C」でクリップボードへコピーしようとする
  4. ・・・すると、以下のように型などの余計な情報が入ってくる
  5. カラム名だけコピペさせてほしい・・・

なので、今回はプラグインの作成手順として「カラム名を簡単にクリップボードにコピーできる機能」をテーマに話を進めていきます。

具体的な機能としては「チェックボックスが ON になったカラムだけ」自動コピーできるようにします。

では、今回も楽しんでやっていきましょう❗

開発環境を整える

まずは開発環境です。

というのも、adminerはなんと「たった1ファイルだけ」で動くようになっていて、中身はがっちりビルドされてしまっており、到底変更できるものではありません。

そのため、adminer のウェブサイトへ行き、開発用ファイル(Current development version)をダウンロードします。

すると、zipファイルがダウンロードされるので、これをPHPでアクセスできるフォルダへ移動すれば準備は完了です。

※ 私の場合は、nginxの設定を変更し、adminer-dev.testでアクセスできるようにしました。

プラグインをつくる

今回はもともと存在している「テーブル構造」のページに新しい機能を追加したいので、plugins/table-structure.phpというファイルをコピーしてplugins/table-structure-with-copy.phpとして使用します。

中身は以下のようにしてください。

plugins/table-structure-with-copy.php

<?php

/** Expanded table structure output
 * @link https://www.adminer.org/plugins/#use
 * @author Matthew Gamble, https://www.matthewgamble.net/
 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License, version 2 (one or other)
 *
 * Modified by Sukohi Kuhoh, https://blog.capilano-fw.com/
 */
class AdminerTableStructureWithCopy {

    /** Print table structure in tabular format
     * @param array data about individual fields
     * @return bool
     */
    function tableStructurePrint($fields) {
        $table_names = [];
        $i = 0;
        echo "<div class='scrollable'>\n";
        echo "<table cellspacing='0' class='nowrap'>\n";
        echo "<thead><tr><th><input class=\"table-structure-clipboard-checkboxes\" type=\"checkbox\" value=\"-1\"><th>" . lang('Column') ."<th>" . lang('Type') . "<th>" . lang('Nullable') . "<th>" . lang('Default') . (support("comment") ? "<th>" . lang('Comment') : "") . "</thead>\n";
        foreach ($fields as $field) {
            echo "<tr" . odd() . ">";
            echo "<th style=\"text-align:center;\"><input class=\"table-structure-clipboard-checkboxes\" type=\"checkbox\" value=\"". $i ."\">";
            echo "<th>". h($field["field"]) . ($field["primary"] ? " (PRIMARY)" : "");
            echo "<td><span>" . h($field["full_type"]) . "</span>";
            echo ($field["auto_increment"] ? " <i>" . lang('Auto Increment') . "</i>" : "");
            echo ($field["collation"] ? " <i>" . h($field["collation"]) . "</i>" : "");
            echo "<td>" . ($field["null"] ? lang('Yes') : lang('No'));
            echo "<td>" . h($field["default"]);
            echo (support("comment") ? "<td>" . h($field["comment"]) : "");
            echo "\n";
            $table_names[] = $field["field"];
            $i++;
        }
        echo "</table>\n";
        echo "</div>\n";
        echo "<div id=\"table-structure-copying-text-container\" style=\"padding:.5rem 30px 0 0;display:none;\">";
        echo "<textarea id=\"table-structure-copying-text\" style=\"width:100%;form-sizing:content;min-height:120px;padding:5px;border:1px solid #999;\"></textarea>";
        echo "<div style=\"margin-top:5px;\"><button id=\"table-structure-copying-button\" style=\"padding:5px 10px;\">Copy to clipboard</button></div>";
        echo "<div id=\"table-name-copy-result\" style=\"color:#0e9300;padding:10px 0 0 0;display:none;font-weight:bold;\"></div>\n";
        echo "</div>";
        echo $this->getScriptCode($table_names);
        return true;
    }

    function getScriptCode($table_names) {

        $nonce = nonce();
        $column_names_json = json_encode($table_names);

        return <<<EOT
            <script{$nonce}>
                
                const columnNames = {$column_names_json};
                let checkedIndexes = [];
                let timer = null;
                
                // Methods
                const initTableStructure = () => {
                
                    const checkboxes = getAllCheckboxElements();
                    
                    [].forEach.call(checkboxes, checkbox => {
                    
                        checkbox.addEventListener('change', onTableStructureCheckboxClick);
                        
                    });
                    
                    const button = document.getElementById('table-structure-copying-button');
                    button.addEventListener('click', onTableStructureButtonClick);
                
                };
                
                // Events
                const onTableStructureCheckboxClick = e => {
                
                    const checkboxIndex = Number(e.target.value);
                    
                    if(checkboxIndex === -1) { // Check all
                    
                        const checkboxes = getAllCheckboxElements();
                        let allIndexes = [];
                        
                        [].forEach.call(checkboxes, checkbox => {
                        
                            checkbox.checked = e.target.checked;
                            const index = Number(checkbox.value);
                            
                            if(index !== -1) {
                            
                                allIndexes.push(index);
                            
                            }
                        
                        });
                        
                        if(e.target.checked) {
                        
                            checkedIndexes = allIndexes;
                        
                        } else {
                        
                            checkedIndexes = [];
                        
                        }
                    
                    } else if(checkedIndexes.includes(checkboxIndex)) {
                    
                        const currentIndex = checkedIndexes.indexOf(checkboxIndex);
                        checkedIndexes.splice(currentIndex, 1);
                    
                    } else {
                    
                        checkedIndexes.push(checkboxIndex);
                    
                    }
                    
                    checkedIndexes.sort((a, b) => {
                    
                        return a - b;
                    
                    });
                    
                    const text = getTableNameText();
                    const container = document.querySelector('#table-structure-copying-text-container');
                    
                    if(text) {
                    
                        container.style.display = 'block';
                    
                    } else {
                    
                        container.style.display = 'none';
                    
                    }
                    
                    const textarea = document.getElementById('table-structure-copying-text');
                    textarea.value = text;
                
                };
                onTableStructureButtonClick = e => {
                
                    const textarea = document.getElementById('table-structure-copying-text');
                    const copyingText = textarea.value;
                    setTextToClipboard(copyingText);
                    setCopyResult();
                
                };
                
                // Getter
                const getAllCheckboxElements = () => {
                
                    return document.querySelectorAll('.table-structure-clipboard-checkboxes');
                
                };
                const getTableNameText = () => {
                
                    const texts = [];
                    
                    checkedIndexes.forEach(checkedIndex => {
                    
                        texts.push(columnNames[checkedIndex]);
                    
                    });
                    
                    return texts.map(text => {
                    
                        return text + ',';
                    
                    }).join('\\n');
                
                };
                
                // Setter
                const setTextToClipboard = (text) => {
                
                    const el = document.createElement('textarea');
                    el.value = text;
                    document.body.appendChild(el);
                    el.select();
                    document.execCommand('copy');
                    document.body.removeChild(el);
                
                };
                const setCopyResult = () => {
                
                    const el = document.getElementById('table-name-copy-result');
                    el.innerHTML = 'Column name(s) copied to clipboard.';
                    el.style.display = 'block';
                    
                    if(timer) {
                    
                        clearTimeout(timer);
                    
                    }
                    
                    timer = setTimeout(() => {
                    
                        el.innerHTML = '';
                        el.style.display = 'none';
                    
                    }, 3000);
                
                };
                
                // Main
                document.addEventListener('DOMContentLoaded', initTableStructure);
            
            </script>
        EOT;

    }
}

※ なお、adminerのライセンスははApache License もしくは GPL 2(2023.11.2 現在)なので注意してください。

ちなみに、プラグイン用にクラスをつくり、それを読み込むと元々存在していたメソッドを上書きできるようになっています。(条件は、返り値がnull以外で、さらに特定のメソッドは上書きできなくなっています)

つまり、今回の例で言うと「tableStructurePrint()」というメソッドを新しく上書きしているというわけです。

もしadminerのメソッド一覧をチェックしたい場合は以下のページからできますよ👍

📝 参考ページ: Adminer – Extensions

プラグインを有効にする

では、先ほどつくったプラグインを有効にしておきます。

ありがたい話で、zipから展開したファイルの中にadminer/plugin.phpというファイルがあり、これを使うと簡単にプラグインを有効にすることができます。

<?php
function adminer_object() {
    // required to run any plugin
    include_once "../plugins/plugin.php";
    
    // autoloader
    foreach (glob("../plugins/*.php") as $filename) {
       include_once $filename;
    }
    // enable extra drivers just by including them
    //~ include "../plugins/drivers/simpledb.php";
    
    $plugins = array(

       // 省略

       new AdminerTableStructureWithCopy, // 👈 ここを追加しました
    );
    
// 以下省略

これだけで、プラグインは有効になります。

では、一旦この状態でブラウザから確認してみましょう。「https://******/adminer/plugin.php」へアクセスします。

すると、以下のようにログインフォームが表示されるのでログインして適当なデータベース内のテーブル構造を表示してみてください。

すると、以下のようにこれまでなかったチェックボックスが表示されているのがわかります。

では、開発用のプラグインが有効になっているようですので、これ以降のテストは後にして本番環境にこのプラグインをセットしていきましょう。

独自プラグインを(本番の)adminer へ適用する

まずadminerがどこにインストールされているのかを確認しておいてください。

例えば/usr/share/adminerにインストールされている場合は以下のようなファイル構成にします。

/usr/share/adminer
 └ plugins/
  └ plugin.php
└ table-structure-with-copy.php

 └ adminer.php(👈 元からある adminer 本体)
└ index-with-plugins.php

※ 新しく作るのはpluginsフォルダとその中のファイル、そしてindex-with-plugins.phpになります。

そして、それぞれのファイルの中身は以下のようにしてください。

/usr/share/adminer/index-with-plugins.php

function adminer_object() {

    include_once "./plugins/plugin.php";

    foreach (glob("./plugins/*.php") as $filename) {
        include_once $filename;
    }

    $plugins = array(
        new AdminerTableStructureWithCopy,
    );

    return new AdminerPlugin($plugins);

}

include "./adminer.php"; // もし latest.php を使っている場合はそちらに変更してください。

/usr/share/adminer/plugins/table-structure-with-copy.php

先ほど作ったプラグイン「AdminerTableStructureWithCopy」の中身をそのままセットしてください。

/usr/share/adminer/plugins/table-structure-with-copy.php

zipファイルの中にあるplugins/plugin.phpの中身と同じでOKです。

はい❗これで、作業はすべて終了しました。
お疲れ様でした😄✨

テストしてみる

では、実際にテストしてみましょう❗

まずブラウザで「https://******/index-with-plugins.php」へアクセスしてログインしてください。

そして、適当なテーブル構造を表示すると・・・・・・

はい!
開発中でも確認したチェックボックスが表示されています。

では、まずは「user_id」のチェックボックスをONにしてみましょう。

どうなるでしょうか・・・・・・

はい!
テキストエリアの中にuser_idが入っていて、さらにコピーボタンも表示になりました。

では、複数のカラム選択と、全体選択も試してみましょう。

(複数選択の場合)

(全体選択の場合)

はい!
うまくいきました。

では、最後に「Copy to clipboard」ボタンをクリックしてコピペできるかチェックしておきましょう。

うまくいくでしょうか・・・・・・

(実際のコピペ)

はい❗
コピーできたよ」メッセージが表示されました。

そして、実際のコピペは以下になります。
↓↓↓

id,
user_id,
message,
created_at,
updated_at,

すべて成功です😄✨

おまけ: 便利な「AdminerTablesFilter」もおすすめです

adminerにはすでにたくさんのプラグインが存在していて、「これ初期状態で有効にしてもいいんじゃない??🤔」という便利なものもあったりします。

そして、その中でもおすすめなのが「AdminerTablesFilter」です。

その名のごとくテーブル名でフィルターをかけることができるものですが実際の例で言うと、通常ならadminerのページ左メニューにはテーブル一覧が表示されるだけで特別な機能があるわけではありません。

しかし、開発が進んでテーブルの数が多くなってしまった場合はどうでしょうか。

そうです。めちゃくちゃ目的のテーブルを探すのがめんどうになりますよね。(私はよくCtrl + Fで検索しています)

AdminerTablesFilterは、ここに検索ボックスを表示しキーワードに関連するテーブルだけ表示してくれるようになります。

そこで❗
せっかくですので、このフィルターも使えるようにしてみましょう。

手順としては先ほどと同じで/usr/share/adminer/pluginsフォルダの中にzipファイルの中にあったplugins/tables-filter.phpをコピーしてプラグインを有効にするだけです。

function adminer_object()
{

    include_once "./plugins/plugin.php";

    foreach (glob("./plugins/*.php") as $filename) {
        include_once $filename;
    }

    $plugins = array(
        new AdminerTablesFilter, // 👈 ここを追加
        new AdminerTableStructureWithCopy,
    );

    return new AdminerPlugin($plugins);

}

include "./adminer.php";

これでまた「より効率的な」開発ができるんじゃないでしょうか。

企業様へのご提案

もちろん入力チェック(バリデーション)がきちんとしていないといけない場合がほとんどでしょうが、中には「それほど気構えせずに使ってもいいシステム」もあったりすると思います。(例えばローカル環境のみのシステムなど)

そんな場合は、ウェブシステムを作成せずadminerを使ってデータ管理することも選択肢のひとつにしてもいいかと思います。

もしそんな場合はadminerのカスタマイズも可能ですので、そんな場合はぜひお問い合わせからご相談ください。

お待ちしております。😄✨

おわりに

ということで、今回はいつもとはちょっと違うadminerのプラグインをテーマにしてみました。

正直なところPHPのコードを見ると、echoHTMLタグを表示するなど昔やっていたようなコードだったのですごい懐かしかったです。

また、JavaScriptのコードも今回は一切フレームワークを使っていませんが、感想としては「JavaScript本体が進化していて、バニラでも相当使える❗」というものでした。

また、CSSは超最新のform-sizing:content(テキストエリアの行数が増えたら自動的に高さを合わせてくれる機能)を使ってみましたが、まだChromeが対応してないようでした。😭

ちなみに、adminerは本番環境(ウェブ上に公開されるシステム)としては少し物足りないイメージもあるかもしれませんが、開発に使うならとても便利で、なにより1ファイルだけなのでインストールが簡単という大きなメリットがあります。

私も長くPhpMyAdminやその他ツールを使っていましたが(最近はIDEにも同梱されてたりしますよね 🤔)、個人的な好みではadminerが1番でした。

みなさんもご自身の1番を見つけてみてくださいね。

ではでは〜❗

フレームワークの
私の1番は、やっぱり
Laravelですね👍」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ