Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

34KB

AGENT.md — PHP Coding Standard

This file defines the coding standards and working rules for AI agents and developers contributing to this PHP codebase. It is based on the principles from PHP: The Right Way and adapted into practical project instructions.

Source reference: https://phptherightway.com/


1. Core Philosophy

Write PHP that is:

  • Readable before clever.
  • Secure by default.
  • Consistent with community standards.
  • Easy to test, debug, and refactor.
  • Separated by responsibility: routing, controllers, services, models, persistence, templates, and configuration should not be mixed together.

PHP does not have only one canonical “right way,” so prefer widely accepted standards, documented project conventions, and clear tradeoffs over personal style.


2. PHP Version Standard

Use the current stable PHP version supported by the project.

Default expectation:

PHP 8.x+

Do not introduce code that depends on unsupported PHP versions unless the project explicitly targets a legacy runtime.

When adding a language feature, verify that it is supported by the project’s configured PHP version in composer.json.

Example:

{
  "require": {
    "php": ">=8.2"
  }
}

3. Coding Style

Follow recognized PHP standards unless the repository already defines stricter rules.

Preferred standards:

  • PSR-1: Basic Coding Standard
  • PSR-12: Extended Coding Style
  • PSR-4: Autoloading

Use automated tooling rather than manual formatting arguments.

Recommended tools:

composer require --dev squizlabs/php_codesniffer
composer require --dev friendsofphp/php-cs-fixer

Example checks:

vendor/bin/phpcs --standard=PSR12 src tests
vendor/bin/php-cs-fixer fix --dry-run --diff

Example fix:

vendor/bin/php-cs-fixer fix

Naming Rules

Use English names for code symbols and infrastructure.

Use:

class InvoiceRepository
{
    public function findByCustomerId(int $customerId): array
    {
        // ...
    }
}

Avoid unclear abbreviations:

class InvRepo
{
    public function fbcid($cid)
    {
        // ...
    }
}

Formatting Rules

  • Use <?php tags. Do not use short open tags.
  • Use strict types at the top of new PHP files when practical:
declare(strict_types=1);
  • One class per file.
  • Match namespaces to directory structure.
  • Keep functions and methods small and focused.
  • Prefer explicit visibility: public, protected, or private.
  • Avoid global state unless required by the framework or legacy integration.

4. Project Structure

Prefer a predictable structure.

Example:

project-root/
  public/
    index.php
  src/
    Controller/
    Service/
    Repository/
    Entity/
    ValueObject/
  templates/
  config/
  tests/
  var/
    cache/
    logs/
  vendor/
  composer.json

Rules:

  • public/ is the web root.
  • Do not expose src/, config/, tests/, vendor/, or .env files through the web server.
  • Put application code under src/.
  • Put generated cache/log files under var/ or another ignored runtime directory.
  • Keep secrets outside the web root.

5. Namespaces and Autoloading

All new application classes must use namespaces.

Use PSR-4 autoloading through Composer.

Example composer.json:

{
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}

After changing autoload rules, run:

composer dump-autoload

Example class:

<?php

declare(strict_types=1);

namespace App\Service;

final class InvoiceCalculator
{
    public function calculateTotal(array $items): int
    {
        // Return cents, not floating-point dollars.
        return array_sum(array_column($items, 'amountCents'));
    }
}

6. Dependency Management

Use Composer for PHP dependencies.

Rules:

  • Add packages with composer require or composer require --dev.
  • Commit composer.json and composer.lock for applications.
  • Do not manually copy vendor libraries into the project.
  • Do not edit files under vendor/.
  • Prefer maintained packages with clear documentation, tests, and recent releases.
  • Remove unused packages.

Commands:

composer install
composer update vendor/package
composer audit
composer validate

Use composer update intentionally. Do not casually update every dependency in unrelated work.


7. Object-Oriented Design

Prefer clear object-oriented code for domain and application logic.

Use classes for cohesive behavior:

final class CustomerName
{
    public function __construct(private string $value)
    {
        if (trim($value) === '') {
            throw new InvalidArgumentException('Customer name is required.');
        }
    }

    public function value(): string
    {
        return $this->value;
    }
}

Guidelines:

  • Keep controllers thin.
  • Put business rules in services or domain objects.
  • Put persistence logic in repositories or data access classes.
  • Use interfaces when multiple implementations are expected or when it improves testing.
  • Avoid huge “utility” classes.
  • Avoid magic methods unless they provide clear framework integration or a documented benefit.

