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:
- PHP 8.4 + Laravel 12
- PostgreSQL 17+ + Redis
- Filament 5 + Livewire 4
- Docker + docker-compose Links:
- GitHub: https://github.com/Relaticle/relaticle (1.2K stars)
- Free hosted: https://relaticle.com (no credit card)
- License: AGPL-3.0. For internal use — no restrictions.
What PHP 8.4 features have you found most useful in production? Curious what patterns this community is adopting
[link] [comments]








English (US) ·