Type Safety - Grazulex/laravel-arc GitHub Wiki
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.
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;
}
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;
}
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
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']
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
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
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
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;
}
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'
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;
}
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
}
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;
}
/**
* @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-property string $name
* @psalm-property Email $email
* @psalm-property Collection<array-key, string> $tags
*/
class User extends Model
{
// Property definitions...
}
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;
}
// Enable type information caching
class CachedTypes extends Model
{
use HasArcProperties;
protected array $arcConfig = [
'cache_type_info' => true,
'cache_duration' => 3600,
];
}
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
}
}
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);
}
}
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);
});
}
}
// Good: Explicit type declarations
#[Property(type: 'string', required: true)]
public string $name;
#[Property(type: 'integer', min: 0)]
public int $age;
// Good: Type-safe value objects
#[Property(type: 'object', cast: Email::class)]
public Email $email;
#[Property(type: 'object', cast: Money::class)]
public Money $price;
// 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'
// Good: Validation for complex types
#[Property(
type: 'array',
validation: [
'array',
'contacts.*.email' => 'email',
'contacts.*.phone' => 'regex:/^\+?[1-9]\d{1,14}$/'
]
)]
public array $contacts;
- 🔧 Explore Property Attributes for more type options
- ✅ Learn about Validation integration
- 🎨 Discover Advanced Features
- 📚 Check Examples for real-world type usage
- 🔍 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.