8. Dependency Injection

Prefer dependency injection over creating dependencies inside classes.

Good:

final class RegisterUser
{
    public function __construct(
        private UserRepository $users,
        private PasswordHasher $passwords
    ) {
    }

    public function handle(string $email, string $plainPassword): void
    {
        $hash = $this->passwords->hash($plainPassword);
        $this->users->create($email, $hash);
    }
}

Avoid:

final class RegisterUser
{
    public function handle(string $email, string $plainPassword): void
    {
        $users = new UserRepository();
        $passwords = new PasswordHasher();
        // ...
    }
}

Rules:

  • Constructor injection is preferred for required dependencies.
  • Do not use service locators casually.
  • Do not hide dependencies in global variables.
  • Keep dependency containers at application boundaries, not inside domain logic.

9. Database Access

Use PDO or a well-maintained database abstraction layer/ORM.

Never concatenate untrusted input into SQL.

Bad:

$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];

Good:

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Rules:

  • Use prepared statements and bound parameters.
  • Validate input before using it in writes.
  • Keep SQL out of templates.
  • Keep database access out of controllers where practical.
  • Use transactions when multiple writes must succeed or fail together.
  • Do not rely only on client-side validation.
  • Do not expose raw database errors to users.

Transaction example:

$pdo->beginTransaction();

try {
    $orders->create($order);
    $auditLog->record('order.created', $order->id());
    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

10. Input Validation and Output Escaping

Treat all external data as untrusted.

Untrusted data includes:

  • $_GET
  • $_POST
  • $_REQUEST
  • $_COOKIE
  • $_SERVER
  • uploaded files
  • request bodies
  • session values
  • database values originally supplied by users
  • third-party API responses

Validate on Input

Example:

$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

if ($email === false || $email === null) {
    throw new InvalidArgumentException('A valid email address is required.');
}

Escape on Output

For HTML output:

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

Usage:

<p><?= e($user->name()) ?></p>

Rules:

  • Escape based on context: HTML, attribute, JavaScript, CSS, URL, SQL, shell.
  • Do not use the same escaping function for every context.
  • Prefer template engines with automatic escaping when appropriate.
  • Avoid allowing raw HTML from users. If required, sanitize with a proven whitelist sanitizer.
  • Use escapeshellarg() when passing controlled values to shell commands, and avoid shell execution when possible.
  • Never trust file paths supplied by users. Reject path traversal values such as ../, /, \, and null bytes.

11. Passwords and Authentication

Never store plain-text passwords.

Use PHP’s password API:

$hash = password_hash($plainPassword, PASSWORD_DEFAULT);

if (! password_verify($plainPassword, $hash)) {
    throw new RuntimeException('Invalid credentials.');
}

Rules:

  • Use password_hash() for new password hashes.
  • Use password_verify() for login checks.
  • Use password_needs_rehash() when algorithm/cost settings change.
  • Do not create your own password hashing algorithm.
  • Do not use general-purpose hashes like md5, sha1, or raw sha256 for passwords.
  • Rate-limit login attempts.
  • Regenerate session IDs after login.
  • Use secure, HTTP-only, SameSite cookies for sessions.

12. Serialization and Data Exchange

Do not call unserialize() on untrusted data.

Prefer JSON for data exchange:

$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
$json = json_encode($data, JSON_THROW_ON_ERROR);

Rules:

  • Use JSON_THROW_ON_ERROR for new code.
  • Validate decoded data before using it.
  • Avoid PHP serialization for data that crosses trust boundaries.

13. Configuration and Secrets

Rules:

  • Keep secrets out of source control.
  • Do not commit passwords, API keys, private keys, tokens, or production DSNs.
  • Store configuration outside the public web root.
  • Use environment variables or ignored local config files for secrets.
  • Provide a safe example file such as .env.example.

Example .gitignore entries:

.env
.env.local
/config/local.php
/var/cache/
/var/log/
/vendor/

Example .env.example:

APP_ENV=local
APP_DEBUG=true
DATABASE_URL=mysql://user:password@localhost:3306/app

14. Error Handling and Logging

Use exceptions for exceptional failure paths.

Development:

  • Show errors locally.
  • Log errors.
  • Use Xdebug when debugging complex issues.

Production:

  • Do not display errors to users.
  • Log errors to a secure log destination.
  • Return safe, generic error messages.
  • Preserve enough context in logs for troubleshooting.

Do not leak:

  • stack traces to users
  • SQL statements with secrets
  • environment variables
  • full filesystem paths
  • tokens or passwords

Example:

try {
    $service->handle($request);
} catch (Throwable $e) {
    $logger->error('Order processing failed.', [
        'exception' => $e,
        'requestId' => $requestId,
    ]);

    http_response_code(500);
    echo 'An unexpected error occurred.';
}

15. Templates and Views

Keep presentation separate from business logic.

Rules:

  • Do not query the database from templates.
  • Do not place business rules in templates.
  • Escape output by default.
  • Prefer simple view models or arrays passed into templates.
  • Use a template engine with automatic escaping when it fits the project.

Plain PHP template example:

<h1><?= e($pageTitle) ?></h1>

<ul>
    <?php foreach ($users as $user): ?>
        <li><?= e($user->name()) ?></li>
    <?php endforeach; ?>
</ul>

16. HTTP and Web Application Rules

Rules:

  • Use the front controller pattern where appropriate.
  • Keep routing separate from business logic.
  • Validate request methods.
  • Use CSRF protection for state-changing forms.
  • Use proper HTTP status codes.
  • Redirect after successful POST to avoid duplicate form submission.
  • Do not trust headers such as X-Forwarded-For unless configured behind a trusted proxy.

Example POST guard:

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method Not Allowed');
}

