Type Safety - Grazulex/laravel-arc GitHub Wiki

🎯 Type Safety

Overview

Laravel Arc leverages PHP 8's type system to provide robust type safety for your model properties. This ensures data integrity, prevents runtime errors, and improves IDE support with autocompletion and static analysis.

PHP 8 Type Declarations

Typed Properties

Laravel Arc works seamlessly with PHP 8 typed properties:

class User extends Model
{
    use HasArcProperties;

    // Strict string type
    #[Property(type: 'string', required: true)]
    public string $name;

    // Nullable string type
    #[Property(type: 'string', required: false)]
    public ?string $nickname = null;

    // Integer with constraints
    #[Property(type: 'integer', min: 0, max: 120)]
    public int $age;

    // Float with precision
    #[Property(type: 'float', min: 0.0)]
    public float $salary;

    // Boolean type
    #[Property(type: 'boolean')]
    public bool $is_active;

    // Array type for JSON data
    #[Property(type: 'json')]
    public array $metadata;

    // Carbon for dates
    #[Property(type: 'datetime')]
    public Carbon $created_at;
}

Union Types

Support for PHP 8 union types:

class Product extends Model
{
    use HasArcProperties;

    // String or null
    #[Property(type: 'string', required: false)]
    public string|null $description = null;

    // Multiple numeric types
    #[Property(type: 'numeric')]
    public int|float $price;

    // Mixed content
    #[Property(type: 'mixed')]
    public mixed $flexible_data;
}

Type Casting and Conversion

Automatic Type Casting

Laravel Arc automatically converts input data to the correct types:

class Order extends Model
{
    use HasArcProperties;

    #[Property(type: 'integer')]
    public int $quantity;

    #[Property(type: 'float')]
    public float $total;

    #[Property(type: 'boolean')]
    public bool $is_paid;

    #[Property(type: 'datetime')]
    public Carbon $order_date;
}

// Input data with various types
$order = new Order();
$order->quantity = '5';           // String '5' -> int 5
$order->total = '99.99';          // String '99.99' -> float 99.99
$order->is_paid = 'true';         // String 'true' -> bool true
$order->order_date = '2023-12-01'; // String date -> Carbon instance

Smart Type Conversion

class SmartCasting extends Model
{
    use HasArcProperties;

    // Intelligent string conversion
    #[Property(type: 'string', transform: 'trim|lowercase')]
    public string $code;

    // Number formatting
    #[Property(type: 'float', transform: 'round:2')]
    public float $price;

    // Date parsing with multiple formats
    #[Property(
        type: 'date',
        dateFormat: ['Y-m-d', 'd/m/Y', 'm-d-Y']
    )]
    public Carbon $birth_date;

    // JSON encoding/decoding
    #[Property(type: 'json')]
    public array $settings;
}

// Automatic conversions
$model = new SmartCasting();
$model->code = '  ABC123  ';       // Becomes: 'abc123'
$model->price = 19.999;            // Becomes: 19.99
$model->birth_date = '01/15/1990'; // Parsed correctly
$model->settings = '{"key":"value"}'; // Becomes: ['key' => 'value']

Enum Support

PHP 8.1 Enums

Full support for PHP 8.1 enums with type safety:

enum UserStatus: string
{
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case SUSPENDED = 'suspended';
    case PENDING = 'pending';
}

enum Priority: int
{
    case LOW = 1;
    case MEDIUM = 2;
    case HIGH = 3;
    case URGENT = 4;
}

class User extends Model
{
    use HasArcProperties;

    // String-backed enum
    #[Property(type: 'enum', enum: UserStatus::class)]
    public UserStatus $status;

    // Integer-backed enum
    #[Property(type: 'enum', enum: Priority::class)]
    public Priority $priority;
}

// Type-safe enum usage
$user = new User();
$user->status = UserStatus::ACTIVE;    // Type-safe assignment
$user->status = 'active';             // Auto-converted to enum
$user->priority = Priority::HIGH;      // Type-safe assignment
$user->priority = 3;                  // Auto-converted to enum

Unit Enums

enum Color
{
    case RED;
    case GREEN;
    case BLUE;
}

class Product extends Model
{
    use HasArcProperties;

    #[Property(type: 'enum', enum: Color::class)]
    public Color $color;
}

// Usage
$product = new Product();
$product->color = Color::RED; // Type-safe

Collection Types

Typed Collections

Type-safe collections with Laravel Collections:

class Blog extends Model
{
    use HasArcProperties;

