mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 20:23:26 +00:00
[Web] Add Mailcow\ class structure and imapsync module classes
This commit is contained in:
26 files changed
+2821
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# mailcow PHP classes (`Mailcow\`)
|
||||
|
||||
New backend code of the mailcow UI/API lives here as plain PHP classes, one directory per module.
|
||||
It replaces the procedural `data/web/inc/functions.*.inc.php` files step by step. There is no framework,
|
||||
no DI container and no ORM: just small classes, constructor injection and PDO.
|
||||
|
||||
The directory sits below `inc/lib/`, which nginx never serves. Classes are loaded by Composer's PSR-4
|
||||
autoloader (`"Mailcow\\": "mailcow/"` in `inc/lib/composer.json`), which every entry point already
|
||||
includes (`prerequisites.inc.php`, crons, `mailcowauth.php`).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
Core/ shared by all modules
|
||||
Context.php who is calling: role, username, ACL, access-all; fromSession(), system()
|
||||
UserError.php expected error with a lang key (+ args), shown as a danger message
|
||||
AccessDenied.php UserError 'access_denied'
|
||||
ApiResponse.php collects messages, writes $_SESSION['return'] (the only class that does)
|
||||
Input.php helpers for loosely typed request values
|
||||
Repository.php base class of all repositories: PDO, query(), insertRow(), updateRow(), transaction()
|
||||
<Module>/
|
||||
<Module>Module.php composition root: create(PDO, Context, array $config) wires everything;
|
||||
fromGlobals() is the legacy entry point for procedural callers
|
||||
Model/ plain typed objects (fromRow(), toArray() for the API shape), no DB
|
||||
Repository/ SQL only (prepared statements); takes and returns models
|
||||
Service/ use cases, business rules, permissions (Context), transactions via repositories
|
||||
Http/ outbound I/O (e.g. OAuth requests)
|
||||
Controller/ request in, messages out: adapter for json_api.php and pages
|
||||
```
|
||||
|
||||
Example: `Imapsync/` (sync jobs, sync sources, OAuth). Use it as the template for new modules.
|
||||
|
||||
## Layers and rules
|
||||
|
||||
- **Model:** typed public properties, `fromRow()` from a DB row, `toArray()` for the API format (keys and
|
||||
order are the API contract). Secrets stay in the model but never in `toArray()`. No getters/setters.
|
||||
- **Repository:** extends `Core\Repository`; the only place with SQL (prepared statements via `query()`).
|
||||
No permission decisions (a visibility filter that is pure SQL may take the `Context`). A method that writes
|
||||
several tables wraps them in `transaction()`, which joins a transaction the caller already runs.
|
||||
- **Service:** one use case per public method, working on single objects. Checks permissions with the
|
||||
`Context` and throws `UserError` / `AccessDenied` with a lang key. No `$_SESSION`, no `global`, no output.
|
||||
When add and edit share their rules, use one private routine for both (e.g. `JobService::save()`,
|
||||
`SourceService::applyBasics()`): missing attributes keep the current value, a new object starts with defaults.
|
||||
- **Controller:** normalises request input (batch ids, attribute merging), loops over batch ids, calls
|
||||
services, turns `UserError` into messages via `ApiResponse` and returns what `json_api.php` passes to
|
||||
`process_*_return()`. The API format (routes, JSON, message keys and order) stays as it was. Each write
|
||||
method is a few lines on top of two private helpers: `handle()` (log entry, error mapping, flush) and
|
||||
`batch()` (one message per id).
|
||||
- **View:** Twig templates, unchanged.
|
||||
|
||||
## Conventions
|
||||
|
||||
- `declare(strict_types=1);` in every file; typed properties, parameters and return types; constructor
|
||||
property promotion; `public readonly` properties instead of getters for values that never change.
|
||||
- Class files contain only declarations (no code at file level), one class per file.
|
||||
- Constructor injection. Global state (`$pdo`, `$_SESSION`, config arrays, `getenv()`) is only read at the
|
||||
entry points and passed to `<Module>Module::create()`. Procedural legacy callers (`json_api.php`, pages,
|
||||
`functions.*.inc.php`) use `<Module>Module::fromGlobals(?Context $context = null)`, the one documented
|
||||
place in the module that reads the globals, instead of repeating the config lines; entry points that build
|
||||
their own state (crons, CLI) call `create()` directly, e.g. with `Context::system()`.
|
||||
`ApiResponse::flush()` is the one class that writes `$_SESSION['return']`.
|
||||
- No interfaces until there are two implementations; no closures as injected dependencies (local callbacks
|
||||
such as `transaction(fn ...)` or the controller's `handle()` are fine); small methods.
|
||||
- Docblocks only where they add information (rules, API contract, non-obvious behaviour), not to repeat
|
||||
the signature.
|
||||
- Classes are not `final`, so PHPUnit can mock them. I/O that tests must replace sits in protected methods
|
||||
(see `Imapsync/Http/OAuthClient::send()`).
|
||||
- Messages: services throw, controllers report. The `msg` is a lang key of the matching section
|
||||
(`success` / `danger`), optionally with arguments: `new UserError('object_exists', [$name])`.
|
||||
- Logs: the `log` part of a message is `[<legacy function>, <action>, <type>, <data>]` as before; request
|
||||
keys holding secrets are masked by `ApiResponse` (configure them in the module).
|
||||
- A database error (`PDOException`) stops the call with one `mysql_error` message, like the legacy code.
|
||||
- Naming: `Mailcow\<Module>\{Model,Repository,Service,Http,Controller}\<Class>`.
|
||||
|
||||
## Converting a legacy `functions.X.inc.php`
|
||||
|
||||
1. Capture the current API behaviour (E2E runs, raw JSON responses, DB state).
|
||||
2. Build the module as above: models, repositories, services, controller, `<Module>Module::create()`.
|
||||
3. Switch the callers (`json_api.php`, pages, crons, other function files) to the module.
|
||||
4. Delete the function file and its `require` lines.
|
||||
5. Compare the API output with step 1; the format must stay the same.
|
||||
|
||||
Large files (e.g. `functions.mailbox.inc.php`) are converted one area at a time (aliases, domains, ...),
|
||||
each as its own module. Until `functions.inc.php` is converted, `Context::canAccessMailbox()` /
|
||||
`canAccessDomain()` call the legacy `hasMailboxObjectAccess()` / `hasDomainAccess()`.
|
||||
|
||||
## Development
|
||||
|
||||
- Unit tests: `tests/php/<Module>/...` (dev-only, never deployed), run with
|
||||
`helper-scripts/dev_tests/run_php_unit_tests.sh`. Services are tested with mocked repositories;
|
||||
repositories are covered by the E2E suites (MariaDB-specific SQL).
|
||||
- Autoloader: after adding the PSR-4 mapping nothing needs regenerating for new classes. If you ever run
|
||||
`composer dump-autoload` (inside the php-fpm container, `-w /web/inc/lib`), never use `-o`, `-a` or
|
||||
`--apcu`, and commit only the changed `autoload_*.php` files.
|
||||
- No dev packages in `inc/lib/composer.json`.
|
||||
- opcache runs with `validate_timestamps=0`: restart php-fpm after changing PHP files
|
||||
(`docker compose restart php-fpm-mailcow`).
|
||||
Reference in new issue
Block a user