Reading the repo
Reading the repo
How to install on Linux
Database management in a single PHP file
Saved setup from 3 h ago
Will it work on my device?
docs/developing.mdJakub Vrána
Adminer loads a database driver based on a URL parameter (e.g., pgsql=).
The drivers live in adminer/drivers/ and plugins/drivers/.
The driver consists of the class Driver and a set of functions that ideally belong in Driver but remain separate due to historical reasons.
A driver also creates the Db class based on available PHP extensions.
There is no DriverMysql or DbMysqlPdo; there is always up to one Driver and one Db.
If the URL contains username=, Adminer attempts to authenticate that user.
If authentication fails, a login form is displayed at the same URL, and POST data is stored in hidden form fields.
If the user authenticates using the same credentials, the action is performed.
All state-changing actions (primarily data modifications, as well as language change or logout) are performed using POST with a CSRF token present.
Adminer sets cookies as SameSite which adds a second protection but not for vulnerabilities on the same site.
As a third protection, Adminer rejects POST requests where the browser sends a cross-site Sec-Fetch-Site header.
If a POST action succeeds, Adminer redirects the browser to a GET request to prevent accidental re-submission.
An unsuccessful POST displays the same page with pre-filled form fields.
Refreshing the page attempts the action again, which is useful when errors were resolved in another browser tab.
Then, the request is routed based on other URL parameters.
For example, if the URL contains indexes=, then adminer/indexes.inc.php is loaded.
The table name is extracted from this parameter, resulting in simpler URLs (e.g., indexes=customers instead of action=indexes&table=customers).
The PHP session is stopped before rendering begins.
This prevents modifying $_SESSION later in the code but allows multiple Adminer pages to be opened simultaneously, even if one has a long-running query.
Database identifiers, such as column names, can be arbitrary, so they are never transferred in URLs or POST requests directly.
They are always wrapped (e.g., fields[col]), which requires escaping the name as described in Escaping.
Adminer often checks for empty strings using $table != "" instead of !$table, since table names can be 0, and !$table would fail in such cases.
A driver announces which features it supports by support(), called with one of these keys:
check - CHECK constraints on the table structure pagecolumns - columns can be defined when creating and altering a tablecomment - comments of tables and columnsconcurrent_indexes - creating and dropping indexes concurrently (PostgreSQL)copy - copying a table including its datacursor - rows are paged by a cursor so the number of the following rows is unknown (Redis, SimpleDB)database - creating, altering and dropping databasesdeferrable - deferrable foreign keys (PostgreSQL)descidx - descending index columnsdrop_col - dropping a columndump - export in the SQL formatevent - scheduled events (MySQL)extension - installed extensions (PostgreSQL)fast_status - table_status() is cheap so the table list is printed at once instead of by a background requestindexes - listing and altering indexeskill - killing a processmaterializedview - materialized views (PostgreSQL)move_col - a column can be added in the middle of a tablepartial_indexes - index condition (PostgreSQL)privileges - the Privileges pageprocedure - procedures in addition to functionsprocesslist - the Process list pageroutine - stored functionsscheme - schemas inside a databasesequence - sequences (PostgreSQL)single_db - there is only one database so the user is taken to it and the database select is not printed (Elasticsearch)single_table - there is only one table in each database so the user is taken to it (Redis)sql - the SQL command page and running multiple queriesstatus - the Status pagetable - tables have a fixed structure, other drivers just select all columnstransaction_ddl - DDL is transactional so an object can be dropped and recreated atomicallytrigger - triggers of tablestype - user defined types (PostgreSQL)variables - the Variables pageview - creating, altering and dropping viewsview_trigger - triggers of views (SQLite, MS SQL)Other capabilities are declared by defining an optional driver function, detected by function_exists():
alter_table() - creating and altering tablesdb_status() - sizes of the whole database at once (SQLite)drop_sql() - the export drops all tables at the beginning (PostgreSQL)drop_tables() - dropping the selected tablesforeign_keys_sql() - the export adds foreign keys after creating all tablesmove_tables() - moving the selected tables to another databasetruncate_all_sql() - the export truncates all tables by a single command (PostgreSQL)truncate_tables() - truncating the selected tablesuse_schema_sql() - the export creates and selects the schema (PostgreSQL, MS SQL)There are 4 main classes: Driver, Db, Adminer and Plugins.
Adminer defines many functions which are namespaced to prevent collisions.
There are no global variables.
Some data is stored in static class variables.
These variables are minified during compilation into random strings, making them inaccessible externally (e.g., by plugins).
Plugins can access some of them using helper functions like Adminer\driver().
Adminer also defines constants in its namespace.
A key example is JUSH, which represents a syntax highlighting ID (e.g., pgsql for PostgreSQL).
Simple conditional checks may use JUSH, but for complex logic, methods in Driver are preferred.
Adminer is highly conservative regarding PHP version requirements.
Source codes require PHP 7.4 to take advantage of type declarations.
These type declarations are stripped during compilation to be compatible with PHP 5.4.
PHP 5.4 is still supported because some users cannot upgrade their servers.
Compatibility is checked by php54 -l on the compiled files, which reports the syntax, and by composer compat, which reports the rest - a function or a constant missing in PHP 5.4.
It uses PHPCompatibility configured in conf/phpcompat.xml and compiles both files itself.
The release script runs the same check next to php -l over the release builds, passing them on the command line, which overrides the files in the ruleset.
It runs separately from composer check because it is slow and because it can't see the guards - a newer function may be called if function_exists() or PHP_VERSION_ID allows it, so the ruleset lists every such call as an exclusion.
Adding a call to a newer function therefore means either guarding it, or verifying that the guard is elsewhere and excluding it there.
This is not theoretical: 6.0.0 shipped oci_set_call_timeout() unguarded, which is available since PHP 7.2.13.
The required PHP version is only increased if it significantly improves the code.
Older PHP versions had bugs that required workarounds, but modern versions primarily introduce new features.
The same philosophy applies to database systems.
Even unsupported database versions are still supported because they remain in use.
Support for an old version is only dropped if maintaining it would overly complicate the code.
For instance, MySQL 4 lacks information_schema, making generated column support impractical, so support for MySQL 4 was removed.
Adminer aims for backward compatibility, particularly for plugins. Only significant improvements, such as adding namespaces, justify breaking changes.
Besides driver classes, Adminer provides the Adminer class for customization.
This class enables Adminer and Adminer Editor (which lacks DDL support) to share functionality.
Developers can extend this class to implement customizations, as I do for my projects.
A more common method for extending Adminer is the Plugins class.
A plugin is simply a class defining any methods from Adminer.
The Plugins::__call method calls all registered plugins until one of them returns non-null.
Some hooks (dumpFormat, dumpOutput, editRowPrint, editFunctions, config) don't short-circuit but append the results of all plugins instead.
The built-in Adminer class is registered as the last plugin, providing the default behavior.
The syntax highlighter of a driver is loaded from adminer/static/jush/modules/jush-<driver>.js in the repository and in the source archive, the same for the bundled drivers and for the plugins in plugins/drivers/.
A deployed driver plugin is a single file with no submodule next to it, so the released copy carries the module inline instead - minified by JsShrink and compressed, the same as the modules merged into jush.js.
The release script fills in the Driver::jushModule() stub, which returns an empty string everywhere else, and the core prints the returned code in an inline <script> wrapped in addEventListener('DOMContentLoaded', ...) because jush.js is deferred and would otherwise run later.
The autocomplete of the query <textarea> is created by Driver::jushAutocomplete(), which returns an empty string in drivers whose queries are not SQL (e.g. Redis).
Adminer follows a strict coding style, though some choices may seem unusual.
For instance, doc-comments are not indented by one space because some editors (e.g., VS Code) insert a space when pressing Enter after */.
There is no enforced rule on " vs. '.
Most code uses " because it's more flexible (e.g., embedding variables).
Even in cases where variable interpolation is unlikely (e.g., $_GET["table"]), I still use ".
' is primarily used for regular expressions and is required for extracting translations in lang().
I avoid "{$var}" because it is longer.
In rare cases where $var cannot be used directly within a string, I prefer splitting the string ("prefix$var" . "suffix").
Never use $_REQUEST.
Decide where the parameter belongs and access it accordingly.
PHP global functions use snake_case, so I use it for functions and variables.
MySQLi’s Db class extends mysqli, so it also uses snake_case.
However, I prefer camelCase for method names and parameters so I use it in other classes.
This inconsistency sometimes results in passing $table_status to a method expecting $tableStatus.
Some pages use uppercase for main object (e.g., $TABLE), but I dislike this despite its visibility.
Return values of functions are usually constructed into variables named $return.
Code within if statements and loops must always be wrapped in {} blocks.
These are removed during minification.
else if is forbidden; use elseif instead.
I use empty lines sparingly to separate code blocks.
Lines containing only } naturally divide the code visually.
Well-used ternary operators enhance readability, but they are sometimes overused in Adminer.
// Preferred
$title = ($update
? lang('Save and continue editing')
: lang('Save and insert next')
);
// Less desirable
if ($update) {
$title = lang('Save and continue editing');
} else { // If you change else to elseif in the future then $title may stay uninitialized
$title = lang('Save and insert next');
}
Adminer has a generous line length limit of 200 characters. Shorter lines are preferable but wrapping them at arbitrary points is unacceptable. Proper line wrapping often requires refactoring, which has caused bugs in the past, so I hesitate to make changes purely for line length.
Lines are wrapped at the boundaries the expression already has, never at an arbitrary column:
before a ., ? or : operator, which starts the continuation line indented by one more tab;
or by putting each argument or array element on its own line.
Trailing comments that make the line too long move above the statement.
Long queries are wrapped inside the string, with the continuation of a clause indented by one tab.
lang('...') must not be split between lang( and the closing ' – compile.php matches it as text.
All functions have doc-comments, but redundancy is avoided.
For example, Db methods are documented only in db.inc.php, not in the drivers.
@param tags include only params with type more specific than the native type declaration or with a comment.
The doc-comments use aliases for complex arrays.
Doc-comments are imperative ("Get" instead of "Gets"), start with a capital letter, and do not end with a period.
Inline comments are useful for linking specifications but are generally avoided for explaining self-explanatory code. They start with a lowercase letter and do not end with a period.
Comments starting with //! mean TODO.
Comments starting with //~ are meant for debugging.
Adminer strictly initializes all variables before use, which is verified. However, Adminer relies on the default value of uninitialized array items. This approach leads to more readable code. Consider the following examples:
// Adminer style
if (extension_loaded("mysqli") && $_GET["ext"] != "pdo")
// Explicit isset
if (extension_loaded("mysqli") && (!isset($_GET["ext"]) || $_GET["ext"] != "pdo"))
// Possible since PHP 7.0
if (extension_loaded("mysqli") && ($_GET["ext"] ?? "") != "pdo")
// With idx() explained later
if (extension_loaded("mysqli") && idx($_GET, "ext") != "pdo")
Adminer silences these errors, but only for undefined array keys - accessing an offset on null or using an undefined variable is still reported.
For cases where the whole array may be missing, Adminer defines idx().
It uses array_key_exists() because isset() would make idx(array(null), 0, '') return an incorrect value.
Using isset can introduce bugs, such as in this case: isset($rw["name"]).
Here, I intended to check if $row contains name, but a typo in the variable name is silently ignored.
The same is true for ??.
idx() doesn't have this problem because PHP reports an error for an undefined variable passed to it.
empty() is even worse and should be avoided in most cases.
Adminer uses @ only where an error is unavoidable, such as when writing to files.
Even if you check whether a file is writable, a race condition exists between the check and the actual write operation.
Adminer does not implement automatic escaping.
When printing untrusted data (including e.g. table names), you must use h(), which escapes HTML special characters including " and ' (it uses str_replace because it is much faster than htmlspecialchars).
Translations are an exception - they are trusted and printed unescaped, so they can contain HTML (a few of them do) and lang() must never be wrapped in h().
To keep them usable elsewhere, lang_format() replaces ' by ’ so that it can't break out of an attribute and also looks nicer; compile.php does the same replacement for single language versions, where lang_format() is not called.
HTML attributes containing a translation should therefore be delimited by ', and a translation printed inside a JavaScript string should still use js_escape() because the replacement doesn't cover \ or newlines.
While a templating system would be useful, it would need to support streaming.
Adminer prints data immediately to display partial results when a query is slow.
When constructing SQL queries, use q() for strings and idf_escape() for identifiers.
Adminer requires full control when constructing queries, making the use of additional helpers challenging.
Query strings are escaped by url_escape(), which is deliberately much weaker than urlencode().
Only %, &, +, =, ? and # are significant in a query string.
", ', <, >, space and everything outside printable ASCII are rewritten by the browser anyway, so escaping them keeps the generated URL byte-identical to the one the browser sends back - remove_from_uri() matches URLs as text and redirect() keys the flash messages by them.
Tabs and newlines must be escaped for a different reason: the browser removes them from an URL instead of escaping them.
Everything else stays verbatim, which makes URLs both shorter and readable: where[0][col]=name instead of where%5B0%5D%5Bcol%5D=name.
A space becomes + because it is two characters shorter than %20.
The hexadecimal digits are upper case to match the browser.
The escaped set is extended by arg_separator.input because a host can set it to e.g. &;; it can only be read, never set at runtime.
url_escape() is only for Adminer's own query string.
Path segments, an URL authority and external APIs (e.g. the AWS signature in simpledb.php) still need the strict urlencode() or rawurlencode().
This is also why the function is not named urlencode() - the namespace fallback would silently retarget every unqualified call in the Adminer namespace, including the drivers.
Brackets stay verbatim in an URL but a parameter name is a different matter.
PHP decodes a parameter name once and only then parses the brackets, so a name must not contain [, ] or = after that single decoding.
This is exactly what bracket_escape() is for, including a whole query string used as a name - $unique_idf in val[...] in select.inc.php - which is read back by bracket_escape($idf, true).
The browser escapes the name once more when submitting the form and PHP undoes only that one layer.
urlencode() would work there too but it is longer, and this identifier is repeated in every cell of the result table.
A value makes a round trip: the database returns it, Adminer prints it to HTML and to URLs, the browser sends it back, and Adminer builds a condition from it. Several helpers take part in this and it is easy to reach for the wrong one, because some of them are driven by the column type and some by the value itself.
convert_field() returns an SQL expression converting a column in the select so that the extension returns something PHP can work with; unconvert_field() is the inverse and wraps an already quoted SQL expression.
Whatever is read through convert_field() must be written back through unconvert_field(), and the value traveling in where[] is then encoded (hexadecimal in MySQL), not raw bytes.
Driver::unconvertFunction() is the display counterpart: it returns the HTML label printed in front of the edit input so that the user knows which function will be applied to the entered value.
It is never used to build a query, so adding a conversion means editing both - unconvert_field() performs it and unconvertFunction() announces it.
Driver::value() converts the fetched value in PHP instead of in SQL, where the extension itself returns an encoded form - PostgreSQL returns bytea as the text \x....
It is applied when displaying a value, so the value carried in where[] is still the one the extension returned; call it explicitly where the actual bytes are needed, as [select.inc.php](/adm
Then run bash setup.sh. It stops at the first error and is safe to run again. Servers and tests aren't started; it lists them at the end. Read it first.
The repo uses git submodules, so the clone fetches them too.
git clone --recurse-submodules https://github.com/vrana/adminer.git cd adminer
PHP >=7.4
sudo apt install php-cli php-mbstring php-xml php-curl unzip composer # Debian/Ubuntu
composer install
If downloaded from Git then run: git submodule update --init (or composer install)
adminer/index.php - Run development version of Adminereditor/index.php - Run development version of Adminer Editor, needs adminer/ next to iteditor/example.php - Example customizationcompile.php - Create a single file versionlang.php - Update translationstests/*.spec.js - Playwright end-to-end testsNo reports for Linux yet.
Opens a chat with this manual already filled in, for Linux. It uses your own account; gitmanual sends nothing.