Skip to content

Code Quality

Accurate reference for Conflux's code quality tooling configuration and enforced architecture rules.

Running Quality Checks

All commands run inside the Docker container.

# Run all checks (Pint + PHPStan + Rector + security audit)
docker compose exec php composer quality

# Fix code style
docker compose exec php vendor/bin/pint --dirty

# Static analysis
docker compose exec php composer quality:phpstan

# Rector dry-run (preview changes without applying)
docker compose exec php composer quality:rector

# Apply Rector changes
docker compose exec php vendor/bin/rector process

# CVE audit
docker compose exec php composer quality:security

Tooling Configuration

Laravel Pint

Style fixer using the Laravel preset with additional rules:

Rule Behaviour
declare_strict_types Required on every PHP file
no_unused_imports Unused use statements are removed
Import ordering Alphabetical
Class element ordering Consistent, enforced

Rector

Automated code upgrades and dead-code removal.

Configuration Value
PHP version target 8.5
Laravel set Enabled
Type coverage level 10
Dead code level 10
Code quality level 10

PHPStan / Larastan

Static analysis at level 10 via Larastan.

Architecture Rules

Enforced by Pest in tests/Unit/ArchTest.php. Violations fail the test suite.

Rule Scope
Every PHP file must declare strict_types=1 App\
No debug helpers (dd, dump, var_dump, ray) App\
All controllers must extend App\Http\Controllers\Controller and be suffixed Controller App\
All models must extend Illuminate\Database\Eloquent\Model App\
Every public method must carry a PHPDoc block with at least a one-line summary App\Services, App\Jobs, App\Contracts, App\Models, App\Enums, App\Http\Controllers, App\Integrations, App\Auth, App\Console\Commands, App\Policies, App\Exceptions, App\Mcp

The public-method-documentation rule excludes App\Filament, App\Data, App\Providers, and App\Logging (declarative or framework-contract methods that don't narrate a design decision), and excludes any constructor that is only promoted properties with an empty body.

The PHPDoc block needs at least a one-line summary of what the method does (or why, if the what is obvious from the signature). No @param/@return needed — type hints already cover that (enforced at PHPStan level 10); add them only for something a type hint can't express (e.g. a Closure signature, an array shape).

/**
 * Resolve the integration registered for the connection's system driver.
 */
public function resolve(SystemDriver $driver): IntegrationContract

Test Structure

Tests are split into three tiers, each in its own directory:

tests/
├── Unit/          # No DB, no HTTP. Instantiate classes directly (new Model()).
│   └── Models/   # Cast shapes, relationship types, trait usage.
├── Integration/   # DB required. RefreshDatabase applied automatically via Pest.php.
│   └── Models/   # Scopes, factory round-trips, helper methods that persist.
└── Feature/       # Full HTTP stack — routes, responses, auth flows.

Rules: - Unit tests must never touch the database. Use new ClassName() directly. - Integration tests get RefreshDatabase automatically — do not declare it per-file. - Feature tests cover HTTP behaviour end-to-end. - Mirror the app/ directory structure inside each tier (e.g. Unit/Models/, Integration/Models/). - Group related tests with describe() blocks (e.g. casts, relationships, scopes, traits). Test names inside a describe() should omit the group prefix — test('status as enum') not test('casts status as enum'). - For API endpoints that wire several layers together, add end-to-end coverage in addition to the mocked Feature tests — see End-to-End Tests below.

End-to-End Tests

Endpoints that span multiple layers (controller → registry/service → integration → outbound HTTP) should have at least one end-to-end Feature test that drives the real chain and fakes only the outermost boundary, rather than mocking internal collaborators:

  • Don't mock internal collaborators (e.g. the IntegrationRegistry). Fake only outbound calls with Http::fake().
  • Register test integrations on the real singleton registry, e.g. app(IntegrationRegistry::class)->register(SystemDriver::WooCommerce, HttpVerifyingTestIntegration::class).
  • Reuse the shared doubles in tests/Support/: FakeIntegration (closure-driven, for unit-level template tests) and HttpVerifyingTestIntegration (makes a real Http call, for e2e wiring).
  • Assert both sides: the persisted state and the outbound request via Http::assertSent(...), so credential/payload plumbing is covered.
  • Keep them alongside the mocked Feature tests — the mocked tests pin controller logic; the e2e test pins the wiring the mocks skip (registry resolution, AbstractIntegration::verify() exception handling, the HTTP boundary).

Reference: tests/Feature/Api/RegisterSystemConnectionE2eTest.php.

PHPStan in Tests

PHPStan level 10 runs over the tests/ directory. Key rules that apply specifically to tests:

  • Null-narrowing after Pest assertions: ->toBeInstanceOf() in a Pest chain does not narrow nullable types for PHPStan. Use assert($var instanceof SomeClass) immediately after the expect call to narrow the type before accessing sub-properties.
  • Test integration doubles: Prefer the shared doubles in tests/Support/ (FakeIntegration, HttpVerifyingTestIntegration) over inline anonymous AbstractIntegration subclasses. Any subclass you do write must implement all abstract hooks (performVerification(), fetchBulkExport()) and carry a /** @return Collection<int, CustomerData|OrderData|ProductData> */ PHPDoc on fetchBulkExport(), otherwise PHPStan infers a narrower generic type and reports a covariance error.
  • Mocking IntegrationContract: Mockery::mock() is typed as MockInterface here (no Mockery PHPStan extension). Annotate the variable /** @var IntegrationContract $integration */ so it satisfies typed parameters, keep the expectation on one line (assign the closure to a variable first), and suppress the resulting analysis noise with // @phpstan-ignore method.notFound, method.nonObject.
  • Avoid redundant assert(): For properties typed via #[DataCollectionOf(Foo::class)], PHPStan already knows the element type. A redundant assert($item instanceof Foo) triggers instanceof.alwaysTrue — skip it and access properties directly.