Files
michaelschiemer/backups/docs-backup-20250731125004/validation/phone-number.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

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.