Skip to content

Coding Standards & Conventions ​

This document codifies style requirements, formatting rules, and architectural conventions enforced across the Vroum Solutions codebase.

PHP (Pint Configuration) ​

Formatter: Laravel Pint (pint.json). Preset: laravel with custom rules.

Key enforced rules:

  • declare_strict_types: All PHP files MUST start with declare(strict_types=1);.
  • fully_qualified_strict_types: Enforces fully-qualified references for built-in types where needed.
  • Class element ordering (ordered_class_elements):
    1. Traits (use statements)
    2. Enum cases
    3. Constants (public → protected → private)
    4. Properties (public → protected → private)
    5. Constructor / Destructor
    6. Magic methods (__*)
    7. PHPUnit methods
    8. Other methods by visibility (public static → public → protected static → protected → private static → private)
  • Visibility required: Every property & method must declare visibility.
  • Convert protected to private (protected_to_private) when feasible.
  • Enforce strict_comparison (===, !==).
  • date_time_immutable: date/time type hints and returns use DateTimeImmutable.
  • No superfluous elseif / useless else blocks.
  • Disallow multiple statements per line.
  • Global namespace imports for classes, constants, and functions.
  • Use modern type casting (modernize_types_casting) and array_push over $a[] = chains.
  • concat_space: one space around ..
  • Traits and interfaces sorted (ordered_traits, ordered_interfaces); self_accessor / self_static_accessor enforced.
  • Class attribute separation: one blank line between methods; properties, constants and trait imports kept ungapped.
  • new_with_parentheses: false — write new Foo without parentheses when there are no arguments.
  • MB string functions enforced (mb_str_functions).
  • tests/TestCase.php is excluded from Pint (notPath).

Example template:

php
declare(strict_types=1);

namespace App\Services;

use DateTimeImmutable;

use function array_filter;

final class InvoiceReportGenerator
{
    public function __construct(
        private readonly PdfClient $pdf,
    ) {}

    public function generate(WorkOrder $workOrder, DateTimeImmutable $asOf): Report
    {
        // ...
    }
}

TypeScript / Vue (Oxlint + Oxfmt via vp check) ​

Lint and format are configured in the lint and fmt blocks of vite.config.ts; there is no ESLint or Prettier config. vp check runs both plus type-aware lint (typeAware / typeCheck on). eslint-plugin-jsonc is loaded as an Oxlint JS plugin for the JSON rules.

  • Relative imports must include their file extension (import/extensions: always, ignorePackages: true) — so @/Plugins/i18n.ts, but plain vue.
  • Unused variables flagged except those prefixed with _.
  • Type-aware rules (typescript/await-thenable, no-misused-promises, no-floating-promises, return-await in try/catch) apply to .ts and .vue.
  • JSON & JSONC files linted with 4-space indentation.
  • Component names in templates use kebab-case; one attribute per line is enforced by the formatter.

Oxfmt ​

fmt block in vite.config.ts. The IDE's Oxfmt plugin reads the same block in automatic mode, so vp fmt and format-on-save agree.

  • tabWidth: 4, singleQuote: true, semi: true, printWidth: 120
  • proseWrap: "never"
  • vueIndentScriptAndStyle: true — <script> content is indented inside the SFC
  • singleAttributePerLine: true
  • sortTailwindcss sorts class lists; long class strings stay on one line (there is no equivalent of prettier-plugin-classnames)
  • htmlWhitespaceSensitivity: "ignore" — a template interpolation too long for the line breaks the surrounding tag, instead of splitting the itself

File & Naming Conventions ​

CategoryConvention
ActionsVerbNoun (e.g., CreateUser, UpdateShop) grouped by domain subfolder
ModelsSingular noun (e.g., Car, WorkOrder)
Data DTOs<Context><Entity>Data or <Verb><Entity>Data (e.g., UserWorkspaceData)
EnumsNoun or qualified variant (e.g., TirePosition, WorkOrderStatus) — placed in app/Entities
ServicesNoun + Service or a clear domain concept
Form Requests<Action><Entity>Request
Policies<Entity>Policy
JobsImperative (SyncExternalPrices)
EventsPast tense or domain change (CarSaved, VehicleCreated)
ListenersHandleCarSaved, SendInvoiceEmail
Vue ComponentsPascalCase filenames, kebab-case usage in templates
Stores<Entity>Store.ts

Domain vocabulary: Shop is the model and the tenant; Garage is that same shop seen from the client portal, and it is code vocabulary there — GarageData, Client\GarageController, Pages/Client/Garage/, the client.garages.* routes. Shop-side code says Shop; portal-side code says Garage. Car is a customer vehicle tied to a client and work orders; Vehicle is the catalog/spec record behind makes, models and images. They are separate models — check which one you need.

Architectural Conventions ​

  • Controllers remain thin: delegate to Actions or Services immediately.
  • Favor immutable Value Objects (final + private constructor static constructors) for domain invariants.
  • Avoid primitive obsession: wrap complex structured arrays in DTOs or Value Objects.
  • Limit static access; prefer dependency injection.
  • Use queued jobs for I/O heavy or long-running operations.

Testing Standards ​

  • Prefer Feature tests (simulate realistic HTTP flows) in tests/Feature.
  • Use factories for model setup; avoid manual attribute arrays.
  • Keep one logical expectation group per test (readability).
  • Extend Tests\DatabaseTestCase (RefreshDatabase + WithFaker) for anything touching the DB, and Tests\Unit\Actions\ActionTestCase for tenant-aware Action tests.
  • Use the #[Test] attribute with snake_case method names describing behavior — store_validates_notes_max_length(). Never the test_ prefix.
  • Every bug fix ships with a new or updated test covering it.

Git & Branching ​

  • main holds the stable release.
  • Feature branches: feature/<short-description>.
  • Run the full validation pipeline before opening a PR: Pint → PHPUnit → vp check → type-check → Vitest → Build.

Commit Messages ​

Structure: <type>(scope): subject Types: feat, fix, docs, style, refactor, test, chore, perf.

  • Subject: imperative mood, max 50 characters, no trailing period.
  • Simple changes: one line. Complex changes: add a body (wrapped at 72 chars) explaining what and why.
  • Keep commits atomic; split unrelated concerns.

Example: feat(tire): add storage assignment action.

Documentation Standards ​

  • Place developer docs in docs/ (this folder).
  • Include code fences with explicit language tags.
  • Provide copyable shell commands (one per line).

Enforcement Summary ​

ToolScopeCommand
PintPHP formatting./vendor/bin/sail pint (--dirty for uncommitted files only)
Oxlint (vp lint)TS/Vue lint, type-awarevp lint resources/js / vp lint resources/js --fix
Oxfmt (vp fmt)Formatting & Tailwind class sortvp fmt (--check to verify)
vp checkFormat + lint + type-aware lint in one passvp check / vp check --fix
vue-tscTS/Vue type checking (CI)vp run type-check
RectorOptional refactoring./vendor/bin/sail php vendor/bin/rector process
Type GenerationDTOs & Enums → TScomposer run transform-types

See development.md for workflow details.