17. Security Checklist

Before completing any feature, verify:

  • All external input is validated.
  • All output is escaped for the correct context.
  • SQL uses prepared statements or safe query builders.
  • Authentication and authorization are checked server-side.
  • Secrets are not committed.
  • Errors are not exposed in production responses.
  • File uploads validate size, extension, MIME type, and storage path.
  • Passwords use password_hash() and password_verify().
  • CSRF protection exists for state-changing requests.
  • Dangerous functions are avoided or justified: eval, exec, shell_exec, system, passthru, unserialize.
  • Dependencies have no known vulnerabilities according to composer audit.

18. Testing Standard

Automated tests are expected for new behavior.

Preferred tools:

  • PHPUnit
  • Pest, if the project already uses it

Rules:

  • Add or update tests with every behavior change.
  • Cover success paths and failure paths.
  • Unit-test business logic.
  • Integration-test database and framework wiring where useful.
  • Functional-test important user flows.
  • Avoid relying on var_dump() or manual browser testing as the only verification.

Example PHPUnit test:

final class InvoiceCalculatorTest extends TestCase
{
    public function testItCalculatesTotalInCents(): void
    {
        $calculator = new InvoiceCalculator();

        $total = $calculator->calculateTotal([
            ['amountCents' => 1000],
            ['amountCents' => 2500],
        ]);

        self::assertSame(3500, $total);
    }
}

Run tests:

vendor/bin/phpunit

19. Static Analysis and Quality Gates

Use static analysis when available.

Recommended tools:

composer require --dev phpstan/phpstan
composer require --dev vimeo/psalm

Common quality commands:

composer validate
composer audit
vendor/bin/phpcs --standard=PSR12 src tests
vendor/bin/phpunit
vendor/bin/phpstan analyse src tests

Do not ignore tool failures without documenting why.


20. Documentation

Use PHPDoc where it adds clarity, especially for arrays, generics-like structures, complex return values, and public APIs.

Good:

/**
 * @return list<Customer>
 */
public function findActiveCustomers(): array
{
    // ...
}

Avoid noisy comments that repeat the code:

// Increment i by one.
$i++;

Rules:

  • Explain why, not just what.
  • Document non-obvious tradeoffs.
  • Keep README setup instructions current.
  • Update examples when behavior changes.

21. Performance and Caching

Rules:

  • Measure before optimizing.
  • Avoid unnecessary database queries in loops.
  • Use pagination for large result sets.
  • Cache expensive reads where appropriate.
  • Use OPcache in production.
  • Do not cache user-specific sensitive data in shared caches without a clear key strategy.

22. Agent Workflow

When modifying this codebase, the AI agent must:

  1. Inspect existing project conventions before adding new patterns.
  2. Prefer small, focused changes.
  3. Preserve public behavior unless explicitly asked to change it.
  4. Add or update tests when behavior changes.
  5. Run relevant checks when possible.
  6. Explain any checks that could not be run.
  7. Avoid introducing new dependencies unless they solve a clear problem.
  8. Never place secrets in code, tests, fixtures, logs, or documentation.
  9. Keep generated code consistent with this file.
  10. Leave the repository better organized than it was found.

