- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
8.4 KiB
8.4 KiB
PhoneNumber Value Object & Validation
Complete phone number handling system with Value Object pattern, validation rule, and database integration.
✅ Features Implemented
PhoneNumber Value Object
- Type-safe Phone Numbers: Immutable readonly class with comprehensive validation
- Multiple Input Formats: Supports international, national, and formatted inputs
- Smart Normalization: Automatic cleaning and formatting of phone number strings
- Country Code Detection: Automatic extraction of country codes from international numbers
- Mobile Detection: Heuristic detection of mobile vs. landline numbers
- Multiple Output Formats: E.164, International, and National formatting
Phone Validation Rule
- Attribute-based Validation:
#[Phone]attribute for automatic validation - Framework Integration: Works with existing validation system
- Flexible Validation: Allows empty values (use with
#[Required]if needed) - Custom Messages: Customizable error messages
- Type Safety: Validates input types and formats
Database Integration
- TypeCaster Support: Automatic conversion between database strings and PhoneNumber objects
- Null Handling: Proper handling of null and empty values
- Round-trip Conversion: Preserves formatting through database operations
- Type Safety: Ensures only valid PhoneNumber instances are stored
Usage Examples
Basic Value Object Usage
use App\Domain\Common\ValueObject\PhoneNumber;
// Create from various formats
$phone1 = PhoneNumber::from('+49 123 456789');
$phone2 = PhoneNumber::parse('+49-123-456-789'); // Normalizes formatting
$phone3 = PhoneNumber::from('0123 456789'); // National format
// Validation
$isValid = PhoneNumber::isValid('+49 123 456789'); // true
$isValid = PhoneNumber::isValid('abc'); // false
// Information extraction
$countryCode = $phone1->getCountryCode(); // '49'
$national = $phone1->getNationalNumber(); // ' 123 456789'
$isMobile = $phone1->isMobile(); // false (German landline)
// Formatting
$e164 = $phone1->toE164(); // '+49123456789'
$international = $phone1->toInternational(); // '+49 123 456 789'
$national = $phone1->toNational(); // '0123 456 789'
// Comparison
$phone4 = PhoneNumber::from('+49-123-456-789');
$isEqual = $phone1->equals($phone4); // true (same number)
$sameCountry = $phone1->isSameCountry($phone4); // true
Validation Rule Usage
use App\Framework\Validation\Rules\Phone;
use App\Framework\Validation\Rules\Required;
class CustomerRequest
{
#[Required]
#[Phone('Please enter a valid phone number.')]
public ?string $phone = null;
#[Phone] // Optional phone field
public ?string $mobile = null;
}
// Usage in validation
$validator = new Validator();
$request = new CustomerRequest();
$request->phone = '+49 123 456789'; // Valid
$request->mobile = 'invalid'; // Invalid
$errors = $validator->validate($request);
// Returns validation errors for invalid phone numbers
Database Integration
use App\Framework\Database\Attributes\Column;
use App\Domain\Common\ValueObject\PhoneNumber;
class Customer
{
#[Column(type: PhoneNumber::class)]
public ?PhoneNumber $phone = null;
}
// Entity usage
$customer = new Customer();
$customer->phone = PhoneNumber::from('+49 123 456789');
// EntityManager automatically handles conversion
$entityManager->save($customer); // Stores as string: "+49 123 456789"
$loaded = $entityManager->find(Customer::class, $id);
// $loaded->phone is automatically converted back to PhoneNumber object
Supported Phone Number Formats
International Formats
PhoneNumber::from('+49 123 456789'); // German
PhoneNumber::from('+1 (555) 123-4567'); // US with formatting
PhoneNumber::from('+33 1 23 45 67 89'); // French
PhoneNumber::from('+44 20 7946 0958'); // UK London
National Formats
PhoneNumber::from('0123 456789'); // German national
PhoneNumber::from('(555) 123-4567'); // US national
PhoneNumber::from('030 12345678'); // German Berlin
Flexible Input Processing
// All of these create equivalent PhoneNumber objects:
$formats = [
'+49 123 456789',
'+49-123-456-789',
'+49.123.456.789',
'+49 (123) 456-789',
'+49 123 456 789', // Multiple spaces
];
foreach ($formats as $format) {
$phone = PhoneNumber::parse($format);
echo $phone->toE164(); // Always: '+49123456789'
}
Country-Specific Features
German Phone Numbers
$landline = PhoneNumber::from('+49 30 12345678'); // Berlin landline
$mobile = PhoneNumber::from('+49 151 12345678'); // Mobile (15x prefix)
echo $landline->isMobile(); // false
echo $mobile->isMobile(); // true
echo $landline->toNational(); // '030 123 456 78'
echo $mobile->toNational(); // '0151 123 456 78'
US Phone Numbers
$usPhone = PhoneNumber::from('+1 555 123 4567');
echo $usPhone->getCountryCode(); // '1'
echo $usPhone->isMobile(); // true (heuristic: 10 digits)
echo $usPhone->toInternational(); // '+1 (555) 123-4567'
echo $usPhone->toNational(); // '(555) 123-4567'
Error Handling
Validation Errors
try {
$phone = PhoneNumber::from('');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // "Phone number cannot be empty."
}
try {
$phone = PhoneNumber::from('123');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // "Phone number too short: 123"
}
try {
$phone = PhoneNumber::from('not-a-phone');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // "Phone number must contain digits: not-a-phone"
}
Safe Validation
// Non-throwing validation
$isValid = PhoneNumber::isValid('+49 123 456789'); // true
$isValid = PhoneNumber::isValid('invalid'); // false
// Use in validation logic
if (PhoneNumber::isValid($userInput)) {
$phone = PhoneNumber::from($userInput);
// Process valid phone number
} else {
// Handle invalid input
}
Testing Coverage
Comprehensive Test Suite
- 19 test cases covering all PhoneNumber functionality
- 10 test cases for TypeCaster database integration
- 8 test cases for Phone validation rule
- Edge cases: Empty values, format variations, error conditions
- Real-world scenarios: International numbers, mobile detection, formatting
Test Examples
// All tests pass with comprehensive assertions
describe('PhoneNumber Value Object', function () {
it('handles various input formats', function () {
$formats = [
'+49 123 456789',
'+49-123-456-789',
'+49.123.456.789',
'0123 456789',
];
foreach ($formats as $format) {
$phone = PhoneNumber::parse($format);
expect($phone)->toBeInstanceOf(PhoneNumber::class);
}
});
});
Integration with Existing Systems
Migration from String Phone Fields
// Before: Simple string validation
class CustomerRequest {
public ?string $phone = null;
}
// After: Type-safe PhoneNumber with comprehensive validation
class CustomerRequest {
#[Phone]
public ?string $phone = null; // Still string for form input
}
// Entity level: Convert to Value Object
class Customer {
#[Column(type: PhoneNumber::class)]
public ?PhoneNumber $phone = null; // Type-safe storage
}
Backward Compatibility
// Existing code continues to work
$phoneString = '+49 123 456789';
// New type-safe approach
$phoneObject = PhoneNumber::from($phoneString);
$backToString = (string) $phoneObject; // '+49 123 456789'
Performance & Best Practices
Efficient Usage
- Normalization: Parse method handles formatting variations efficiently
- Validation: Use
isValid()for non-throwing validation in user input - Comparison: Use
equals()method for accurate phone number comparison - Formatting: Choose appropriate format method for display context
Framework Integration
- Validation: Combine with
#[Required]for mandatory fields - Database: TypeCaster handles conversion automatically
- Forms: Accept string input, validate with
#[Phone], convert to object in entity - APIs: Format output with appropriate method (
toE164()for APIs,toInternational()for display)
The PhoneNumber implementation provides a complete solution for phone number handling in the framework, ensuring type safety, validation, and proper formatting throughout the application.