    // Collection of strings
    #[Property(
        type: 'collection',
        itemType: 'string',
        validation: 'each:string|min:1'
    )]
    public Collection $tags;

    // Collection of models
    #[Property(
        type: 'collection',
        itemType: Comment::class
    )]
    public Collection $comments;

    // Typed array
    #[Property(
        type: 'array',
        itemType: 'integer'
    )]
    public array $view_counts;
}

// Type-safe collection operations
$blog = new Blog();
$blog->tags = collect(['php', 'laravel', 'arc']); // Collection of strings
$blog->view_counts = [100, 250, 500];             // Array of integers

Generic Collections

class TypedCollections extends Model
{
    use HasArcProperties;

    // Generic collection with type constraints
    #[Property(
        type: 'collection',
        itemType: 'array',
        validation: [
            'each:array',
            '*.name' => 'required|string',
            '*.value' => 'required|numeric'
        ]
    )]
    public Collection $metrics;

    // Collection with custom objects
    #[Property(
        type: 'collection',
        itemType: 'object',
        cast: MetricObject::class
    )]
    public Collection $structured_metrics;
}

Custom Type Casting

Value Objects

Create type-safe value objects:

class Email
{
    public function __construct(
        public readonly string $value
    ) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email format');
        }
    }

    public function __toString(): string
    {
        return $this->value;
    }

    public function domain(): string
    {
        return substr($this->value, strpos($this->value, '@') + 1);
    }
}

class Money
{
    public function __construct(
        public readonly float $amount,
        public readonly string $currency = 'USD'
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
    }

    public function format(): string
    {
        return number_format($this->amount, 2) . ' ' . $this->currency;
    }
}

class User extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'object',
        cast: Email::class
    )]
    public Email $email;

    #[Property(
        type: 'object',
        cast: Money::class
    )]
    public Money $salary;
}

// Type-safe usage
$user = new User();
$user->email = new Email('[email protected]');
$user->salary = new Money(50000.00, 'USD');

// Auto-casting from primitives
$user->email = '[email protected]';  // Auto-cast to Email object
$user->salary = 60000;              // Auto-cast to Money object

// Type-safe methods
echo $user->email->domain();        // 'example.com'
echo $user->salary->format();       // '60,000.00 USD'

Custom Casters

class PhoneNumberCaster
{
    public function cast($value): PhoneNumber
    {
        if ($value instanceof PhoneNumber) {
            return $value;
        }

        return new PhoneNumber($value);
    }

    public function serialize(PhoneNumber $value): string
    {
        return $value->international();
    }
}

class Contact extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'object',
        cast: PhoneNumberCaster::class
    )]
    public PhoneNumber $phone;
}

Type Validation

Runtime Type Checking

class TypeSafeModel extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'string',
        strictTypes: true  // Enable strict type checking
    )]
    public string $name;

    #[Property(
        type: 'integer',
        strictTypes: true,
        typeErrorMessage: 'Age must be a valid integer'
    )]
    public int $age;
}

// Strict type validation
$model = new TypeSafeModel();
try {
    $model->name = 123; // Will throw TypeError
} catch (TypeError $e) {
    // Handle type error
}

Type Guards

class TypeGuards extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'mixed',
        typeGuard: function ($value) {
            return is_string($value) || is_numeric($value);
        },
        typeErrorMessage: 'Value must be string or numeric'
    )]
    public mixed $flexible_value;

    #[Property(
        type: 'array',
        typeGuard: function ($value) {
            return is_array($value) && 
                   array_is_list($value) && 
                   count($value) <= 10;
        }
    )]
    public array $limited_list;
}

IDE Support and Static Analysis

PhpStan Integration

/**
 * @phpstan-property string $name
 * @phpstan-property Email $email
 * @phpstan-property Collection<string> $tags
 */
class User extends Model
{
    use HasArcProperties;

    #[Property(type: 'string')]
    public string $name;

    #[Property(type: 'object', cast: Email::class)]
    public Email $email;

    #[Property(type: 'collection', itemType: 'string')]
    public Collection $tags;
}

Psalm Support

/**
 * @psalm-property string $name
 * @psalm-property Email $email
 * @psalm-property Collection<array-key, string> $tags
 */
class User extends Model
{
    // Property definitions...
}

Performance Optimizations

Lazy Type Casting

class OptimizedModel extends Model
{
    use HasArcProperties;

