はじめに
こんにちは。ニフティ株式会社入会システムチームの高畑です。 最近マスタデータ管理のために、Adminer Editorというアプリケーションを試してみましたので紹介します。
背景
システム設定やマスタデータをDB上のテーブルで管理している際に、操作ミスによっては影響が大きいためSQLを直接操作したくありません。そのため、社内運用ツールを作成することもありますが、シンプルなデータであれば汎用的なツールで十分な場合もあると思います。 汎用的なツールとしてAdminer Editorというアプリケーションを検証しました。本記事では実際に使う上で必要な機能として以下を挙げてカスタマイズ例と動作確認をしています。
- データ操作以外のテーブル作成、変更操作をしたくない
 - 不要なテーブルは見えないようにしたい
 - 不要な列は見えないようにしたい
 - 誰がデータを操作したか記録を残したい
 - アクセス制限をしたい
 
Adminer Editorとは
Adminer EditorとはPHP製のDBデータ編集ツールです。SQLを操作せずに画面上の操作だけでテーブル上のデータ修正ができます。類似のツールにAdminerがありますが、Adminerと比較するとテーブル作成や削除といったDDLが制限されているためDB管理者というよりはDB利用者のためのツールになります。 また、MySQL、PostgreSQL、OracleDB、Microsoft SQL Server等さまざまなRDBMSに対応しています。 ツール本体は単一のPHPファイルとなっており導入しやすく、クラスを継承してインスタンスメソッドをオーバーライドすることでPHP触ることができれば簡単にカスタマイズできます。 今回は、導入の手軽さと利用できるRDBMSの豊富さから検証してみました。
デモ
ソースコードと実行環境はDockerを用意しています。MySQLが提供しているworld databaseというサンプルデータをMySQLにインポートして操作していますので、実際に触ってみたいという方はREADMEを参考に構築してみてください。
Adminer Editorの設定とカスタマイズ方法
導入に必要なのはAdminer Editor本体と設定ファイルの2つだけです。設定ファイルにはDBのホスト名、ユーザー名、パスワード等の基本設定の他、データ挿入、変更、削除等の操作をした際に呼ばれるメソッドのオーバーライドをします。 ここでは設定ファイルをworld.php、本体をeditor.phpとしています。world.phpにアクセスするとAdminer Editorが利用できます。さらにadminer.cssを設置することでCSSを読み込み反映してくれます。 設定方法は以下ページを参考にしています。
| 
					 1 2 3 4 5  | 
						. ├── adminer.css ├── editor.php └── world.php  | 
					
