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
- Property Attributes - Learn about all Property options
- Advanced Features - Overview of all advanced features
- Debug & Analysis Tools - Test your transformations
- Best Practices - Recommended patterns
- Examples Gallery - Real-world usage examples
โฌ ๏ธ Back to: Advanced Features | โก๏ธ Next: Auto-Discovery Relations