From 81c5ae33ab471ae0b07d0d714d4649052083b620 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Fri, 28 Mar 2025 17:30:30 +0100 Subject: [PATCH] Docs: wrap --- developing.md | 220 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 169 insertions(+), 51 deletions(-) diff --git a/developing.md b/developing.md index 160b125a..64c76321 100644 --- a/developing.md +++ b/developing.md @@ -4,57 +4,108 @@ Jakub Vrána ## Request Lifecycle -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. +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 available PHP extensions. There is no `DriverMysql` or `DbMysqlPdo`; there is always up to one `Driver` and one `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=`, 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. +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. 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. +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 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`). +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`). -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. +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]`), and any `[` 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 empty strings using `$table != ""` instead of `!$table`, since table names can be `0`, and `!$table` would fail in such cases. ## Classes, Functions, Constants, Variables -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()`. +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()`. -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 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 -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. +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 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. +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 for backward compatibility, particularly for plugins. Only significant improvements, such as adding namespaces, justify breaking changes. +Adminer aims for backward compatibility, particularly for plugins. +Only significant improvements, such as adding namespaces, justify breaking changes. ## Extending Functionality -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. +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 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. +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 -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 `*/`. +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 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()`. +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 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"`). +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. +Never use `$_REQUEST`. +Decide where the parameter belongs and access it accordingly. -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. Return values of functions are usually constructed into variables named `$return`. +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. +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. +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. My editor shortcut jumps between empty lines, primarily for navigating functions. Lines containing only `}` naturally divide the code visually. +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 enhance readability, but they are sometimes overused in Adminer. @@ -73,19 +124,30 @@ if ($update) { } ``` -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. +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 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. +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 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. +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. -Comments starting with `//!` mean TODO. Comments starting with `//~` are meant for debugging. +Comments starting with `//!` mean TODO. +Comments starting with `//~` are meant for debugging. ## Error Handling -Adminer strictly initializes all variables before use, which is [verified](/phpstan.neon). However, Adminer relies on the default value of uninitialized array items. This approach leads to more readable code. Consider the following examples: +Adminer strictly initializes all variables before use, which is [verified](/phpstan.neon). +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 @@ -101,7 +163,10 @@ if (extension_loaded("mysqli") && ($_GET["ext"] ?? "") != "pdo") if (extension_loaded("mysqli") && idx($_GET, "ext") != "pdo") ``` -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: +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) { @@ -110,69 +175,112 @@ function idx($array, $key, $default = null) { } ``` -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. The same is true for `??`. `empty()` is even worse and 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. +The same is true for `??`. +`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 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 -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. +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, use `q()` for strings and `idf_escape()` for identifiers. Adminer requires full control when constructing queries, making the use of additional helpers 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 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. Many closing HTML tags are optional (e.g., `` or ``) and Adminer obviously doesn't print them. +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. +Many closing HTML tags are optional (e.g., `` or ``) and Adminer obviously doesn't print them. -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. +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, 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. +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 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. +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 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. +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. -Previously, these functions were bound directly in HTML (``), but strict CSP enforcement made this impossible. Now, Adminer registers event handlers using a short `