Auto-generated by paul:map-codebase — 4 parallel analysis agents. Covers stack, architecture, conventions, and concerns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
604 lines
20 KiB
Markdown
604 lines
20 KiB
Markdown
# Coding Conventions
|
||
|
||
**Analysis Date:** 2026-03-12
|
||
|
||
---
|
||
|
||
## 1. Documented Rules (AGENTS.md / CLAUDE.md)
|
||
|
||
These rules are canonical and enforced across the project:
|
||
|
||
- Code must be readable by a stranger — clear names, no magic
|
||
- No logic in views, no copy-paste, no random helpers without coherent design
|
||
- Each function/class has **one responsibility**, typically 30–50 lines; split if longer
|
||
- Max **3 levels of nesting** (if/foreach); move deeper logic to separate methods
|
||
- Comments only where they explain **why**, never **what**
|
||
- DB: Medoo is mentioned in docs but the codebase uses **raw PDO** with prepared statements — no string-concatenated SQL for user input
|
||
- XSS: escape all output in views with the `e()` helper
|
||
- CSRF token required on all POST forms, validated via `Csrf::validate()`
|
||
- All styles live in `resources/scss/`, never inline in view files
|
||
- Never use native `alert()` / `confirm()` — use `window.OrderProAlerts.confirm()`
|
||
- Reusable UI blocks go in `resources/views/components/`
|
||
|
||
---
|
||
|
||
## 2. Naming Conventions
|
||
|
||
### PHP
|
||
|
||
| Element | Convention | Example |
|
||
|---|---|---|
|
||
| Classes | PascalCase | `OrdersRepository`, `ShipmentProviderRegistry` |
|
||
| Methods | camelCase | `findDetails()`, `updateOrderStatus()` |
|
||
| Variables | camelCase | `$orderId`, `$statusLabelMap` |
|
||
| Constants | UPPER_SNAKE_CASE | `SESSION_KEY` (in `Csrf`) |
|
||
| Namespaces | PascalCase, PSR-4 | `App\Modules\Orders`, `App\Core\Http` |
|
||
| Interfaces | PascalCase + `Interface` suffix | `ShipmentProviderInterface` |
|
||
| Test classes | PascalCase + `Test` suffix | `CronJobTypeTest` |
|
||
|
||
No abbreviations in names. `$orderId` not `$oid`. Loop variables in short 2–3 line loops are the only exception.
|
||
|
||
### Files
|
||
|
||
- One class per file, filename matches class name exactly
|
||
- `PascalCase.php` for all PHP class files
|
||
- `kebab-case` or `snake_case` not used in PHP
|
||
|
||
### CSS / SCSS
|
||
|
||
- BEM-style class naming: `block__element--modifier`
|
||
- Examples: `.sidebar__brand`, `.orders-ref__meta`, `.btn--primary`, `.is-active`
|
||
- State modifier classes: `.is-active`, `.is-collapsed`, `.is-open`, `.is-disabled`
|
||
- JavaScript hook classes: `.js-` prefix (e.g. `.js-sidebar-collapse`, `.js-filter-toggle-btn`)
|
||
|
||
---
|
||
|
||
## 3. PHP Code Style
|
||
|
||
### Strict Types
|
||
|
||
Every PHP file starts with:
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
```
|
||
No exceptions observed across the entire `src/` tree.
|
||
|
||
### Class Declaration
|
||
|
||
All classes are `final` unless specifically designed for extension (none in this codebase are open for extension):
|
||
```php
|
||
final class OrdersRepository
|
||
{
|
||
public function __construct(private readonly PDO $pdo)
|
||
{
|
||
}
|
||
}
|
||
```
|
||
|
||
Constructor property promotion is used throughout:
|
||
```php
|
||
public function __construct(
|
||
private readonly Template $template,
|
||
private readonly Translator $translator,
|
||
private readonly AuthService $auth,
|
||
private readonly OrdersRepository $orders,
|
||
private readonly ?ShipmentPackageRepository $shipmentPackages = null
|
||
) {
|
||
}
|
||
```
|
||
|
||
### Return Types and Type Hints
|
||
|
||
All method signatures carry full PHP 8.x type declarations. Return types always declared:
|
||
```php
|
||
public function findDetails(int $orderId): ?array
|
||
public function updateOrderStatus(int $orderId, string $newStatusCode, ...): bool
|
||
public function run(int $limit): array
|
||
```
|
||
|
||
PHPDoc `@param` and `@return` annotations are added for complex array shapes:
|
||
```php
|
||
/**
|
||
* @return array{items:array<int, array<string, mixed>>, total:int, page:int, per_page:int, error:string}
|
||
*/
|
||
public function paginate(array $filters): array
|
||
```
|
||
|
||
### Defensive Casting
|
||
|
||
All values from external sources (request input, DB rows) are cast explicitly before use:
|
||
```php
|
||
$orderId = max(0, (int) $request->input('id', 0));
|
||
$search = trim((string) ($filters['search'] ?? ''));
|
||
$totalPaid = $row['total_paid'] !== null ? (float) $row['total_paid'] : null;
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Design Patterns
|
||
|
||
### Service Layer Pattern
|
||
|
||
Business logic lives in dedicated `*Service` classes, not controllers or repositories:
|
||
- `src/Modules/Settings/AllegroOrderImportService.php` — order import logic
|
||
- `src/Modules/Settings/ShopproStatusSyncService.php` — status sync logic
|
||
- `src/Modules/Settings/ShopproPaymentStatusSyncService.php`
|
||
|
||
Controllers delegate to services, which delegate to repositories.
|
||
|
||
### Repository Pattern
|
||
|
||
All database access goes through `*Repository` classes. No direct PDO calls in controllers or services beyond what repositories expose:
|
||
- `src/Modules/Orders/OrdersRepository.php`
|
||
- `src/Modules/Settings/AllegroIntegrationRepository.php`
|
||
- `src/Modules/Shipments/ShipmentPackageRepository.php`
|
||
|
||
### Interface + Registry Pattern
|
||
|
||
Provider integrations use an interface + registry:
|
||
```php
|
||
// src/Modules/Shipments/ShipmentProviderInterface.php
|
||
interface ShipmentProviderInterface
|
||
{
|
||
public function code(): string;
|
||
public function createShipment(int $orderId, array $formData): array;
|
||
// ...
|
||
}
|
||
|
||
// src/Modules/Shipments/ShipmentProviderRegistry.php
|
||
final class ShipmentProviderRegistry
|
||
{
|
||
public function get(string $code): ?ShipmentProviderInterface { ... }
|
||
public function all(): array { ... }
|
||
}
|
||
```
|
||
Registered at boot time in `routes/web.php`:
|
||
```php
|
||
$shipmentProviderRegistry = new ShipmentProviderRegistry([
|
||
$shipmentService, // AllegroShipmentService
|
||
$apaczkaShipmentService,
|
||
]);
|
||
```
|
||
|
||
### Cron Handler Pattern
|
||
|
||
Cron jobs follow a handler contract. Each job type has a dedicated handler class with a `handle(array $payload): array` method:
|
||
- `src/Modules/Cron/AllegroOrdersImportHandler.php`
|
||
- `src/Modules/Cron/ShopproStatusSyncHandler.php`
|
||
- `src/Modules/Cron/AllegroTokenRefreshHandler.php`
|
||
|
||
Handlers are registered as a string-keyed array in `CronRunner`:
|
||
```php
|
||
$runner = new CronRunner($repository, $logger, [
|
||
'allegro_orders_import' => new AllegroOrdersImportHandler($ordersSyncService),
|
||
'shoppro_order_status_sync' => new ShopproStatusSyncHandler($shopproStatusSyncService),
|
||
]);
|
||
```
|
||
|
||
### Manual Dependency Injection (No Container)
|
||
|
||
There is no DI container. All dependencies are wired manually in `routes/web.php` and `Application.php`. The Application class acts as a **Service Locator** for top-level objects (`$app->db()`, `$app->orders()`, `$app->logger()`), but controllers and services receive dependencies through constructor injection.
|
||
|
||
### Middleware Pipeline
|
||
|
||
Middleware uses a `handle(Request $request, callable $next): Response` or `__invoke` signature. The router builds a pipeline with `array_reduce`:
|
||
```php
|
||
// src/Core/Routing/Router.php
|
||
$pipeline = array_reduce(
|
||
array_reverse($middlewares),
|
||
fn (callable $next, callable|object $middleware): callable => $this->wrapMiddleware($middleware, $next),
|
||
fn (Request $req): mixed => $handler($req)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Database Access
|
||
|
||
### Driver
|
||
|
||
Raw **PDO** with MySQL (`src/Core/Database/ConnectionFactory.php`). **No ORM**, no query builder (Medoo is referenced in docs but not in actual code). PDO is configured with:
|
||
```php
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||
PDO::ATTR_EMULATE_PREPARES => false,
|
||
```
|
||
|
||
### Prepared Statements — Mandatory
|
||
|
||
**All** user-supplied data goes through prepared statements with named parameters:
|
||
```php
|
||
$stmt = $this->pdo->prepare(
|
||
'UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id'
|
||
);
|
||
$stmt->execute(['status' => $newStatusCode, 'id' => $orderId]);
|
||
```
|
||
|
||
For `IN (...)` clauses with a dynamic list, positional `?` placeholders are used and values are bound with `bindValue`:
|
||
```php
|
||
$placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
|
||
$stmt = $this->pdo->prepare('SELECT ... WHERE oi.order_id IN (' . $placeholders . ')');
|
||
foreach ($cleanIds as $index => $orderId) {
|
||
$stmt->bindValue($index + 1, $orderId, PDO::PARAM_INT);
|
||
}
|
||
```
|
||
|
||
Dynamic `ORDER BY` columns are resolved with `match` expressions against a hard-coded allowlist — never interpolated from user input:
|
||
```php
|
||
$sortColumn = match ($sort) {
|
||
'source_order_id' => 'o.source_order_id',
|
||
'total_with_tax' => 'o.total_with_tax',
|
||
default => $effectiveOrderedAtSql,
|
||
};
|
||
```
|
||
|
||
### SQL Style
|
||
|
||
Multi-line SQL strings use string concatenation only for safe, code-generated SQL fragments (not user data). Named SQL helper methods extract reusable SQL snippets:
|
||
```php
|
||
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
|
||
{
|
||
return 'CASE WHEN ' . $orderAlias . '.source = "allegro" AND ...';
|
||
}
|
||
|
||
private function effectiveOrderedAtSql(string $orderAlias): string
|
||
{
|
||
return 'COALESCE(' . $orderAlias . '.ordered_at, ' . $orderAlias . '.source_created_at, ...)';
|
||
}
|
||
```
|
||
|
||
### Error Handling in DB Layer
|
||
|
||
Repository methods catch `Throwable` and return safe defaults (empty array, `null`, or `false`) rather than letting exceptions propagate to the controller:
|
||
```php
|
||
try {
|
||
$rows = $this->pdo->query('SELECT ...')->fetchAll(PDO::FETCH_COLUMN);
|
||
} catch (Throwable) {
|
||
return [];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Frontend Patterns
|
||
|
||
### View System
|
||
|
||
Plain PHP views rendered via `src/Core/View/Template.php`. The `render()` method:
|
||
1. Renders the view file with `extract($data)` — data keys become local variables
|
||
2. Wraps it in a layout via a second `renderFile()` call, injecting `$content`
|
||
|
||
```php
|
||
$html = $this->template->render('orders/list', $data, 'layouts/app');
|
||
```
|
||
|
||
Two helpers are injected into every view scope:
|
||
```php
|
||
$e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
$t = static fn (string $key, array $replace = []): string => $translator->get($key, $replace);
|
||
```
|
||
|
||
**Always use `$e()` for output.** Use `<?= (string) $rawHtml ?>` (raw echo) only when the value is pre-escaped HTML built in the controller.
|
||
|
||
### View Structure
|
||
|
||
```
|
||
resources/views/
|
||
├── layouts/
|
||
│ ├── app.php # Main authenticated layout (sidebar + topbar)
|
||
│ └── auth.php # Login layout
|
||
├── components/
|
||
│ ├── table-list.php # Generic paginated table with filters, sorting, column toggle
|
||
│ └── order-status-panel.php
|
||
├── orders/
|
||
│ ├── list.php
|
||
│ └── show.php
|
||
├── settings/
|
||
│ └── *.php # One view per settings section
|
||
└── shipments/
|
||
└── prepare.php
|
||
```
|
||
|
||
Views are **data receivers**, not logic writers. All HTML fragment generation for complex cells (status badges, product previews) happens in controller methods and is passed as pre-built HTML strings with `raw: true` in the table config.
|
||
|
||
### Reusable Components
|
||
|
||
The `resources/views/components/table-list.php` component accepts a `$tableList` config array and renders a full paginated table with filters, column toggles, and action forms. Include it with:
|
||
```php
|
||
<?php include $this->resolve('components/table-list') ?>
|
||
// or via the view data key 'tableList'
|
||
```
|
||
|
||
The component reads its own config defensively:
|
||
```php
|
||
$config = is_array($tableList ?? null) ? $tableList : [];
|
||
$rows = is_array($config['rows'] ?? null) ? $config['rows'] : [];
|
||
```
|
||
|
||
### SCSS / CSS
|
||
|
||
Source: `resources/scss/`
|
||
Compiled output: `public/assets/css/`
|
||
Build command: `npm run build:assets` (uses Dart Sass)
|
||
|
||
```
|
||
resources/scss/
|
||
├── app.scss # Main panel stylesheet
|
||
├── login.scss # Auth page stylesheet
|
||
└── shared/
|
||
└── _ui-components.scss # Shared UI primitives (imported with @use)
|
||
```
|
||
|
||
**Never put styles in view files.** All styles go in SCSS. Cache-busting on the compiled CSS is done via `filemtime()` in the layout:
|
||
```php
|
||
<link rel="stylesheet" href="/assets/css/app.css?ver=<?= filemtime(...) ?: 0 ?>">
|
||
```
|
||
|
||
### JavaScript
|
||
|
||
No bundler, no TypeScript. Vanilla JavaScript only (ES5 style with `var` in components, ES6 `const/let` allowed in module files).
|
||
|
||
**Module pattern:** All JS in views is wrapped in an IIFE to avoid global scope pollution:
|
||
```js
|
||
(function() {
|
||
// component code
|
||
})();
|
||
```
|
||
|
||
**UI Alerts and Confirmations:** The custom `window.OrderProAlerts` module (built from `resources/modules/jquery-alerts/`) is used for all confirmations. Never use `window.confirm()` directly. The pattern in table-list:
|
||
```js
|
||
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
|
||
window.OrderProAlerts.confirm({ title, message, ... }).then(function(accepted) {
|
||
if (!accepted) return;
|
||
form.setAttribute('data-confirmed', '1');
|
||
form.submit();
|
||
});
|
||
}
|
||
```
|
||
|
||
**DOM targeting:** Use `data-` attributes and `js-` prefixed class names for JS hooks. Never use presentation class names (`.btn--primary`) as JS selectors.
|
||
|
||
**localStorage:** Filter state, column visibility, and sidebar collapsed state are persisted in `localStorage` with namespaced keys (e.g. `tableList_/orders/list_orders_filters_open`).
|
||
|
||
---
|
||
|
||
## 7. Security
|
||
|
||
### CSRF
|
||
|
||
All POST forms include a hidden token field:
|
||
```html
|
||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||
```
|
||
|
||
Controllers validate it before processing:
|
||
```php
|
||
$csrfToken = (string) $request->input('_csrf_token', '');
|
||
if (!Csrf::validate($csrfToken)) {
|
||
$_SESSION['order_flash_error'] = $this->translator->get('auth.errors.csrf_expired');
|
||
return Response::redirect('/orders/' . $orderId);
|
||
}
|
||
```
|
||
|
||
`Csrf::validate()` uses `hash_equals()` to prevent timing attacks (`src/Core/Security/Csrf.php`).
|
||
|
||
### XSS
|
||
|
||
Every dynamic value echoed in a view must go through `$e()`:
|
||
```php
|
||
<?= $e($buyerName) ?>
|
||
<?= $e($t('navigation.orders')) ?>
|
||
```
|
||
|
||
Pre-built HTML fragments (e.g. status badge HTML assembled in controller) are output raw and must already be escaped internally using `htmlspecialchars(..., ENT_QUOTES, 'UTF-8')`.
|
||
|
||
### Secrets Encryption
|
||
|
||
Integration credentials (API keys, tokens) are encrypted at rest using AES-256-CBC with HMAC-SHA256 authentication (`src/Modules/Settings/IntegrationSecretCipher.php`). The cipher uses a key derived from `INTEGRATIONS_SECRET` env var. Encrypted values are prefixed with `v1:`.
|
||
|
||
---
|
||
|
||
## 8. Error Handling
|
||
|
||
### Global Handler
|
||
|
||
Unhandled exceptions are caught in `Application::registerErrorHandlers()`:
|
||
```php
|
||
set_exception_handler(function (Throwable $exception) use ($debug): void {
|
||
$this->logger->error('Unhandled exception', [
|
||
'message' => $exception->getMessage(),
|
||
'file' => $exception->getFile(),
|
||
'line' => $exception->getLine(),
|
||
]);
|
||
|
||
$message = $debug ? $exception->getMessage() : 'Internal server error';
|
||
Response::html($message, 500)->send();
|
||
});
|
||
```
|
||
|
||
### Repository Layer
|
||
|
||
Repositories return safe fallback values on error and never expose raw exception messages to callers:
|
||
```php
|
||
try {
|
||
// ...
|
||
} catch (Throwable $exception) {
|
||
return [
|
||
'items' => [],
|
||
'total' => 0,
|
||
'error' => $exception->getMessage(), // returned in the data, not thrown
|
||
];
|
||
}
|
||
```
|
||
|
||
For methods returning `bool`, failure is signaled by returning `false`:
|
||
```php
|
||
public function updateOrderStatus(...): bool
|
||
{
|
||
try { /* ... */ return true; }
|
||
catch (Throwable) { return false; }
|
||
}
|
||
```
|
||
|
||
For methods returning `?array`, failure returns `null`.
|
||
|
||
### Cron Layer
|
||
|
||
The `CronRunner` catches per-job exceptions, logs them, marks the job as failed, and continues processing the next job:
|
||
```php
|
||
} catch (Throwable $exception) {
|
||
$this->repository->markJobFailed($jobId, $exception->getMessage(), ...);
|
||
$this->logger->error('Cron job failed', [
|
||
'job_id' => $jobId,
|
||
'job_type' => $jobType,
|
||
'error' => $exception->getMessage(),
|
||
]);
|
||
$failed++;
|
||
}
|
||
```
|
||
|
||
### Logger
|
||
|
||
`src/Core/Support/Logger.php` — custom file logger writing to `storage/logs/`. Format:
|
||
```
|
||
[2026-03-12 14:22:01] ERROR Cron job failed {"job_id":42,"job_type":"allegro_orders_import","error":"..."}
|
||
```
|
||
|
||
Methods: `$logger->error(string $message, array $context = [])` and `$logger->info(...)`. Context is encoded as JSON on the same line. Log path is configured via `app.log_path` in `config/app.php`.
|
||
|
||
---
|
||
|
||
## 9. Testing
|
||
|
||
### Framework
|
||
|
||
PHPUnit 11.x. Config: `phpunit.xml` at project root.
|
||
|
||
```bash
|
||
composer test # vendor/bin/phpunit -c phpunit.xml --testdox
|
||
```
|
||
|
||
Settings:
|
||
- `failOnWarning="true"` — warnings are test failures
|
||
- `failOnRisky="true"` — risky tests fail
|
||
- `executionOrder="depends,defects"` — dependency-aware ordering
|
||
|
||
### Test File Location
|
||
|
||
Tests live in `tests/` (PSR-4 namespace `Tests\`), mirroring the `src/` structure:
|
||
```
|
||
tests/
|
||
├── bootstrap.php
|
||
└── Unit/
|
||
├── Cron/
|
||
│ └── CronJobTypeTest.php
|
||
└── Settings/
|
||
└── OrderStatusMappingRepositoryTest.php
|
||
```
|
||
|
||
**Note:** The active `tests/` directory currently contains only `bootstrap.php`. The working test files exist in `archive/2026-03-02_users-only-reset/tests/` — these are the reference patterns to follow when adding new tests.
|
||
|
||
### Test Class Pattern
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace Tests\Unit\Settings;
|
||
|
||
use App\Modules\Settings\OrderStatusMappingRepository;
|
||
use PDO;
|
||
use PHPUnit\Framework\Attributes\CoversClass;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
#[CoversClass(OrderStatusMappingRepository::class)]
|
||
final class OrderStatusMappingRepositoryTest extends TestCase
|
||
{
|
||
private PDO $pdo;
|
||
private OrderStatusMappingRepository $repository;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->pdo = new PDO('sqlite::memory:');
|
||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
$this->pdo->exec('CREATE TABLE ...'); // schema DDL
|
||
$this->repository = new OrderStatusMappingRepository($this->pdo);
|
||
}
|
||
|
||
public function testReplaceAndReadMappingsForIntegration(): void
|
||
{
|
||
$this->repository->replaceForIntegration(10, [...]);
|
||
$rows = $this->repository->listByIntegration(10);
|
||
self::assertArrayHasKey('new', $rows);
|
||
self::assertSame('completed', $rows['paid']['orderpro_status_code']);
|
||
}
|
||
}
|
||
```
|
||
|
||
Key patterns:
|
||
- Use `#[CoversClass(ClassName::class)]` PHP 8 attribute on every test class
|
||
- SQLite in-memory database for repository tests (no real DB connection needed)
|
||
- Test method names start with `test` and describe behavior: `testReplaceAndReadMappingsForIntegration`
|
||
- Use `self::assertSame()` (strict) not `assertEquals()`
|
||
- Classes and methods are `final`
|
||
- No mocking framework observed — tests use real classes with SQLite in-memory
|
||
|
||
### What to Test
|
||
|
||
Repository tests test read/write behavior against an in-memory SQLite DB. Value object / enum tests verify constants and helper methods. No integration tests against the real MySQL database.
|
||
|
||
---
|
||
|
||
## 10. Code Quality Tools
|
||
|
||
### SonarQube
|
||
|
||
Configured via `sonar-project.properties`:
|
||
```
|
||
sonar.sources=src,resources/views,routes
|
||
sonar.tests=tests
|
||
sonar.exclusions=archive/**,node_modules/**,vendor/**,public/assets/**,storage/**
|
||
sonar.php.version=8.1
|
||
```
|
||
Runs against an external SonarQube server at `https://sonar.project-pro.pl`.
|
||
|
||
### PHPUnit (Coverage Source)
|
||
|
||
`phpunit.xml` configures `src/` as the source for coverage analysis.
|
||
|
||
### No Local Static Analysis Config
|
||
|
||
No `phpstan.neon`, `.phpcs.xml`, or `psalm.xml` are present. SonarQube is the sole static analysis tool. PHP_CS_Fixer and PHPStan are not in `composer.json`.
|
||
|
||
---
|
||
|
||
## 11. Import / Use Statement Organization
|
||
|
||
Use statements are grouped and ordered:
|
||
1. Internal `App\Core\*` namespaces
|
||
2. Internal `App\Modules\*` namespaces
|
||
3. PHP built-in classes (`PDO`, `DateTimeImmutable`, `RuntimeException`, `Throwable`)
|
||
|
||
Each `use` on its own line. No `use` grouping with `{}`.
|
||
|
||
---
|
||
|
||
## 12. Configuration
|
||
|
||
App config is split by concern in `config/`:
|
||
- `config/app.php` — application settings (paths, locale, session, logging, cron)
|
||
- `config/database.php` — DB connection parameters
|
||
|
||
The `Application::config(string $key, mixed $default)` method resolves dot-notation keys:
|
||
```php
|
||
$app->config('app.integrations.secret', '')
|
||
$app->config('database.host')
|
||
```
|
||
|
||
Environment variables are loaded via `src/Core/Support/Env.php` and read in config files.
|
||
|
||
---
|
||
|
||
*Convention analysis: 2026-03-12*
|