99.9% type coverage, PHPStan strict, zero N+1 — building a production CRM in PHP 8.4

1 week ago 7

Hey r/php, I just shipped v3.0 of an open-source CRM I've been building (Relaticle). Wanted to share some PHP-specific engineering decisions, since this community appreciates that kind of thing.

PHP 8.4 strict mode in production: Every class is final. Every file uses strict_types. Typed properties and return types everywhere:

declare(strict_types=1); final class People extends Model implements HasCustomFields { /** @use HasFactory<PeopleFactory> */ use HasFactory; use HasUlids; use SoftDeletes; use UsesCustomFields; /** @var list<string> */ protected $fillable = ['name', 'creation_source']; /** @return BelongsTo<Company, $this> */ public function company(): BelongsTo { return $this->belongsTo(Company::class); } }

Spatie's laravel-data for typed DTOs:

final class SubscriberData extends Data { public function __construct( public string $email, public ?string $first_name = '', public ?string $last_name = '',

PHP 8.4 with strict_types everywhere is genuinely a joy to write. The language has come so far.

99.9% type coverage: I run PHPStan at level 7 (via Larastan). Every method signature is typed. Every return type is explicit. CI fails on any violation — no exceptions, no baselines.

/** @param Collection<int, Contact> $contacts */ public function processImport(Collection $contacts): ImportResult { }

Is it overkill? Maybe. But in a CRM where data integrity matters (contacts, deals, money), catching type mismatches at static analysis time is cheaper than catching them in production.

N+1 query prevention: One line in AppServiceProvider:

Model::preventLazyLoading(!app()->isProduction());

Strict lazy loading enabled globally. Forget an eager load? Exception in development. This alone caught 10-20 performance issues before they shipped.

PostgreSQL over MySQL: Migrated from MySQL to PostgreSQL 17+ in v3.0. Key reason: JSONB. I built no-code custom fields — users create fields without touching code. All stored as JSONB with GIN indexes:

-- PostgreSQL JSONB with proper indexing CREATE INDEX idx_custom_fields ON contacts USING GIN (custom_fields); -- Partial path queries that MySQL JSON can't do efficiently SELECT * FROM contacts WHERE custom_fields->>'industry' = 'SaaS';

MySQL's JSON type can't do proper indexing or partial path queries at this level. For a CRM with dynamic schemas, PostgreSQL is the better fit.

Testing with Pest: Comprehensive test suite — unit, feature, and browser tests. Pest's syntax makes test writing feel less like a chore:

arch('strict types') ->expect('App') ->toUseStrictTypes(); arch('avoid open for extension') ->expect('App') ->classes() ->toBeFinal(); });

Architecture tests prevent structural issues at CI time. If someone accidentally breaks a convention, CI catches it.

Import wizard (the hardest problem): Real-world CSVs are chaos:

  • Automatic date format detection (uses Laravel's date validator under the hood)
  • Fuzzy + exact column matching
  • Relationship mapping (person → company linkage)
  • Chunked processing for large files
  • Granular error reporting (which rows failed, why) If anyone's solving CSV import in PHP, happy to discuss approaches.

Stack:

What PHP 8.4 features have you found most useful in production? Curious what patterns this community is adopting

submitted by /u/Local-Comparison-One to r/PHP
[link] [comments]
Read Entire Article