23. Pull Request / Review Checklist

Before considering work complete:

  • Code follows PSR-12 or project-specific style.
  • Namespaces and autoloading are correct.
  • Composer files are valid.
  • No unrelated dependency updates were introduced.
  • New behavior is tested.
  • Existing tests pass.
  • SQL is parameterized.
  • User input is validated.
  • Output is escaped.
  • No secrets are committed.
  • Errors are handled safely.
  • Documentation was updated where needed.

24. Legacy PHP Exception Policy

If this project contains legacy PHP:

  • Do not rewrite large areas without approval.
  • Add tests around legacy behavior before refactoring.
  • Improve safety incrementally.
  • Replace deprecated patterns as touched.
  • Avoid mixing modernization with unrelated feature work.
  • Document any compatibility constraints.

Legacy code should still move toward:

  • Composer autoloading
  • Namespaces
  • PDO/prepared statements
  • Centralized configuration
  • Automated tests
  • Safer error handling

25. Non-Negotiable Rules

The agent must not:

  • Commit secrets.
  • Build SQL using untrusted string concatenation.
  • Store plain-text passwords.
  • Use md5, sha1, or raw fast hashes for passwords.
  • Display production errors to users.
  • unserialize() untrusted data.
  • Put database queries in templates.
  • Edit files under vendor/.
  • Add dependencies without a clear reason.
  • Ignore failing tests or quality checks without explanation.

A project may include scripts like this:

{
  "scripts": {
    "test": "phpunit",
    "style": "phpcs --standard=PSR12 src tests",
    "style:fix": "php-cs-fixer fix",
    "analyse": "phpstan analyse src tests",
    "quality": [
      "@style",
      "@test",
      "@analyse"
    ]
  }
}

Then run:

composer quality

27. Final Instruction to Coding Agents

When in doubt, choose the boring, obvious, secure PHP solution:

  • Composer-managed dependencies
  • PSR-style code
  • Namespaced classes
  • Dependency injection
  • PDO prepared statements
  • Escaped output
  • Tested behavior
  • Clear errors and logs
  • No secrets in source control

Project Overview

This project is a small PHP MVC framework called MindVisionCode PHP.

It is intentionally inspired by a Classic ASP MVC framework style:

  • Central dispatcher
  • Controllers and actions
  • ViewModels
  • Repository classes
  • Simple validation
  • Database migrations
  • Small, readable files
  • Minimal dependencies

Do not turn this into Laravel, Symfony, Slim, or another large framework.

Tech Stack

  • PHP 8.2+
  • Composer
  • PSR-4 autoloading
  • PDO
  • PHP views
  • Optional SQLite/MySQL/SQL Server through PDO

Development Commands

Install dependencies:

composer install

Regenerate autoload files:

composer dump-autoload

Run local server:

php -S localhost:8000 -t public

Run basic tests:

php tests/run.php

Coding Rules

  • Keep code simple and readable.
  • Prefer small classes.
  • Use typed properties and return types where practical.
  • Avoid hidden magic.
  • Do not add dependencies without a clear reason.
  • Preserve the framework style.
  • Explain any architectural changes.

Request Flow

Browser → public/index.php → Request → Dispatcher → Router → Route → Controller → ViewModel/Repository → View → Response

Creating Table

When you create tables and code for a table you will need a corrisponding _audit table with audit_id , id that refrences the object id , an action R I U D , the fields in json , username and created at

Color System

Visual direction: calm operational clarity with high-signal status semantics.

Core palette:

  • Primary: #1F4E79 (municipal navy; trust and structure)
  • Secondary: #0F766E (teal support/action)
  • Accent: #2563EB (interactive focus/action emphasis)

Semantic status palette:

  • Success / On Track: #2E7D32
  • Warning / At Risk: #B45309
  • Error / Blocked: #B91C1C
  • Info / Neutral Progress: #2563EB
  • Overdue / Critical Flag: #7F1D1D

Neutral foundation:

  • Background: #F7F9FC
  • Surface: #FFFFFF
  • Border: #D0D7E2
  • Primary Text: #111827
  • Secondary Text: #4B5563

