Transformation Pipeline - Grazulex/laravel-arc GitHub Wiki

๐Ÿ”„ Transformation Pipeline

The Transformation Pipeline is one of Laravel Arc's most powerful features, allowing you to pre-process data before casting. This enables clean data transformations while maintaining type safety and performance.

๐ŸŽฏ Overview

Transformations are applied before the casting process, ensuring that data is clean and properly formatted before being assigned to DTO properties.

Key Benefits

  • Data Cleaning: Automatically trim whitespace, normalize case, etc.
  • Security: Hash sensitive data before storage
  • Consistency: Ensure uniform data format across your application
  • Chainable: Apply multiple transformations in sequence
  • Type-Safe: Maintain data integrity throughout the process

๐Ÿ”ง Built-in Transformers

TrimTransformer

Removes leading and trailing whitespace.

use Grazulex\Arc\Transformers\TrimTransformer;

class UserDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        transform: [TrimTransformer::class]
    )]
    public string $name;
}

// Input: '  John Doe  '
// Output: 'John Doe'

LowercaseTransformer

Converts string to lowercase.

use Grazulex\Arc\Transformers\LowercaseTransformer;

class UserDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        transform: [LowercaseTransformer::class]
    )]
    public string $email;
}

// Input: '[email protected]'
// Output: '[email protected]'

UppercaseTransformer

Converts string to uppercase.

use Grazulex\Arc\Transformers\UppercaseTransformer;

class AddressDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        validation: 'size:2',
        transform: [UppercaseTransformer::class]
    )]
    public string $country_code;
}

// Input: 'us'
// Output: 'US'

HashTransformer

Hashes the input value (configurable algorithm).

use Grazulex\Arc\Transformers\HashTransformer;

class UserDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        transform: [HashTransformer::class]
    )]
    public string $password_hash;
}

// Input: 'mypassword123'
// Output: 'ef92b778....' (SHA256 hash)

๐Ÿ†• SlugTransformer

Generates URL-friendly slugs with cross-field transformation support.

use Grazulex\Arc\Transformers\SlugTransformer;

// Basic slug generation
class ArticleDTO extends LaravelArcDTO
{
    #[Property(type: 'string', required: true)]
    public string $title;
    
    // Custom transformer that reads from 'title' field
    #[Property(
        type: 'string',
        required: false,
        transform: [TitleToSlugTransformer::class]  // See custom transformer below
    )]
    public ?string $slug;
}

// Custom transformer for cross-field transformation
class TitleToSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(
            sourceField: 'title',     // Read from this field
            separator: '-',
            maxLength: 50
        );
    }
}

// Usage
$article = new ArticleDTO([
    'title' => 'Best PHP Practices 2025'
]);
echo $article->slug; // 'best-php-practices-2025'

๐Ÿงช Working Test Examples (v2.2.5+)

With the latest fixes in v2.2.5, SlugTransformer now works seamlessly with anonymous classes in tests:

use Tests\TestCase; // โœ… Important: Use proper TestCase
use Grazulex\Arc\LaravelArcDTO;
use Grazulex\Arc\Attributes\Property;
use Grazulex\Arc\Transformers\SlugTransformer;

class SlugTransformerIntegrationTest extends TestCase
{
    public function test_slug_generated_from_title_field(): void
    {
        // โœ… Anonymous classes now work perfectly
        $dto = new class extends LaravelArcDTO {
            #[Property(type: 'string', required: true)]
            public string $title;
            
            #[Property(
                type: 'string',
                required: false,
                transform: [TitleToSlugTransformer::class]
            )]
            public ?string $slug;
        };
        
        $instance = new $dto([
            'title' => 'Hello World Article',
            'slug' => '' // This gets overwritten by transformer
        ]);
        
        $this->assertEquals('Hello World Article', $instance->title);
        $this->assertEquals('hello-world-article', $instance->slug);
    }
    
    public function test_slug_with_trim_and_length_limit(): void
    {
        $dto = new class extends LaravelArcDTO {
            #[Property(type: 'string', required: false, transform: [TrimTransformer::class])]
            public ?string $name;
            
            #[Property(
                type: 'string',
                required: false,
                transform: [NameToSlugTransformer::class]  // maxLength: 20
            )]
            public ?string $slug;
        };
        
        $instance = new $dto([
            'name' => '  This is a Very Long Product Name  ',
            'slug' => null
        ]);
        
        $this->assertEquals('This is a Very Long Product Name', $instance->name);
        $this->assertEquals('this-is-a-very-long', $instance->slug);
        $this->assertTrue(strlen($instance->slug) <= 20); // Respects maxLength
    }
    
    public function test_slug_transforms_own_value_when_provided(): void
    {
        $dto = new class extends LaravelArcDTO {
            #[Property(type: 'string', required: false)]
            public ?string $title;
            
            #[Property(
                type: 'string',
                required: false,
                transform: [UnderscoreSlugTransformer::class]  // separator: '_'
            )]
            public ?string $slug;
        };
        
        $instance = new $dto([
            'title' => 'Some Title',
            'slug' => 'Custom Slug Value!' // This gets transformed
        ]);
        
        $this->assertEquals('Some Title', $instance->title);
        $this->assertEquals('custom_slug_value', $instance->slug); // Underscore separator
    }
}

