184 lines
5.4 KiB
Markdown
184 lines
5.4 KiB
Markdown
# Conventions & Patterns
|
|
|
|
## Naming
|
|
|
|
| Element | Convention | Example |
|
|
|---------|-----------|---------|
|
|
| Classes | PascalCase | `OrdersController`, `AllegroApiClient` |
|
|
| Methods / variables | camelCase | `findDetails()`, `$statusCode` |
|
|
| Constants | UPPER_SNAKE_CASE | `SESSION_KEY`, `OPTION_KEYS` |
|
|
| DB columns | snake_case | `source_order_id`, `payment_status` |
|
|
| PHP files | Match class name | `OrdersController.php` |
|
|
| View files | kebab-case | `table-list.php`, `order-status-panel.php` |
|
|
| SCSS partials | `_kebab-case.scss` | `_automation.scss` |
|
|
| No abbreviations | Full names | `$translatedText` not `$t` (except loop indices) |
|
|
|
|
## Code Constraints (CLAUDE.md)
|
|
|
|
- Max **~50 lines** per method; longer → split
|
|
- Max **3 nesting levels** (if/foreach); deeper → extract to method
|
|
- Single Responsibility: one class = one job
|
|
- All classes are `final` (no accidental inheritance)
|
|
- `declare(strict_types=1)` in every file
|
|
- Comments only for **WHY**, never for WHAT
|
|
|
|
## Database Pattern
|
|
|
|
**PDO prepared statements only — no ORM, no string concatenation.**
|
|
|
|
```php
|
|
// Correct
|
|
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id');
|
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
|
|
// Never
|
|
$pdo->query("SELECT * FROM orders WHERE id = $id"); // forbidden
|
|
```
|
|
|
|
- `ATTR_EMULATE_PREPARES = false` (real server-side preparation)
|
|
- `ATTR_ERRMODE = ERRMODE_EXCEPTION`
|
|
- Parameter type hints: `PDO::PARAM_INT` for integers
|
|
|
|
## Security Patterns
|
|
|
|
### CSRF
|
|
|
|
```php
|
|
// Generate (in controller)
|
|
'csrfToken' => Csrf::token() // stores in $_SESSION['_csrf_token']
|
|
|
|
// In view
|
|
<input type="hidden" name="_token" value="<?= $e($csrfToken) ?>">
|
|
|
|
// Validate (in controller)
|
|
if (!Csrf::validate((string) $request->input('_token', ''))) { ... }
|
|
```
|
|
|
|
Field name is always `_token`. Uses `hash_equals()` for timing-safe comparison.
|
|
|
|
### XSS Escaping
|
|
|
|
All user-controlled output escaped with `$e()` helper (available in all views):
|
|
|
|
```php
|
|
$e = fn(mixed $v): string => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
|
|
|
// Usage
|
|
<?= $e($order['customer_name']) ?>
|
|
<?= $e($t('orders.status.label')) ?>
|
|
```
|
|
|
|
**Never** output raw variables without `$e()`.
|
|
|
|
### Session
|
|
|
|
Configured with: `cookie_httponly=true`, `cookie_secure=true`, `cookie_samesite=Lax`, `use_strict_mode=true`.
|
|
Access via `Session::get()` / `Session::set()` helpers — not raw `$_SESSION` in business logic.
|
|
|
|
## Controller Pattern
|
|
|
|
```php
|
|
final class OrdersController {
|
|
public function __construct(
|
|
private readonly Template $template,
|
|
private readonly Translator $translator,
|
|
private readonly OrdersRepository $orders,
|
|
// ...
|
|
) {}
|
|
|
|
public function index(Request $request): Response {
|
|
// 1. Parse & validate input
|
|
$filters = ['search' => trim((string) $request->input('search', ''))];
|
|
|
|
// 2. Call repository
|
|
$result = $this->orders->paginate($filters);
|
|
|
|
// 3. Prepare view data
|
|
$rows = array_map(fn($row) => $this->toTableRow($row), $result['items']);
|
|
|
|
// 4. Render
|
|
return Response::html(
|
|
$this->template->render('orders/index', ['rows' => $rows], 'layouts/app')
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## View Pattern
|
|
|
|
Views use two magic helpers injected by `Template::renderFile()`:
|
|
- `$e($value)` — HTML-escape
|
|
- `$t($key, $replace)` — translate
|
|
|
|
Layout composition:
|
|
```php
|
|
$this->template->render('orders/index', $data, 'layouts/app')
|
|
// renders views/orders/index.php, wraps in views/layouts/app.php via $content
|
|
```
|
|
|
|
## UI Rules
|
|
|
|
### Alerts & Confirmations
|
|
- **Always** use `window.OrderProAlerts.confirm({message, onConfirm})` from `jquery-alerts.js`
|
|
- **Never** use native `alert()` or `confirm()`
|
|
|
|
### CSS / SCSS
|
|
- All styles go in `resources/scss/` — never inline `<style>` or `style=""` attributes in PHP templates
|
|
- CSS custom properties for dynamic colors: `style="--status-color: <?= $e($color) ?>"` → used via `var(--status-color)` in SCSS
|
|
- Build: `npm run build:assets`
|
|
- UI must be **compact** — maximize info density, minimize whitespace
|
|
|
|
### Reusable Components
|
|
- Extract repeated UI blocks to `resources/views/components/`
|
|
- Current components: `table-list.php`, `order-status-panel.php`
|
|
- Changes to a component must be verified in **all** places it is used
|
|
|
|
## Flash Messages
|
|
|
|
```php
|
|
// Set (in controller)
|
|
Flash::set('error', $this->translator->get('auth.errors.csrf_expired'));
|
|
return Response::redirect('/login');
|
|
|
|
// Read (in view)
|
|
<?php if (!empty($errorMessage)): ?>
|
|
<div class="alert alert--danger"><?= $e($errorMessage) ?></div>
|
|
<?php endif; ?>
|
|
```
|
|
|
|
## Exception Hierarchy
|
|
|
|
```
|
|
OrderProException (base)
|
|
├── AllegroApiException
|
|
├── AllegroOAuthException
|
|
├── ApaczkaApiException
|
|
├── IntegrationConfigException
|
|
└── ShipmentException
|
|
```
|
|
|
|
Throw specific domain exceptions, not generic `\Exception`.
|
|
|
|
## Error Handling
|
|
|
|
Global exception handler in `Application::registerErrorHandlers()`:
|
|
- Always logs to `storage/logs/app.log` with JSON context
|
|
- Shows `message` in debug mode, `"Internal server error"` in production
|
|
|
|
Log format: `[2026-04-26 14:30:00] ERROR message {"context":"value"}`
|
|
|
|
## Routing Convention
|
|
|
|
```php
|
|
// Public
|
|
$router->get('/login', [AuthController::class, 'showLogin']);
|
|
$router->post('/login', [AuthController::class, 'login']);
|
|
|
|
// Authenticated
|
|
$router->get('/orders', [OrdersController::class, 'index'], [$authMiddleware]);
|
|
|
|
// JSON API with API key
|
|
$router->post('/api/print-jobs', [PrintApiController::class, 'store'], [$apiKeyMiddleware]);
|
|
```
|