Ant Design token strategy:

  • Configure global tokens first (colorPrimary, colorSuccess, colorWarning, colorError, colorInfo, colorBgBase, colorText, borderRadius, density-related sizing).
  • Keep status meaning consistent across tables, tags, timelines, and alerts.

Typography System

Tone: professional, clear, non-decorative, optimized for dense operational reading.

Type stack:

  • Primary UI text: Public Sans, Segoe UI, Arial, sans-serif
  • Data-heavy/labels fallback: IBM Plex Sans, Segoe UI, sans-serif
  • Monospace for IDs/reference codes: IBM Plex Mono, Consolas, monospace

Type hierarchy (desktop-first):

  • H1: 28px / 36px / 600
  • H2: 22px / 30px / 600
  • H3: 18px / 26px / 600
  • Body default: 14px / 22px / 400
  • Dense table/body compact: 13px / 20px / 400
  • Caption/meta: 12px / 18px / 400

Typography principles:

  • Prioritize legibility over brand flourish.
  • Keep numeric/date fields highly scannable.
  • Use consistent casing and label patterns for form-heavy workflows.

Spacing & Layout Foundation

Layout direction: compact, structured, desktop-optimized operational workspace.

Spacing system:

  • Base unit: 8px
  • Micro spacing for dense table controls: 4px
  • Section spacing rhythm: 16px / 24px / 32px

Grid and containers:

  • Desktop-first 12-column grid
  • Content max-width bands for readability in wide monitors
  • Persistent side regions for risk queue/filter panels when useful

Component density rules:

  • Default to compact controls for grid and forms
  • Keep touch-target inflation out of scope (PC-only product)
  • Reserve larger spacing only for high-risk confirmations/modals

Visual rhythm:

  • Clear row grouping and section boundaries
  • Strong alignment around key operational identifiers (municipality, cycle, status, due date)

Accessibility Considerations

  • WCAG 2.2 AA minimum contrast targets:
    • 4.5:1 for normal text
    • 3:1 for large text and UI components
  • Keyboard-first operation:
    • Visible 2px focus indicators on interactive elements
    • Logical tab order in grids, forms, drawers, and modals
  • Do not rely on color alone:
    • Pair status colors with labels/icons/patterns
  • Motion and feedback:
    • Subtle motion only for orientation, not decoration
    • Respect reduced-motion preferences
  • Dense-data readability:
    • Preserve minimum font sizes and row heights that remain readable during long operational sessions

UI Design

  • Refer to template info in the files docs\ux-color-themes.html and docs\ux-design-directions.html

UI/UX Component Rules

These rules define the established UI patterns for this project. Follow them exactly when building new pages, views, or components. Do not invent new patterns — extend the existing ones.


Navigation

Link structure Every nav link must be an <a class="nav-link"> with:

  • An inline SVG icon (14×14, fill="none", stroke="currentColor", stroke-width="1.75", Heroicons-style path).
  • The label text immediately after the icon.
  • aria-hidden="true" on the SVG.
<a class="nav-link" href="/campaigns">
    <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24" aria-hidden="true">
        <path stroke-linecap="round" stroke-linejoin="round" d="..."/>
    </svg>
    Campaigns
</a>

Group separators Nav links are divided into logical groups separated by <span class="nav-sep" aria-hidden="true"></span>:

  • Group 1: Home
  • Group 2: Campaigns, Campaign Types
  • Group 3: Jobs, Job Types
  • Then a separator before the logged-in user name and logout button.

Add a separator between groups and before the auth area. Never put a separator at the very start or end of the nav.

Active state Add is-active class to the current page's link. Use exact match for /, prefix match (str_starts_with) for all others.

Logout button The logout button must use class="nav-logout-btn" — never button button-secondary. It is a ghost outlined button styled for the dark header background. Do not use the standard button classes here.

Contrast Nav link text uses rgba(255, 255, 255, 0.88). Never go below 0.85 — the minimum for WCAG AA on the #1F4E79 header.

Focus rings All nav links and the logout button have outline: 2px solid rgba(255, 255, 255, 0.6) on :focus-visible. Do not remove this.


Page Layout

Every page content area uses a .content-stack as its root element. This is a CSS grid with gap: 24px that stacks the page toolbar, panels, and other sections vertically.

<section class="content-stack">
    <div class="page-toolbar">...</div>
    <section class="section-panel">...</section>
</section>

