laravel-data

repository·main·Indexed 23 days ago

https://github.com/spatie/laravel-data

A Laravel package for creating rich, typed data objects that serve as a single source of truth. It replaces the need for separate Form Requests, API Transformers, and manual TypeScript definitions by providing automated transformation, request handling, validation based on PHP type hints, and TypeScript definition generation.

Tokens
61K
Snippets
179
Records
260
Agent score
80%

What's inside laravel-data

  1. Overview of laravel-data capabilities

    main

    By extending the Data class, you gain several automated features for handling data within Laravel applications:

    • Transformation: Automatically transform data objects into resources (similar to Laravel API resources).
    • Lazy Loading: Transform only requested parts of data objects using lazy properties.
    • Request Handling: Automatically create and validate data objects from incoming request data.
    • Validation: Automatically resolve validation rules based on PHP type hints and property definitions.
    • Type Safety: Ensure data is typed when moving between the frontend and backend.
    • Frontend Integration: Automatically generate TypeScript definitions from your PHP data objects.
    • Eloquent Integration: Save data objects as properties of an Eloquent model.
  2. What can you do with Laravel Data?

    main

    By extending the Data class, you gain several automated capabilities:

    • Transformation: Automatically transform data objects into resources (similar to Laravel API resources).
    • Lazy Loading: Transform only requested parts of data objects using lazy properties.
    • Request Handling: Automatically create and validate data objects from request data.
    • Validation: Automatically resolve validation rules from property types and support automatic validation upon instantiation.
    • Frontend Integration: Generate TypeScript definitions from your PHP data objects.
    • Eloquent Integration: Save data objects as properties of an Eloquent model.
    • Flexibility: Construct data objects from various input types.
  3. Create Conditional and Relational Lazy properties

    main

    You can control when a lazy property is included based on internal state or model relations:

    • Conditional Lazy: Use Lazy::when() to include a property only if a specific condition is met. Note that if the condition is false, you cannot manually include() it later.
    • Relational Lazy: Use Lazy::whenLoaded() to include a property only if a specific Eloquent relation has already been loaded on the model.
    • Default Included: Use ->defaultIncluded() on a Lazy instance to make a property always present unless explicitly removed via exclude().
  4. Map property names with MapName, MapInputName, and MapOutputName

    main

    You can map the names of properties for input (creating data objects) and output (converting data objects to arrays/JSON) using PHP attributes.

    • #[MapName('name')]: Maps both input and output names.
    • #[MapInputName('name')]: Maps only the name used when creating the data object from an array.
    • #[MapOutputName('name')]: Maps only the name used when exporting the data object.

    Crucial Rule: When using these attributes, most internal Laravel Data features (validation rules, includes, excludes, excepts, and only) still require the original property name, not the mapped name.

    class UserData extends Data
    {
        public function __construct(
            #[MapName('favorite_song')] // name mapping
            public Lazy|SongData $song,
            public string $title,
        ) {
        }
    }
  5. How nested wrapping works

    main

    Wrapping behavior changes depending on whether the object is a single Data object or a DataCollection when nested inside another Data object:

    1. Nested Data Objects: A single Data object included as a property in another Data object will never be wrapped, even if a wrap key is set. It will always appear as a direct property.
    2. Nested DataCollections: A DataCollection inside a Data object will be wrapped if a wrapping key is set, allowing you to mimic Laravel Resource behavior.
    // Nested DataCollection will be wrapped
    class AlbumData extends Data
    {
        public function __construct(
            public string $title,
            #[DataCollectionOf(SongData::class)]
            public DataCollection $songs,
        ) {}
    
        public static function fromModel(Album $album): self
        {
            return new self(
                $album->title,
                // This collection will be wrapped under the 'data' key
                SongData::collect($album->songs, DataCollection::class)->wrap('data')
            );
        }
    }
  6. Configure default wrapping keys

    main

    You can define how data objects are wrapped using three different methods:

    1. Per-class default: Define a defaultWrap() method inside your Data object to return a specific key.
    2. Global configuration: Set a global wrap key in your config/data.php file. Setting this to null disables global wrapping.
    3. Manual wrapping: Use the wrap() method on an instance as needed.
    // Per-class default
    class SongData extends Data
    {
        public function defaultWrap(): string
        {
            return 'data';
        }
    }
    
    // Global configuration in config/data.php
    'wrap' => 'data',
  7. Validation when using Livewire

    main

    When using Data objects within Livewire components, laravel-data does not provide automatic validation.

    You must handle validation yourself. This is because laravel-data currently only supports validating payloads that are being transformed into data objects, rather than validating the data objects themselves during Livewire's hydration process.

  8. Control route parameter priority over request body

    main

    By default, values injected from route parameters take priority over values present in the request body. This is useful for ensuring IDs match the route.

    If you want to allow the request body to override the route parameter (for example, when updating a slug), set the replaceWhenPresentInPayload flag to false in the attribute.

    class SongData extends Data {
        #[FromRouteParameter('slug', replaceWhenPresentInPayload: false )]
        public string $slug;
    }
  9. Validate nested Data objects and collections

    main

    Validation rules propagate through nested structures:

    • Nested Data Objects: If a property is another Data object, the package generates rules for the nested object's properties using dot notation (e.g., artist.name).
    • Nested Data Collections: If a property is an array of Data objects (e.g., array<SongData>), the package applies the rules of the child object to each element using wildcard notation (e.g., songs.*.title).

    Example of a collection:

    class AlbumData extends Data
    {
        /**
        * @param array<SongData> $songs
        */
        public function __construct(
            public string $title,
            public array $songs,
        ) {}
    }

    This generates rules like songs.*.title and songs.*.artist based on the SongData definition.

  10. Skip validation for all properties in a Data class

    main

    To skip validation for every property within a Data class globally or during specific operations, you can use one of two methods:

    1. Data Factories: Use the built-in data factories to generate instances without triggering validation.
    2. Configuration: Set the validation_strategy in your config/data.php file to define how validation should be handled across the application.
  11. Use abstract Data classes in collections

    main

    You can use an abstract Data class as the type for elements within an array property. When using docblock annotations, ensure you specify the abstract class to allow the package to handle polymorphic instantiation (if PropertyMorphableData is implemented) or standard collection mapping.

    class Band extends Data
    {
        public string $name;
        
        /**  @var array<Person> */
        public array $members;
    }
  12. Understand NamedType and Composite Types

    main

    The package uses specific structures to represent different ways types are defined in PHP.

    NamedType properties:

    • name: The name of the type.
    • builtIn: Whether the type is a built-in PHP type.
    • acceptedTypes: An array of accepted types as strings.
    • kind: The DataTypeKind of the type.
    • dataClass: The data object class of the property or the collection it collects.
    • dataCollectableClass: The collectable type of the data objects.
    • isCastable: Whether the type is Castable.

    UnionType and IntersectionType properties:

    • types: An array of types, which can themselves be NamedType, UnionType, or IntersectionType.