// Custom transformers used in tests
class TitleToSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(sourceField: 'title');
    }
}

class NameToSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(sourceField: 'name', maxLength: 20);
    }
}

class UnderscoreSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(separator: '_');
    }
}

Advanced SlugTransformer Features

  • Cross-field transformation: Generate slug from another field
  • Length limiting: Truncate with word boundary preservation
  • Custom separators: Use -, _, or any character
  • Multilingual support: Proper transliteration for different languages
  • Context awareness: Access to all DTO fields during transformation
// Example with multiple language slugs
class MultilingualArticleDTO extends LaravelArcDTO
{
    #[Property(type: 'string', required: true)]
    public string $title_en;
    
    #[Property(type: 'string', required: false)]
    public ?string $title_fr;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [EnglishSlugTransformer::class]
    )]
    public ?string $slug_en;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [FrenchSlugTransformer::class]
    )]
    public ?string $slug_fr;
}

class EnglishSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(
            sourceField: 'title_en',
            language: 'en',
            maxLength: 60
        );
    }
}

class FrenchSlugTransformer extends SlugTransformer
{
    public function __construct()
    {
        parent::__construct(
            sourceField: 'title_fr',
            language: 'fr',
            maxLength: 60
        );
    }
}

๐Ÿ”— Chaining Transformations

Apply multiple transformations in sequence by providing an array:

use Grazulex\Arc\Transformers\{TrimTransformer, LowercaseTransformer};

class UserDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        validation: 'email',
        transform: [
            TrimTransformer::class,      // 1. Remove whitespace
            LowercaseTransformer::class  // 2. Convert to lowercase
        ]
    )]
    public string $email;
}

// Input: '  [email protected]  '
// Step 1: '[email protected]'
// Step 2: '[email protected]'

Complex Chaining Example

class ProductDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        transform: [
            TrimTransformer::class,
            LowercaseTransformer::class,
            SlugTransformer::class  // Custom transformer
        ]
    )]
    public string $slug;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [
            TrimTransformer::class,
            UppercaseTransformer::class
        ]
    )]
    public ?string $sku;
}

$product = new ProductDTO([
    'slug' => '  My Great Product  ',  // becomes: 'my-great-product'
    'sku' => '  abc123  '              // becomes: 'ABC123'
]);

๐Ÿ  Custom Transformers

Create your own transformers by implementing the TransformerInterface:

Basic Custom Transformer

use Grazulex\Arc\Contracts\TransformerInterface;
use Illuminate\Support\Str;

class SlugTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }

        return Str::slug($value);
    }
}

Advanced Custom Transformer with Configuration

class PhoneTransformer implements TransformerInterface
{
    public function __construct(
        private string $defaultCountryCode = '+1'
    ) {}
    
    public function transform(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }
        
        // Remove all non-numeric characters
        $phone = preg_replace('/[^0-9]/', '', $value);
        
        // Add country code if not present
        if (!str_starts_with($phone, $this->defaultCountryCode)) {
            $phone = $this->defaultCountryCode . $phone;
        }
        
        return $phone;
    }
}

Transformer with Dependencies

class GeocodeTransformer implements TransformerInterface
{
    public function __construct(
        private GeocodeService $geocodeService
    ) {}
    
    public function transform(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }
        
        // Transform address to coordinates
        $coordinates = $this->geocodeService->geocode($value);
        
        return [
            'address' => $value,
            'latitude' => $coordinates['lat'],
            'longitude' => $coordinates['lng']
        ];
    }
}

๐Ÿ“š Real-World Examples

User Registration DTO

class UserRegistrationDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        validation: 'max:255',
        transform: [TrimTransformer::class]
    )]
    public string $first_name;
    
    #[Property(
        type: 'string',
        required: true,
        validation: 'max:255',
        transform: [TrimTransformer::class]
    )]
    public string $last_name;
    
    #[Property(
        type: 'string',
        required: true,
        validation: 'email|unique:users,email',
        transform: [
            TrimTransformer::class,
            LowercaseTransformer::class
        ]
    )]
    public string $email;
    
    #[Property(
        type: 'string',
        required: true,
        validation: 'min:8',
        transform: [HashTransformer::class]
    )]
    public string $password;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [PhoneTransformer::class]
    )]
    public ?string $phone;
}