Page toolbar (.page-toolbar) always contains:

  • A .section-heading div with an <h1> and optional <p> description on the left.
  • A primary action button (or back link) on the right.
<div class="page-toolbar">
    <div class="section-heading">
        <h1>Page Title</h1>
        <p>One-line description of what this page manages.</p>
    </div>
    <a class="button button-primary" href="/resource/create">+ New Resource</a>
</div>

Page h1 size is 32px / 40px line-height (set in .section-heading h1). Do not override this inline.


Section Panels

Every content block on a page must be wrapped in <section class="section-panel">. Key rules:

  • The left edge always has border-left: 3px solid var(--primary) — this is set globally in CSS and must not be removed or overridden.
  • Every panel that has a header must use .panel-header, which automatically adds a border-bottom separator below it.
  • .panel-header contains a left <div> with <h2> + optional <p>, and a right side for action buttons.
  • Panel <h2> is 18px. Do not make it larger than the page <h1>.
<section class="section-panel">
    <div class="panel-header">
        <div>
            <h2>Section Title</h2>
            <p>Brief description of this section's content.</p>
        </div>
        <button class="button button-secondary" type="button">Action</button>
    </div>

    <!-- panel body content here -->
</section>

Stat Cards (Dashboard Metrics)

Use .stat-card for any numeric summary. Rules:

  • When a stat card navigates somewhere, use <a class="stat-card" href="..."> — not a <div>. The CSS a.stat-card handles hover lift and the “View all →” affordance automatically.
  • When a stat card is purely informational (no destination), use <div class="stat-card">.
  • Numbers display at 38px / font-weight 700 in var(--primary).
  • Labels use the .stat-card span style: 11px uppercase, var(--text-secondary).
  • Never put more than one number in a stat card.
  • Place stat cards in a .stats-grid container. For 4 cards use .stats-grid.stats-grid-4. For 3 use .stats-grid alone (default 3-column).
<div class="stats-grid stats-grid-4">
    <a class="stat-card" href="/campaigns">
        <span>Campaigns</span>
        <strong><?= e((string) $model->totalCampaigns) ?></strong>
    </a>
    <!-- ... -->
</div>

Dashboard Pages

When a page is an overview/metrics page (like the home dashboard):

  1. Start with a .stats-grid of stat cards covering the main record counts.
  2. Follow with a .dashboard-panels grid of two side-by-side .section-panel blocks.
  3. Left panel: a recent-records list using .dashboard-table.
  4. Right panel: a breakdown or summary using .type-breakdown.

Dashboard table (.dashboard-table) rules:

  • Columns: ID (.dashboard-table-id), name/type, date (.dashboard-table-date), action link (.dashboard-table-action).
  • No JavaScript — server-rendered, static HTML table.
  • Always include a link column to the resource's edit page.
  • Always include an empty state (.empty-state) with a helpful create link for when there is no data.

Type breakdown (.type-breakdown) rules:

  • Each row is a .type-breakdown-row showing: label, proportional bar, count.
  • When the rows link somewhere use <a class="type-breakdown-row" href="...">. The CSS handles hover and focus-visible.
  • Calculate the bar width as round(count / max * 100)%. Pre-calculate $max before the loop.

Forms

Structure All multi-section forms use .ct-form (a CSS grid with gap: 24px) containing one or more .form-section blocks, ending with .form-actions.

<form method="post" action="..." class="ct-form" novalidate>
    <?= csrf_field() ?>

    <div class="form-section">
        <label class="field field-full">
            <span>Field label <span class="required-mark">*</span></span>
            <input class="input" type="text" name="name" required>
        </label>
    </div>

    <div class="form-actions">
        <button class="button button-primary" type="submit">Save</button>
        <a class="button button-secondary" href="/resource">Cancel</a>
    </div>
</form>

Sticky save bar .ct-form .form-actions is position: sticky; bottom: 0 — the save bar sticks to the viewport bottom when the form is taller than the screen. This is set globally in CSS. Do not override or move form-actions outside .ct-form.

Native validation Use HTML5 required, maxlength, pattern, min, max on inputs. The CSS rule .input:invalid:not(:placeholder-shown) automatically shows an error border without JavaScript. Use novalidate on the form tag to prevent browser-native popups while still benefiting from the CSS pseudo-class.

Required mark Mark required fields with <span class="required-mark">*</span> inside the label. Color is var(--error) via CSS.

