
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、これは個人で違ってくると思いますが、開発者の皆さんはそれぞれ「お気に入りの開発ツール」みたいなものが決まっていると思います。
例えば、私の場合でしたらIDE
はほぼphpstorm
のみを使っていたりしますが、VSCode
も人気ですし、この辺は好みが分かれるところじゃないでしょうか。
そして、今回ご紹介するのがデータベースの操作がウェブ上でできる
adminer(あどまいなー)
のお話です。
adminer とはウェブ上からMySQL
などのデータベース管理ができるツールで、とてもシンプル&軽量なのが気に入ってずっと使っています。(PhpMyAdmin
のシンプル版といった位置づけですかね )
しかし、そんな大好きなadminer
でも「あー、これできたらいいのにな」という部分が出てきたりもします。
そこで
今回はadminer
のプラグインを独自につくる方法をご紹介したいと思います。
ぜひ何かの参考になりましたら嬉しいです。
「英語の発音アプリで
『あ、間違えた』って言ったら、
正解になったんですが…」
開発環境: adminer 4.8.1
目次 [非表示]
今回やりたいこと
いつも私が開発をしていると遭遇する「あー、これできたらいいのにな」は複数カラムを一気にコピーしたいときです。
例えば、以下のような流れです。
- 開発中に「あれ、あのカラム名なんだっけ
」となる
- adminer でチェックする
- 必要な複数カラムを選択反転させて「Ctrl + C」でクリップボードへコピーしようとする
- ・・・すると、以下のように型などの余計な情報が入ってくる
- カラム名だけコピペさせてほしい・・・
なので、今回はプラグインの作成手順として「カラム名を簡単にクリップボードにコピーできる機能」をテーマに話を進めていきます。
具体的な機能としては「チェックボックスが 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
のコードを見ると、echo
でHTML
タグを表示するなど昔やっていたようなコードだったのですごい懐かしかったです。
また、JavaScript
のコードも今回は一切フレームワークを使っていませんが、感想としては「JavaScript本体が進化していて、バニラでも相当使える
」というものでした。
また、CSS
は超最新のform-sizing:content
(テキストエリアの行数が増えたら自動的に高さを合わせてくれる機能)を使ってみましたが、まだChrome
が対応してないようでした。
ちなみに、adminer
は本番環境(ウェブ上に公開されるシステム)としては少し物足りないイメージもあるかもしれませんが、開発に使うならとても便利で、なにより1ファイルだけなのでインストールが簡単という大きなメリットがあります。
私も長くPhpMyAdmin
やその他ツールを使っていましたが(最近はIDE
にも同梱されてたりしますよね )、個人的な好みでは
adminer
が1番でした。
みなさんもご自身の1番を見つけてみてくださいね。
ではでは〜
「フレームワークの
私の1番は、やっぱり
Laravelですね」