Product Creation DTO

class ProductDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        validation: 'max:255',
        transform: [TrimTransformer::class]
    )]
    public string $name;
    
    #[Property(
        type: 'string',
        required: true,
        transform: [
            TrimTransformer::class,
            LowercaseTransformer::class,
            SlugTransformer::class
        ]
    )]
    public string $slug;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [
            TrimTransformer::class,
            UppercaseTransformer::class
        ]
    )]
    public ?string $sku;
    
    #[Property(
        type: 'float',
        required: true,
        validation: 'min:0',
        transform: [PriceTransformer::class]  // Custom: round to 2 decimals
    )]
    public float $price;
}

API Token DTO

class ApiTokenDTO extends LaravelArcDTO
{
    #[Property(
        type: 'string',
        required: true,
        transform: [
            TrimTransformer::class,
            LowercaseTransformer::class
        ]
    )]
    public string $name;
    
    #[Property(
        type: 'string',
        required: false,
        transform: [GenerateTokenTransformer::class]  // Auto-generate if not provided
    )]
    public ?string $token;
    
    #[Property(
        type: 'array',
        required: false,
        default: [],
        transform: [ArrayNormalizeTransformer::class]  // Remove duplicates, sort
    )]
    public array $scopes;
}

โšก Performance Considerations

Conditional Transformations

class ConditionalTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        // Only transform if value meets certain criteria
        if (empty($value) || !is_string($value)) {
            return $value;
        }
        
        // Expensive operation only when needed
        return $this->expensiveTransformation($value);
    }
    
    private function expensiveTransformation(string $value): string
    {
        // Complex transformation logic
        return $value;
    }
}

Cached Transformations

class CachedSlugTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }
        
        return Cache::remember(
            'slug_' . md5($value),
            now()->addHours(24),
            fn() => Str::slug($value)
        );
    }
}

๐Ÿ› ๏ธ Testing Transformations

Unit Testing Transformers

class SlugTransformerTest extends TestCase
{
    public function test_transforms_string_to_slug(): void
    {
        $transformer = new SlugTransformer();
        
        $this->assertEquals('hello-world', $transformer->transform('Hello World'));
        $this->assertEquals('test-123', $transformer->transform('Test 123'));
        $this->assertEquals('', $transformer->transform(''));
    }
    
    public function test_handles_non_string_values(): void
    {
        $transformer = new SlugTransformer();
        
        $this->assertEquals(123, $transformer->transform(123));
        $this->assertEquals(null, $transformer->transform(null));
        $this->assertEquals([], $transformer->transform([]));
    }
}

Integration Testing with DTOs

class UserDTOTest extends TestCase
{
    public function test_email_transformation(): void
    {
        $user = new UserDTO([
            'email' => '  [email protected]  '
        ]);
        
        $this->assertEquals('[email protected]', $user->email);
    }
    
    public function test_chained_transformations(): void
    {
        $product = new ProductDTO([
            'slug' => '  My Great Product  '
        ]);
        
        $this->assertEquals('my-great-product', $product->slug);
    }
}

๐Ÿ“– Best Practices

1. Keep Transformers Simple

// โœ… Good - Single responsibility
class TrimTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        return is_string($value) ? trim($value) : $value;
    }
}

// โŒ Bad - Multiple responsibilities
class ComplexTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        // Don't do multiple things in one transformer
        return strtolower(trim($value));
    }
}

2. Handle Edge Cases

class SafeTransformer implements TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        // Always check type before transforming
        if (!is_string($value)) {
            return $value;
        }
        
        // Handle empty values gracefully
        if (empty($value)) {
            return $value;
        }
        
        return $this->doTransformation($value);
    }
}

3. Order Matters

// โœ… Good - Logical order
transform: [
    TrimTransformer::class,       // 1. Clean whitespace first
    LowercaseTransformer::class,  // 2. Then normalize case
    SlugTransformer::class        // 3. Finally format
]

// โŒ Bad - Illogical order
transform: [
    SlugTransformer::class,       // This might not work as expected
    TrimTransformer::class,       // if the slug transformer expects clean input
    LowercaseTransformer::class
]

๐Ÿ”— Related Pages


โฌ…๏ธ Back to: Advanced Features | โžก๏ธ Next: Auto-Discovery Relations