それぞれの機能実現方法
前述したカスタム方法により機能追加しています。
不要なテーブルを非表示にしたい
tableNameをオーバーライドすることでどのテーブルを表示させるか制御できます。非表示にしたいテーブル名なら空文字を返します。 ここではテーブル名が country  を非表示にしています。
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						function tableName($tableStatus) {   // countryテーブルを非表示   $ignored_list = ['country'];   if (in_array($tableStatus["Name"], $ignored_list, true)) {     return '';   }   return parent::tableName($tableStatus); }  | 
					
不要なテーブルの列を非表示にしたい
同様に非表示にしたい列名は空文字を返すと非表示にできます。ここでは、ID 列を非表示にしています。
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						function fieldName($field, $order = 0) {   // ID列を非表示   $ignored_list = ['ID'];   if (in_array($field["field"], $ignored_list, true)) {     return '';   }   return parent::fieldName($field, $order = 0); }  | 
					
操作記録を残す
必要なファイルが増えてしまいますが、利便性を考えてPHPのログライブラリであるMonologを導入します。追加情報としてIPアドレス、X-Forwarded-For、ログインユーザーを出力することで誰が操作したかわかるようにしています。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | 
						class AdminerSoftware extends Adminer   {     private $logger;     function __construct()     {       $this->logger = $this->getLogger('php://stdout', Level::Debug);     }     function getLogger($filename, $level = Level::Debug)     {       $logger = new Logger('adminer');       $handler = new StreamHandler($filename, $level);       $handler->setFormatter(new JsonFormatter());       $logger->pushHandler($handler);       $logger->pushProcessor(function ($record) {         $record['extra']['remote_user'] = $_SERVER['REMOTE_USER'];         $record['extra']['remote_ip'] = $_SERVER['REMOTE_ADDR'];         $record['extra']['xff'] = $_SERVER['X-Forwarded-For'];         return $record;       });       return $logger;     } ...  | 
					
SELECT、INSERT、 UPDATE、DELETEはそれぞれ selectQuery 、 messageQuery メソッドがAdminer Editor本体から呼ばれるため、用意したロガーオブジェクトでログ出力します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						function selectQuery($query, $time, $failed = false) {   $output_query = str_replace("\\n", " ", $query);   $this->logger->debug($output_query); } function messageQuery($query, $time, $failed = false) {   $output_query = str_replace("\\n", " ", $query);   $this->logger->info($output_query); }  | 
					
Monolog導入後は以下のようなファイル構成になりました。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13  | 
						. |-- composer.json |-- composer.lock |-- public |   |-- adminer.css |   |-- editor.php |   `-- world.php `-- vendor     |-- autoload.php     |-- composer     |-- monolog     `-- psr  | 
					
アカウント認証する
ログイン認証は login メソッドが担当しています。ユーザ名、パスワードを入力すると引数として渡されますので、簡易的に認証したいのであれば、それらを比較するだけで認証できます。 実際にはIPアドレス制限やリバースプロキシ導入する、パスワードをハッシュ化しておく等対策が必要に思います。 以下はIPアドレスをもとにしてアクセス制限をかける例と、簡易的な認証の例です。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14  | 
						<?php $permit_ip = ["x.x.x.x", "y.y.y.y"]; if (!in_array($_SERVER['REMOTE_ADDR'], $permit_ip, true)) { } ... class AdminerSoftware extends Adminer   {    ...    function login($login, $password)    {        return $login === 'xxxx' && $password === 'yyyy';    }  | 
					
実際に触ってみる
上記の設定後、ローカル環境で動作確認を実施します。
①ログイン画面
  
②テーブル一覧(countryが非表示になっています)。

③データ一覧で神奈川県で検索してみます(ID列が非表示になっています)。
  
④神奈川県に人口2000人の町TestCityを追加してみます。追加時もID列は表示されていません。

⑤無事TestCityが追加されました。

⑥編集リンクを押すと修正、削除ができます。

⑦ログからそれぞれの操作を確認できました。
| 
					 1 2 3 4  | 
						adminer-editor-customized-adminer-1  | {"message":"INSERT INTO `city` (`Name`, `CountryCode`, `District`, `Population`) VALUES ('TestCity', 'JPN', 'Kanagawa', '2000');","context":{},"level":200,"level_name":"INFO","channel":"adminer","datetime":"2023-06-16T01:46:58.380121+00:00","extra":{"remote_user":null,"remote_ip":"xxxx","xff":null}} adminer-editor-customized-adminer-1  | {"message":"UPDATE `city` SET `Name` = 'TestCity', `CountryCode` = 'JPN', `District` = 'Kanagawa', `Population` = '3000' WHERE `ID` = '4081';","context":{},"level":200,"level_name":"INFO","channel":"adminer","datetime":"2023-06-16T01:48:36.217265+00:00","extra":{"remote_user":null,"remote_ip":"xxxx","xff":null}} adminer-editor-customized-adminer-1  | {"message":"DELETE FROM `city` WHERE `ID` = '4081';","context":{},"level":200,"level_name":"INFO","channel":"adminer","datetime":"2023-06-16T01:50:09.335932+00:00","extra":{"remote_user":null,"remote_ip":"xxxx","xff":null}}  | 
					
おわりに
本記事では、Adminer Editorをカスタマイズすることで運用上必要な機能を追加できることを検証しました。参考になれば幸いです。
            

