php
62 lines · 9 steps
Building a reusable address form validator in PHP
A validator class accumulates field errors into a keyed map, layering required checks, format rules, and country-specific logic.
Explained by
highlit
1<?php
2
3namespace App\Validation;
4
5class AddressFormValidator
6{
7 private array $errors = [];
8
9 public function validate(array $data): array
10 {
11 $this->errors = [];
12
13 $this->required($data, 'recipient', 'Recipient name is required.');
14 $this->required($data, 'phone', 'A contact phone number is required.');
15
16 if (!empty($data['phone']) && !preg_match('/^\+?[0-9 ()-]{7,20}$/', $data['phone'])) {
17 $this->add('phone', 'Phone number format is invalid.');
18 }
19
20 $address = $data['address'] ?? [];
21
22 $this->requiredNested($address, 'address.line1', 'Street address is required.');
23 $this->requiredNested($address, 'address.city', 'City is required.');
24 $this->requiredNested($address, 'address.country', 'Country is required.');
25
26 $country = strtoupper(trim($address['country'] ?? ''));
27
28 if ($country === 'US') {
29 $zip = $address['postal_code'] ?? '';
30 if (!preg_match('/^\d{5}(-\d{4})?$/', $zip)) {
31 $this->add('address.postal_code', 'A valid US ZIP code is required.');
32 }
33 if (empty($address['state'])) {
34 $this->add('address.state', 'State is required for US addresses.');
35 }
36 } elseif ($country !== '' && empty($address['postal_code'])) {
37 $this->add('address.postal_code', 'Postal code is required.');
38 }
39
40 return $this->errors;
41 }
42
43 private function required(array $data, string $key, string $message): void
44 {
45 if (trim((string) ($data[$key] ?? '')) === '') {
46 $this->add($key, $message);
47 }
48 }
49
50 private function requiredNested(array $address, string $fullKey, string $message): void
51 {
52 $field = substr($fullKey, strrpos($fullKey, '.') + 1);
53 if (trim((string) ($address[$field] ?? '')) === '') {
54 $this->add($fullKey, $message);
55 }
56 }
57
58 private function add(string $key, string $message): void
59 {
60 $this->errors[$key][] = $message;
61 }
62}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Collecting errors into a keyed map lets one pass report every problem at once instead of failing on the first.
- 2Small private helpers like required and add keep validation rules readable and consistent.
- 3Conditional branches let you apply locale-specific rules without cluttering the common path.
Related explainers
java
public record AppConfig( String host, int port, String databaseUrl,
Loading typed config from env vars in Java
records
configuration
environment-variables
Intermediate
6 steps
rust
use rand::distributions::{Alphanumeric, DistString}; use rand::rngs::OsRng; #[derive(Debug, Clone, PartialEq, Eq)]
A newtype wrapper for API tokens in Rust
newtype-pattern
randomness
encapsulation
Intermediate
6 steps
typescript
export function isValidCardNumber(input: string): boolean { const digits = input.replace(/[\s-]/g, ""); if (!/^\d{12,19}$/.test(digits)) {
Validating card numbers with the Luhn check
luhn-algorithm
checksum
input-validation
Intermediate
7 steps
ruby
require 'date' require 'set' class BusinessDayCalculator
Counting business days in Ruby
dates
sets
ranges
Intermediate
8 steps
php
<?php namespace App\Http\Controllers;
A cached autocomplete endpoint in Laravel
caching
validation
query-ranking
Intermediate
8 steps
typescript
interface UserAgentInfo { browser: { name: string; version: string }; os: { name: string; version: string }; device: 'mobile' | 'tablet' | 'desktop';
Parsing a user-agent string with ordered rules
regex
parsing
pattern-matching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-reusable-address-form-validator-in-php-explained-php-771e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.