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 (ESLint + Prettier)

ESLint uses flat config in eslint.config.js (there is no .eslintrc.cjs), with typescript-eslint, eslint-plugin-vue, eslint-plugin-import-x and eslint-plugin-jsonc.

  • Indentation: 4 spaces (indent for TS, vue/script-indent + vue/html-indent for SFCs — the two are mutually disabled so they never fight).
  • Semicolons required.
  • Relative imports must include their file extension (import-x/extensions: always, ignorePackages: true) — so @/Plugins/i18n.ts, but plain vue.
  • Component names in templates must use kebab-case (registeredComponentsOnly: false — this applies to every component).
  • import-x/order groups imports with blank lines between groups.
  • Unused variables flagged except those prefixed with _.
  • Max line length not enforced by ESLint (Prettier handles it at 120).
  • JSON & JSONC files linted with 4-space indentation.

Vue-specific rules:

  • HTML indentation: 4 spaces.
  • Max attributes per line: 1 (vue/max-attributes-per-line singleline: 1) — this matches Prettier's singleAttributePerLine.

Prettier

.prettierrc:

  • tabWidth: 4, singleQuote: true, semi: true, printWidth: 120
  • proseWrap: "never"
  • vueIndentScriptAndStyle: true<script> content is indented inside the SFC
  • singleAttributePerLine: true
  • Plugins: organize-imports, tailwindcss (class sorting), classnames (long class-list wrapping)

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: the tenant is a Shop (not "Garage" — that term survives only in prose). 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 → ESLint → 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)
ESLintTS/Vue lintnpm run lint / npm run lint:fix
PrettierFormatting, import & Tailwind class sortnpm run prettier
vue-tscTS/Vue type checkingnpm 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.