mirror of
https://github.com/vrana/adminer.git
synced 2025-08-04 21:58:28 +02:00
Docs: improve style thanks to ChatGPT
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
- Reproducible [bug reports](https://github.com/vrana/adminer/issues/new?template=bug_report.md) are warmly welcomed.
|
||||
- [Feature requests](https://github.com/vrana/adminer/issues/new?template=BLANK_ISSUE) are also fine but I'm quite picky about what to accept to Adminer. Please don't be offended if I close the issue as Not Planned, especially if it can be achieved with a plugin.
|
||||
- [Pull requests](https://github.com/vrana/adminer/pulls) both for bugs and simple features are also fine. Before doing anything more complicated, get familiar with [Adminer philosophy](/developing.md).
|
||||
- [Feature requests](https://github.com/vrana/adminer/issues/new?template=BLANK_ISSUE) are also fine, but I'm quite picky about what to accept into Adminer. Please don't be offended if I close the issue as "Not Planned," especially if it can be achieved with a plugin.
|
||||
- [Pull requests](https://github.com/vrana/adminer/pulls) for both bug fixes and simple features are welcome. Before working on anything more complicated, get familiar with the [Adminer philosophy](https://github.com/vrana/adminer/blob/master/developing.md).
|
||||
|
15
README.md
15
README.md
@@ -1,17 +1,16 @@
|
||||
# Adminer
|
||||
|
||||
**Adminer** is a full-featured database management tool written in PHP.
|
||||
It consists of a single file ready to deploy to the target server.
|
||||
**Adminer** is a full-featured database management tool written in PHP. It consists of a single file ready to deploy to the target server.
|
||||
**Adminer Editor** offers data manipulation for end-users.
|
||||
|
||||
https://www.adminer.org/
|
||||
[Official Website](https://www.adminer.org/)
|
||||
|
||||
## Features
|
||||
- **Supports:** MySQL, MariaDB, PostgreSQL, CockroachDB, SQLite, MS SQL, Oracle
|
||||
- **Plugins for:** Elasticsearch, SimpleDB, MongoDB, Firebird, ClickHouse, IMAP
|
||||
- **Requirements:** PHP 5.3+
|
||||
|
||||
## Screenshot
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
If downloaded from Git then run: `git submodule update --init`
|
||||
@@ -27,8 +26,8 @@ If downloaded from Git then run: `git submodule update --init`
|
||||
- `tests/*.html` - Katalon Recorder test suites
|
||||
|
||||
## Plugins
|
||||
There are [several plugins](plugins/) distributed with Adminer and there are also many user-contributed plugins linked from https://www.adminer.org/plugins/.
|
||||
To use a plugin, simply upload it to `adminer-plugins/` next to `adminer.php`. You can also upload plugins for drivers (e.g. `elastic.php`) here.
|
||||
There are [several plugins](/plugins/) distributed with Adminer, as well as many user-contributed plugins linked on the [Adminer Plugins page](https://www.adminer.org/plugins/).
|
||||
To use a plugin, simply upload it to the `adminer-plugins/` directory next to `adminer.php`. You can also upload plugins for drivers (e.g., `elastic.php`) in this directory.
|
||||
|
||||
```
|
||||
- adminer.php
|
||||
@@ -40,7 +39,7 @@ To use a plugin, simply upload it to `adminer-plugins/` next to `adminer.php`. Y
|
||||
- adminer-plugins.php
|
||||
```
|
||||
|
||||
Some plugins require configuration. To use them, create a file `adminer-plugins.php`. You can also specify the loading order here.
|
||||
Some plugins require configuration. To use them, create a file named `adminer-plugins.php`. You can also specify the loading order in this file.
|
||||
|
||||
```php
|
||||
<?php // adminer-plugins.php
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
I support only the last published version and the last development version (last commit).
|
||||
Only the latest published version and the latest development version (last commit) are supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, add a new draft security advisory at https://github.com/vrana/adminer/security/advisories/new.
|
||||
To report a vulnerability, create a new draft security advisory at [GitHub Security Advisories](https://github.com/vrana/adminer/security/advisories/new).
|
||||
|
||||
I handle security issues with top priority. If you don't hear from me in a week then please ping the bug. Once I accept the bug, the fix should be available and new version released within days. I will mark the bug as public after releasing a new version or declining the bug.
|
||||
Security issues are handled with top priority. If you don't receive a response within a week, please follow up on the report. Once a vulnerability is acknowledged, a fix should be available and a new version released within a few days. The issue will be made public after the fix is released or if the report is declined.
|
||||
|
147
developing.md
147
developing.md
@@ -1,71 +1,71 @@
|
||||
# Notes for developers
|
||||
# Notes for Developers
|
||||
|
||||
Jakub Vrána
|
||||
|
||||
## Request lifecycle
|
||||
## Request Lifecycle
|
||||
|
||||
Request lifecycle is pretty simple. Adminer loads a database driver based on a URL parameter (e.g. `pgsql=`). The drivers live in [adminer/drivers/](/adminer/drivers/) and [plugins/drivers/](/plugins/drivers/). The driver consists of the class [`Driver`](/adminer/include/driver.inc.php) and a bunch of functions which would have a better home in `Driver` but aren't there for historical reasons.
|
||||
The request lifecycle is straightforward. Adminer loads a database driver based on a URL parameter (e.g., `pgsql=`). The drivers live in [adminer/drivers/](/adminer/drivers/) and [plugins/drivers/](/plugins/drivers/). The driver consists of the class [`Driver`](/adminer/include/driver.inc.php) and a set of functions that ideally belong in `Driver` but remain separate due to historical reasons.
|
||||
|
||||
A driver also creates the [`Db`](https://github.com/vrana/adminer/blob/v5.0.6/adminer/drivers/mysql.inc.php#L62) class based on the available PHP extensions. So there's no `DriverMysql` or `DbMysqlPdo`, there's always up to one `Driver` and `Db`.
|
||||
A driver also creates the [`Db`](https://github.com/vrana/adminer/blob/v5.0.6/adminer/drivers/mysql.inc.php#L62) 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=` then Adminer tries to authenticate that user. If it fails then a login form is displayed on the same URL and post data is stored to hidden form fields. If the user authenticates with the same credentials then the action is performed.
|
||||
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 all data modifications but also language change or logout) are performed using POST with a CSRF token present. This token is not necessary in modern browsers because Adminer also sets cookies as SameSite but it is kept just to be sure. If the POST action is successful then Adminer redirects the browser to GET so that page refresh doesn't perform the action again. Unsuccessful POST displays the same page with form fields pre-filled. Page refresh tries the same action again to handle the situation where the reason for the error was fixed e.g. in a different browser tab.
|
||||
All state-changing actions (primarily data modifications, as well as language change or logout) are performed using POST with a CSRF token present. This token is unnecessary in modern browsers because Adminer sets cookies as SameSite, but it remains for additional security. 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 the other URL parameters. E.g. if there's `indexes=` in the URL then [adminer/indexes.inc.php](/adminer/indexes.inc.php) is loaded. The table name is taken from this parameter. This creates simpler URLs than e.g. `action=indexes&table=customers`.
|
||||
Then, the request is routed based on other URL parameters. For example, if the URL contains `indexes=`, then [adminer/indexes.inc.php](/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`).
|
||||
|
||||
PHP session is stopped before the page rendering starts. This disallows setting `$_SESSION` later in the code but allows opening another page with Adminer e.g. if there's a long running query in the first one.
|
||||
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 could be arbitrary so they are never transferred in URL or POST naked. They are always wrapped to `fields[col]` or similar. A possible `[` in the name is escaped.
|
||||
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]`), and any `[` in the name is escaped.
|
||||
|
||||
Adminer often checks for an empty string using `$table != ""` or similar. It is because a table name could be `0` and a simple check `!$table` would fail.
|
||||
Adminer often checks for empty strings using `$table != ""` instead of `!$table`, since table names can be `0`, and `!$table` would fail in such cases.
|
||||
|
||||
## Classes, functions, constants, variables
|
||||
## Classes, Functions, Constants, Variables
|
||||
|
||||
Adminer defines lots of functions and some global variables. The functions live in a namespace so they don't collide with anything else. The global variables should be avoided but Adminer uses them for simplicity. They are minified during compilation to some random strings so they are unusable externally (e.g. by plugins). Plugins can access some of them using helpers (e.g. `Adminer\driver()`).
|
||||
Adminer defines many functions and some global variables. Functions are namespaced to prevent collisions. Global variables should be avoided, but Adminer uses them for simplicity. 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()`.
|
||||
|
||||
There are also some constants in Adminer's namespace. A prominent example is `JUSH` which is a syntax highlighting ID (e.g. `pgsql` for PostgreSQL). This is sometimes used for simple ifs in Adminer code but it should be avoided for anything more complicated - usually a method in `Driver` is better.
|
||||
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.
|
||||
|
||||
## Backwards compatibility
|
||||
## Backwards Compatibility
|
||||
|
||||
Adminer is very conservative about the required PHP version. PHP 5.3 is still supported. Users sometimes upload Adminer to a server where they just couldn't upgrade PHP. The compatibility is occasionally [checked](https://github.com/vrana/adminer/blob/v5.0.6/phpcs.xml#L121). I bump the required PHP version only if it improves the code significantly. In the past, old PHP versions had some bugs and working around them was a pain. It's not true anymore and new PHP versions usually only add new features. If using a new feature would lead to better Adminer code then I'd consider bumping the required PHP version. This happened e.g. with anonymous functions or namespaces.
|
||||
Adminer is highly conservative regarding PHP version requirements. PHP 5.3 is still supported because some users cannot upgrade their servers. Compatibility is periodically [checked](https://github.com/vrana/adminer/blob/v5.0.6/phpcs.xml#L121). 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 is true also for database systems. I support even the officially unsupported database versions because they are actually still used in the real world. I remove support for an old version only if it would complicate the code significantly. E.g. info about generated columns is available in `information_schema` which is not present in MySQL 4. So I've dropped support for MySQL 4 because it would need a totally different code path for getting info about columns.
|
||||
The same philosophy applies to database systems. Even unsupported database versions are still accommodated 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 to be compatible with its past versions which is important mainly for plugins. Only a significant improvement such as adding namespaces could break backwards compatibility.
|
||||
Adminer aims for backward compatibility, particularly for plugins. Only significant improvements, such as adding namespaces, justify breaking changes.
|
||||
|
||||
## Extending functionality
|
||||
## Extending Functionality
|
||||
|
||||
Apart from the driver classes, there's also the class [`Adminer`](/adminer/include/adminer.inc.php) used for customization. It's powerful enough to split the functionality of Adminer and Adminer Editor (which has no DDL). It's possible to extend this class to create own customization. I use this feature to create admin interfaces for my own projects.
|
||||
Besides driver classes, Adminer provides the [`Adminer`](/adminer/include/adminer.inc.php) 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 way of hooking into Adminer is using the class [`Plugins`](/adminer/include/plugins.inc.php). I don't like the code in this class (it's very repetitive) but it serves the purpose well. It allows creating plugins which don't need to extend any Adminer class. Developers are familiar with creating a class containing some methods so this has low entry requirements. I've considered using hooks instead (e.g. `$hooks->register("tableName", $callback)` and then `$hooks->call("tableName")` but I like the fact that I can simply call `$adminer->tableName()` in Adminer code. Supporting this syntax with hooks would mean just moving the repetitive code elsewhere.
|
||||
A more common method for extending Adminer is the [`Plugins`](/adminer/include/plugins.inc.php) class. Although its code is somewhat repetitive, it effectively allows developers to create plugins without extending `Adminer` directly. Since developers are accustomed to defining classes with methods, this approach has a low entry barrier. I considered a hook system (`$hooks->register("tableName", $callback)`, then `$hooks->call("tableName")`), but I prefer the direct call syntax (`$adminer->tableName()`). Implementing hooks while maintaining this syntax would simply relocate repetitive code.
|
||||
|
||||
## Code style
|
||||
## Code Style
|
||||
|
||||
Adminer uses quite strict code [style](/phpcs.xml) which might be perhaps slightly unusual. E.g. doc-comments are not indented by one space which is because some editors start the next line with a space if you hit Enter after `*/` (e.g. VS Code). I'm not very attached to a particular code style and I'm open to changes but all code must look the same. So if you want to change the code style then you need to adapt all existing code to it.
|
||||
Adminer follows a strict [coding style](/phpcs.xml), 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's no rule about using `"` or `'`. Most code uses `"` because it's more versatile e.g. if you decide to use a variable in the string later. I use it even in cases where it's unlikely that a variable would be used later (e.g. `$_GET["table"]`) just because I have an editor snippet to insert this. `'` is used mostly with regular expressions and it is mandatory for extracting translations in `lang()`.
|
||||
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 `"` due to an existing editor snippet. `'` is primarily used for regular expressions and is required for extracting translations in `lang()`.
|
||||
|
||||
I don't use `"{$var}"` because it's longer. In the rare cases where `$var` couldn't be used in a string directly, I rather split the string: `"prefix$var" . "suffix"`.
|
||||
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`. Make your mind about the right place for the param and access it there.
|
||||
Never use `$_REQUEST`. Decide where the parameter belongs and access it accordingly.
|
||||
|
||||
I'm not very happy about naming style. PHP's global functions use snake_case so I use it too in functions and variable names. The MySQLi's `Db` class extends the `mysqli` class so it uses snake_case for its methods too. But I prefer camelCase which is used in methods of other classes and their parameters. So it's not very consistent and sometimes you pass `$table_status` to a method accepting `$tableStatus`. The best solution is to use one word for everything which is not very practical. Some pages use capitals for the main object, e.g. `$TABLE` which I don't like very much but it shines nicely in the code.
|
||||
I am not entirely satisfied with the naming style. 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`. The best approach would be to use single-word names, though this is impractical. Some pages use uppercase for main object (e.g., `$TABLE`), but I dislike this despite its visibility.
|
||||
|
||||
Code after `if` and loops must be wrapped to `{}` blocks. They are removed in minification. `else if` is forbidden, use `elseif`.
|
||||
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 to split code blocks but perhaps slightly less than usual. I have an editor shortcut to jump between empty lines and I use it primarily to jump between functions if a file has them. Lines with lone `}` divide the code optically well enough for me.
|
||||
I use empty lines sparingly to separate code blocks. My editor shortcut jumps between empty lines, primarily for navigating functions. Lines containing only `}` naturally divide the code visually.
|
||||
|
||||
Well used ternary operators make the code more readable and shorter. However, Adminer code sometimes overuses them.
|
||||
Well-used ternary operators enhance readability, but they are sometimes overused in Adminer.
|
||||
|
||||
```php
|
||||
// I find this more readable and less repetitive:
|
||||
// Preferred
|
||||
$title = ($update
|
||||
? lang('Save and continue edit')
|
||||
: lang('Save and insert next')
|
||||
);
|
||||
|
||||
// Than this:
|
||||
// Less desirable
|
||||
if ($update) {
|
||||
$title = lang('Save and continue edit');
|
||||
} else { // If you change else to elseif in the future then $title may stay uninitialized
|
||||
@@ -73,17 +73,17 @@ if ($update) {
|
||||
}
|
||||
```
|
||||
|
||||
Adminer has a line length limit 250 which is ridiculous. All lines fit my screen but I still want to make them shorter. Perhaps 150 would be more reasonable but I also hate wrapping lines at random places - they must be wrapped at logical blocks which often requires at least a small refactoring. These refactorings introduced bugs in the past so I'm hesitant to do them just to fit some arbitrary limit.
|
||||
Adminer has an excessive line length limit of 250 characters. While all lines fit my screen, I prefer shorter lines. A limit of 150 would be more reasonable, but wrapping lines 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.
|
||||
|
||||
## Comments
|
||||
|
||||
All functions have doc-comments but I hate repetition so e.g. the `Db` methods are documented only in [mysql.inc.php](/adminer/drivers/mysql.inc.php) and not in the other drivers. Parameter names are not repeated in `@param`, only the type and description is there based on the order. Doc-comment is imperative ("Get" not "Gets"), starts with a capital letter and doesn't end with a fullstop
|
||||
All functions have doc-comments, but redundancy is avoided. For example, `Db` methods are documented only in [mysql.inc.php](/adminer/drivers/mysql.inc.php), not in other drivers. `@param` tags include only type and description, based on order. Doc-comments are imperative ("Get" instead of "Gets"), start with a capital letter, and do not end with a period.
|
||||
|
||||
Inline commets are useful e.g. to link specifications but they are usually avoided to explain the code which should be self-explanatory. Inline comments start with a small letter but I'm not very happy about it. They don't end with a fullstop.
|
||||
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, though I am not entirely happy with this convention.
|
||||
|
||||
## Error handling
|
||||
## Error Handling
|
||||
|
||||
Adminer strictly initializes all variables before use which is [checked](https://github.com/vrana/php-initialized). However, Adminer relies on the default value of uninitialized array items. This leads to much more readable code, consider e.g. this code:
|
||||
Adminer strictly initializes all variables before use, which is [verified](https://github.com/vrana/php-initialized). However, Adminer relies on the default value of uninitialized array items. This approach leads to more readable code. Consider the following examples:
|
||||
|
||||
```php
|
||||
// Adminer style
|
||||
@@ -99,92 +99,91 @@ if (extension_loaded("mysqli") && ($_GET["ext"] ?? "") != "pdo")
|
||||
if (extension_loaded("mysqli") && idx($_GET, "ext") != "pdo")
|
||||
```
|
||||
|
||||
Treating undefined variables as empty was a big progress from the C language where they contain random data. Sadly, developers abused this feature which led PHP to issue first notices and then warnings in this case. Adminer [silences](https://github.com/vrana/adminer/blob/v5.0.6/adminer/include/errors.inc.php) these errors. In projects, where I'm forced to check array key existence before using it, I quickly create a function like this.
|
||||
Treating undefined variables as empty was a significant improvement over the C language, where they contained random data. Unfortunately, developers abused this feature, leading PHP to issue first notices and later warnings. Adminer [silences](/adminer/include/errors.inc.php) these errors. In projects where I am required to check array key existence before usage, I quickly create a function like this:
|
||||
|
||||
```php
|
||||
function idx($array, $key, $default = null) {
|
||||
// Note that this couldn't use isset() because idx(array(null), 0, '') would return a wrong value
|
||||
return (array_key_exists($key, $array) ? $array[$key] : $default);
|
||||
// Note: isset() cannot be used here because idx(array(null), 0, '') would return an incorrect value.
|
||||
return array_key_exists($key, $array) ? $array[$key] : $default;
|
||||
}
|
||||
```
|
||||
|
||||
It would be possible to use such a function in Adminer but the code would still be less readable than the current approach. Using `isset` can lead to bugs such as in this code: `isset($rw["name"])` (I want to check if `$row` contains `name` but I've made a typo in the variable name which `isset` silences). `empty()` is even worse and it should be avoided in most cases.
|
||||
Although it would be possible to use such a function in Adminer, the code would still be less readable than the current approach. 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. `empty()` is even worse and should be avoided in most cases.
|
||||
|
||||
Adminer uses `@` only in cases where a possible error is unavoidable, e.g. when writing to files (even if you check if the file is writable then there's a race condition between the check and the actual write).
|
||||
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.
|
||||
|
||||
## Escaping
|
||||
|
||||
There's no auto-escaping in Adminer. When printing untrusted data (including e.g. table names), you must use `h()` which is a shortcut for `htmlspecialchars` with escaping also `"` and `'`. It would be nice to have some template system but it would have to be powerful enough to support streaming. Adminer needs to print data immediately to display at least partial results when some query is slow.
|
||||
Adminer does not implement automatic escaping. When printing untrusted data (including e.g. table names), you must use `h()`, which is a shortcut for `htmlspecialchars` that also escapes `"` and `'`. 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, you must use `q()` for strings and `idf_escape()` for identifiers. Adminer needs full power when constructing queries so using some helper here would be challenging.
|
||||
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.
|
||||
|
||||
## Minimalism
|
||||
|
||||
Adminer is minimalist in everything - if something doesn't need to be there then it shouldn't be. It's true for UI which I try to keep as uncluttered as possible. E.g. I'm almost never interested in an index name, I'm always interested in columns of that index. So Adminer displays the index name only in `title=""`. The same is true for code - e.g. public visibility is the default so it doesn't need to be explicitly specified. People use `public` to differentiate from the case where they've forgotten to specify the visibility but I don't suffer from this.
|
||||
Adminer is minimalist in every aspect - if something is unnecessary, it should not be included. This philosophy extends to the UI, which remains as uncluttered as possible. For example, index names are usually irrelevant compared to the columns they reference, so Adminer displays index names only in `title=""`. The same principle applies to the code; for instance, `public` visibility is the default, so it does not need to be explicitly specified.
|
||||
|
||||
If something could be implemented in a plugin then I accept it to the core only if it's useful for almost everyone. E.g. [sticky table headers](https://github.com/vrana/adminer/issues/918) are useful for everyone so they have been included. But [dark mode switcher](https://github.com/vrana/adminer/issues/926) would clutter the UI and it's useful only for someone so I've created a plugin for that.
|
||||
If a feature can be implemented as a plugin, it is only added to the core if it benefits almost everyone. For example, [sticky table headers](https://github.com/vrana/adminer/issues/918) are useful to all users and have been included, whereas a [dark mode switcher](https://github.com/vrana/adminer/issues/926) would clutter the UI and is only useful for some, so it remains a plugin.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Adminer uses [Git submodules](https://git-scm.com/docs/git-submodule) for dependencies which predates [Composer](https://getcomposer.org/) or other package managers. Submodules are very convenient for developing - e.g. I add some feature to the syntax highlighter, commit this change and then I immediately use it in Adminer. The Adminer's commit just includes the current HEAD of the submodule. I don't need to release a new version for every change, update lock files or do stuff like that.
|
||||
Adminer uses [Git submodules](https://git-scm.com/docs/git-submodule) for dependencies, predating [Composer](https://getcomposer.org/) and other package managers. Submodules simplify development - for example, I can add a feature to the syntax highlighter, commit the change, and immediately use it in Adminer. Adminer commits simply reference the current HEAD of the submodule, avoiding the need for frequent version releases, lock file updates, or other package management tasks.
|
||||
|
||||
## Tests
|
||||
|
||||
Adminer doesn't have unit tests but it has quite extensive [end-to-end tests](/tests/). The advantage of these tests is that they verify the correct behavior even in the UI which is otherwise hard to test. They currently run about 10 minutes but it's bearable before releasing a new version. The advantage is that they allow to discover even e.g. JavaScript errors for real use-cases.
|
||||
Adminer does not include unit tests but has extensive [end-to-end tests](/tests/). These tests verify correct behavior, including UI functionality, which is otherwise difficult to test. The tests take about 10 minutes to run, which is acceptable before a release. They help detect even JavaScript errors in real-world use cases.
|
||||
|
||||
## JavaScript
|
||||
|
||||
Adminer should work with disabled JavaScript but it's more pleasant with JS enabled. Adminer doesn't use any framework but instead it has simple helpers like `qsa()` which is `document.querySelectorAll()` and then simple functions calling these helpers. These functions used to be bound directly in HTML (`<a onclick="tableClick()">`) but enabling strict CSP made this impossible. Adminer now registers these helpers using a short `<script>` element right after the tag it handles. It usually uses `qsl()` (query selector last) for this. The advantage of this approach is that the handlers are available immediately - remember that Adminer sometimes needs to wait for the database server for the response so it couldn't register the handlers e.g. at the end of the page. The only exception are handlers registered in a loop - registering them individually is slow so they are registered at once after the loop.
|
||||
Adminer functions without JavaScript but is more user-friendly when JavaScript is enabled. It does not rely on any framework but includes simple helpers like `qsa()`, a shorthand for `document.querySelectorAll()`, along with small functions that call these helpers.
|
||||
|
||||
JavaScript code is split into [functions.js](/adminer/static/functions.js) (which is common) and [editing.js](/adminer/static/editing.js) (which is Adminer or Adminer Editor specific). These two files are concatenated in compilation to one file because they can't live without each other.
|
||||
Previously, these functions were bound directly in HTML (`<a onclick="tableClick()">`), but strict CSP enforcement made this impossible. Now, Adminer registers event handlers using a short `<script>` element immediately following the relevant tag, typically using `qsl()` (query selector last). This ensures handlers are available immediately. The only exception is handlers registered in a loop, where bulk registration is more efficient.
|
||||
|
||||
JavaScript follows coding style specified in [eslint.config.mjs](/eslint.config.mjs) but ESLint requires adding dependencies to the project which I don't want so I run it externally.
|
||||
JavaScript code is split into [functions.js](/adminer/static/functions.js) (common utilities) and [editing.js](/adminer/static/editing.js) (specific to Adminer or Adminer Editor). These files are concatenated during compilation since they depend on each other.
|
||||
|
||||
JavaScript code follows the coding style defined in [eslint.config.mjs](/eslint.config.mjs), but because ESLint requires additional dependencies, I run it externally.
|
||||
|
||||
## Styles
|
||||
|
||||
Adminer generates a very simple HTML and styles it with simple CSS. It respects user preference for dark mode. Users can change the style with `adminer.css`. If you want to style some element without a class name then be a little creative with your selectors. If it's hard then I generally accept patches adding a class name to an element you want to style.
|
||||
Adminer generates simple HTML and styles it with basic CSS, respecting user preferences for dark mode. Users can customize styles via `adminer.css`. If styling an element without a class name is difficult, I generally accept patches that add meaningful class names.
|
||||
|
||||
## Compilation
|
||||
|
||||
Adminer source code is structured into a reasonable number of reasonably small files. For simple deployment, these files are bundled into a single `*.php` file. The bundling is done by inlining `include` files. Static files such as `*.js` or `*.css` are also inlined and served by the `?file=` route.
|
||||
Adminer’s source code is divided into a manageable number of reasonably small files. For simpler deployment, these files are bundled into a single `*.php` file by inlining `include` files. Static files (`*.js`, `*.css`) are also inlined and served via the `?file=` route.
|
||||
|
||||
If you wonder why includes in Adminer start with `./` - it's to skip `include_path` and it has nothing to do with compilation.
|
||||
Includes in Adminer start with `./` to bypass `include_path`, which is unrelated to compilation.
|
||||
|
||||
Compilation also [shrinks](https://github.com/vrana/PhpShrink) PHP code - it removes whitespace, comments and shortens variable names. This has the benefit that plugins couldn't overwrite Adminer's variables. The compiled file is binary which is perfectly valid in PHP but it's not valid UTF-8 which is debatable.
|
||||
Compilation also [shrinks](https://github.com/vrana/PhpShrink) PHP code by removing whitespace, comments, and shortening variable names. This prevents plugins from overwriting Adminer’s variables. The compiled file is binary, which is valid PHP but not valid UTF-8 - a debatable choice.
|
||||
|
||||
A huge chunk of the compiled file was occupied by translations. In source codes, a translation maps from English string to a localized string. For compilation, the identifiers are changed to numbers and translations are LZW compressed which saves a lot. This is decompressed into a session variable when Adminer runs to save time. It's also possible to compile a single language file which is even smaller.
|
||||
Translations used to occupy a large portion of the compiled file. In the source code, translations map English strings to localized versions. During compilation, identifiers are converted to numbers, and translations are LZW-compressed to save space. This data is decompressed into a session variable at runtime to improve performance. A single-language compilation is also possible to create even smaller files.
|
||||
|
||||
`compile.php` outputs the compiled file into the current directory but you don't need to run it from Adminer's directory. I often run it from a different directory to prepare releases (29 files) or dogfood versions of Adminer.
|
||||
`compile.php` outputs the compiled file to the current directory, but it does not need to be run from Adminer’s directory. I often run it from a separate directory to prepare releases (29 files) or test versions of Adminer.
|
||||
|
||||
## Version check
|
||||
## Version Check
|
||||
|
||||
Adminer checks for new versions from [adminer.org/version/](https://www.adminer.org/version/) which is quite elaborate. The response is signed with a private key to avoid MitM attacks. The downside is that adminer.org gets the addresses of Adminer installations in the logs. However, I don't check these logs and nobody else have an access to this server. There's a [plugin](/plugins/version-noverify.php) to disable version checks but please check the version by some other means if you use it to get security updates. It should be considered to get the version info from some independent entity, e.g. GitHub.
|
||||
Adminer checks for new versions via [adminer.org/version/](https://www.adminer.org/version/), using a signed response to prevent MitM attacks. However, this means `adminer.org` logs the IP addresses of Adminer installations. I do not review these logs, and no one else has access to the server. A [plugin](/plugins/version-noverify.php) disables version checks, but users should verify versions by other means to ensure security updates. It should be considered to get the version info from some independent entity, e.g. GitHub.
|
||||
|
||||
## Translations
|
||||
|
||||
All user visible strings should be marked as translatable with `lang('')`. This extracts them for translation and actually translates them if a translation is available.
|
||||
All user-visible strings should be translatable using `lang('')`. This extracts them for translation and applies translations if available.
|
||||
|
||||
Translations are updated with [lang.php](/lang.php) which also checks style, e.g. matching full stops.
|
||||
|
||||
Plurals are stored as arrays. The logic for picking the right element from this array is in [lang.inc.php](/adminer/include/lang.inc.php).
|
||||
|
||||
Web is translated separately using Google Sheets.
|
||||
Translations are updated via [lang.php](/lang.php), which also checks for style consistency, such as matching punctuation. Plurals are stored as arrays, with selection logic handled in [lang.inc.php](/adminer/include/lang.inc.php). The website translations are managed separately via Google Sheets.
|
||||
|
||||
## Commits
|
||||
|
||||
Every commit should do only one thing and it should be as small as possible. An [example](https://github.com/peterpp/jush/commit/2de4bac) of a poor commit in a related project describes one useful change in the description but it actually does three things:
|
||||
- Adds dark mode which is desired
|
||||
- Randomly changes some colors in the light mode
|
||||
- Changes indentation in some files which makes them inconsistent with everything else
|
||||
Every commit should do only one thing and be as small as possible. An [example](https://github.com/peterpp/jush/commit/2de4bac) of a poor commit in a related project describes one useful change in the description but actually does three things:
|
||||
|
||||
This commit should be split into three and I'd accept only the change that is actually described.
|
||||
- Adds dark mode, which is desired.
|
||||
- Randomly changes some colors in light mode.
|
||||
- Changes indentation in some files, making them inconsistent with everything else.
|
||||
|
||||
I try to honor authorship if possible but I don't want commits changing the repo to a wrong state in the history. It means that I often amend e.g. pull requests. Please don't be offended by this - your proposed change is still there under your name but the code might be slightly different. This is simpler for me than requesting changes to such pull requests.
|
||||
This commit should be split into three, and I would accept only the change that is actually described.
|
||||
|
||||
If the change modifies Adminer behavior for end users then it should be documented in [CHANGELOG](/CHANGELOG.md) in the same commit. This is quite important - I have a keyboard shortcut to blame the current line and I have another shortcut to open GitHub for the returned SHA. I often blame lines in changelog to see what they actually modified. Changes invisible to users (such as refactorings) shouldn't be documented here - the commit log is a sufficient place for them.
|
||||
I try to honor authorship whenever possible, but I don’t want commits introducing an incorrect state into the repository’s history. This means that I often amend pull requests. Please don’t be offended by this - your proposed change will still be there under your name, but the code might be slightly different. This is simpler for me than requesting changes to such pull requests.
|
||||
|
||||
Commit messages should start with a capital letter and the first line shouldn't end with a full stop. There's no line length limit but be reasonable. If the commit is specific to some area (e.g. SQLite or CSS) then the message should be formatted as `Area: Message`. Detailed description is used rarely e.g. to link other commits (use the first 7 chars of SHA in this case).
|
||||
If a change modifies Adminer’s behavior for end users, it should be documented in [CHANGELOG](/CHANGELOG.md) in the same commit. This is quite important - I have a keyboard shortcut to blame the current line and another shortcut to open GitHub for the returned SHA. I often blame lines in the changelog to see what they actually modified. Changes that are invisible to users (such as refactorings) shouldn’t be documented here; the commit log is sufficient for them.
|
||||
|
||||
If a commit handles some bug then it should be marked `(bug #n)` or `(fix #n)` if it fixes that bug.
|
||||
Commit messages should start with a capital letter, and the first line shouldn’t end with a period. There is no strict line length limit, but be reasonable. If the commit is specific to a particular area (e.g., SQLite or CSS), the message should be formatted as `Area: Message`. A detailed description is rarely used, except when linking to other commits (use the first seven characters of the SHA in this case).
|
||||
|
||||
Always diff your change before actually committing it. It helps with finding errors such as forgotten debug code.
|
||||
If a commit addresses a bug, it should be marked as `(bug #n)` or `(fix #n)` if it fixes the bug.
|
||||
|
||||
Always diff your changes before committing. This helps catch errors, such as forgotten debug code.
|
||||
|
Reference in New Issue
Block a user