Error messages Server-side validation errors go in <small class="field-error"> immediately after the input. Add input-error class to the input when it has a server error.


Attribute Builder (Drag-Reorder Lists)

When a form includes a reorderable list of items (like campaign type attributes):

  • Each row is .attribute-row with draggable="true" and Alpine.js drag event handlers.
  • The first element of every row must be <span class="attr-drag-handle" title="Drag to reorder">&#8597;</span>.
    The CSS hides the unicode character with font-size: 0 and replaces it with a ::before radial-gradient dot grid — this is automatic. Do not change the HTML to use an SVG or other icon.
  • is-dragging and is-drag-over classes are applied by Alpine and styled by CSS — do not add custom drag styles.

Loading States

Never use plain text for loading indicators in content areas. Use .skeleton-rows with .skeleton-row children.

<div class="skeleton-rows" x-cloak x-show="isLoading">
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
</div>

Rules:

  • Use 5 skeleton rows for full-page table loaders.
  • Use 3 skeleton rows for smaller/drilldown table loaders.
  • Always pair with x-cloak and x-show on Alpine-controlled sections so the skeleton only shows while loading.
  • The shimmer animation and fade-out opacity are set automatically by CSS via :nth-child selectors.

Cards, rows, and panels that navigate must be <a> elements, not <div> elements with onclick. This applies to:

  • Stat cards with a destination
  • Type breakdown rows
  • Any list row that is entirely clickable

Use the appropriate CSS modifier class (a.stat-card, a.type-breakdown-row) — the hover and focus styles are already defined.

Inline table action links (Edit, Delete) in Tabulator formatter functions must use these conventions:

  • Edit: class="button button-secondary button-sm"
  • Delete: class="button button-danger button-sm" with a window.confirmXxx() guard function
  • Jobs/drill-down: class="button button-primary button-sm"

Accessibility

Focus rings

  • All content-area links get outline: 2px solid var(--accent); outline-offset: 2px on :focus-visible — set globally in CSS via .page-content a:focus-visible. Do not suppress this.
  • All buttons already have outline: 2px solid var(--accent); outline-offset: 2px via .button:focus-visible.
  • Nav links and logout button have white outline rings — see the Navigation section above.

Contrast minimums

  • Normal body text: minimum 4.5:1 against its background.
  • Nav link text: minimum rgba(255,255,255, 0.85) against #1F4E79.
  • Secondary text (var(--text-secondary): #4B5563) on white: ~7.2:1 — acceptable.
  • Do not use var(--text-muted) (#6B7280) for body text — only for supplementary labels and counts.

ARIA

  • All SVG icons in nav links must have aria-hidden="true".
  • All .nav-sep elements must have aria-hidden="true".
  • Empty states must be descriptive — include both what is missing and a create action link.

Repository Patterns for Metrics

When a page needs aggregate data (counts, recent records, breakdowns), add methods to the relevant repository. Do not inline aggregate SQL in the controller.

Count method pattern

public function count(): int
{
    $row = $this->database->first('SELECT COUNT(*) AS total FROM table_name');
    return (int) ($row['total'] ?? 0);
}

Recent records pattern Inline the TOP limit directly — SQL Server does not accept bound parameters in TOP expressions.

public function recentWithType(int $limit = 5): array
{
    return $this->database->query(
        "SELECT TOP ({$limit}) t.id, t.created_at, ref.name AS type_name
         FROM main_table t
         INNER JOIN ref_table ref ON t.ref_id = ref.id
         ORDER BY t.id DESC"
    );
}

Grouped count pattern Use LEFT JOIN so that reference rows with zero records still appear.

public function countByType(): array
{
    return $this->database->query(
        'SELECT ref.name AS type_name, COUNT(t.id) AS record_count
         FROM ref_table ref
         LEFT JOIN main_table t ON t.ref_id = ref.id
         GROUP BY ref.id, ref.name
         ORDER BY record_count DESC, ref.name ASC'
    );
}

ViewModels for Dashboard Pages

Dashboard pages use a dedicated ViewModel. Metric fields use typed defaults so the view is never working with null.

class MyDashboardViewModel
{
    public int $totalThings    = 0;
    public int $totalTypes     = 0;
    public array $recentItems  = [];
    public array $itemsByType  = [];
}

Populate it in the controller using private repo() methods or inline new Repository(database()) calls — consistent with how other controllers are written in this project.

Powered by TurnKey Linux.