    // Lazy casting - only cast when accessed
    #[Property(
        type: 'object',
        cast: ExpensiveObject::class,
        lazy: true
    )]
    public ExpensiveObject $expensive_data;

    // Eager casting - cast immediately on assignment
    #[Property(
        type: 'string',
        transform: 'trim|lowercase',
        lazy: false
    )]
    public string $code;
}

Type Caching

// Enable type information caching
class CachedTypes extends Model
{
    use HasArcProperties;

    protected array $arcConfig = [
        'cache_type_info' => true,
        'cache_duration' => 3600,
    ];
}

Error Handling

Type Errors

class ErrorHandling extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'integer',
        onTypeError: 'log'  // Options: 'throw', 'log', 'ignore'
    )]
    public int $number;

    #[Property(
        type: 'string',
        fallbackValue: 'default',  // Fallback on type error
        onTypeError: 'fallback'
    )]
    public string $text;
}

// Custom error handler
class CustomErrorHandling extends Model
{
    use HasArcProperties;

    #[Property(
        type: 'date',
        onTypeError: [self::class, 'handleDateError']
    )]
    public Carbon $date;

    public static function handleDateError($value, $property)
    {
        Log::warning("Invalid date value for {$property}: {$value}");
        return now(); // Return current date as fallback
    }
}

Testing Type Safety

Unit Tests

class TypeSafetyTest extends TestCase
{
    public function test_string_property_type_enforcement()
    {
        $user = new User();
        
        // Valid assignment
        $user->name = 'John Doe';
        $this->assertIsString($user->name);
        
        // Type conversion
        $user->name = 123;
        $this->assertIsString($user->name);
        $this->assertEquals('123', $user->name);
    }

    public function test_enum_type_safety()
    {
        $user = new User();
        
        // Enum assignment
        $user->status = UserStatus::ACTIVE;
        $this->assertInstanceOf(UserStatus::class, $user->status);
        
        // String to enum conversion
        $user->status = 'inactive';
        $this->assertEquals(UserStatus::INACTIVE, $user->status);
    }

    public function test_invalid_enum_value_throws_error()
    {
        $this->expectException(ValueError::class);
        
        $user = new User();
        $user->status = 'invalid_status';
    }

    public function test_collection_type_safety()
    {
        $blog = new Blog();
        
        // Valid collection
        $blog->tags = collect(['php', 'laravel']);
        $this->assertInstanceOf(Collection::class, $blog->tags);
        
        // Array to collection conversion
        $blog->tags = ['vue', 'javascript'];
        $this->assertInstanceOf(Collection::class, $blog->tags);
    }
}

Property-Based Testing

class PropertyBasedTypeTest extends TestCase
{
    public function test_numeric_properties_with_random_values()
    {
        $this->forAll(
            Generator\choose(0, 1000)
        )->then(function (int $value) {
            $model = new Product();
            $model->price = $value;
            
            $this->assertIsFloat($model->price);
            $this->assertGreaterThanOrEqual(0, $model->price);
        });
    }

    public function test_string_properties_with_random_inputs()
    {
        $this->forAll(
            Generator\string()
        )->then(function (string $value) {
            $model = new User();
            $model->name = $value;
            
            $this->assertIsString($model->name);
        });
    }
}

Best Practices

1. Use Strict Typing

// Good: Explicit type declarations
#[Property(type: 'string', required: true)]
public string $name;

#[Property(type: 'integer', min: 0)]
public int $age;

2. Leverage Value Objects

// Good: Type-safe value objects
#[Property(type: 'object', cast: Email::class)]
public Email $email;

#[Property(type: 'object', cast: Money::class)]
public Money $price;

3. Use Enums for Constants

// Good: Type-safe enums instead of constants
#[Property(type: 'enum', enum: UserRole::class)]
public UserRole $role;

// Avoid: String constants
// public string $role; // 'admin', 'user', 'guest'

4. Validate Complex Types

// Good: Validation for complex types
#[Property(
    type: 'array',
    validation: [
        'array',
        'contacts.*.email' => 'email',
        'contacts.*.phone' => 'regex:/^\+?[1-9]\d{1,14}$/'
    ]
)]
public array $contacts;

Next Steps

  1. 🔧 Explore Property Attributes for more type options
  2. ✅ Learn about Validation integration
  3. 🎨 Discover Advanced Features
  4. 📚 Check Examples for real-world type usage
  5. 🔍 Review API Reference for complete type system

Type safety redefined! 🎯 Laravel Arc brings modern PHP type safety to your Laravel models, ensuring robust and maintainable code.

⚠️ **GitHub.com Fallback** ⚠️