Skill for creating and editing Spatie Laravel Data classes following Prowi conventions. Use when working with Data classes, DTOs, or data transfer objects...
You are an expert at working with Spatie Laravel Data classes in the Prowi application. Your role is to create clean, validated Data classes that follow established patterns.
This is the #1 mistake to avoid! Data classes use constructor property promotion - all properties MUST be defined in the constructor.
class UserData extends Data
{
// BAD - Don't define properties here!
public string $name;
public string $email;
public int $age;
public function __construct()
{
// Properties should be here instead
}
}
class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
Why constructor property promotion:
Data classes should be organized by their usage context, NOT in a flat directory structure.
Place Data classes in nested directories that mirror where they are used:
app/Data/
├── Http/
│ └── Controllers/
│ └── Api/
│ ├── AgentController/
│ │ ├── AgentData.php
│ │ └── AgentListData.php
│ └── ConversationController/
│ ├── ConversationData.php
│ └── MessageData.php
├── Inertia/
│ ├── ConversationListItemData.php
│ └── DashboardData.php
└── Mcp/
└── Tools/
├── SendMessageResult/
│ └── SendMessageResultData.php
└── ListAgents/
└── AgentListData.php
For API Controller Responses:
app/Http/Controllers/Api/AgentController.phpapp/Data/Http/Controllers/Api/AgentController/AgentData.phpApp\Data\Http\Controllers\Api\AgentControllerFor Inertia Props:
app/Data/Inertia/ConversationListItemData.phpApp\Data\InertiaFor MCP Tool Results:
app/Mcp/Tools/SendMessageTool.phpapp/Data/Mcp/Tools/SendMessageTool/SendMessageResultData.phpApp\Data\Mcp\Tools\SendMessageToolIf a Data class is used across multiple contexts, consider placing it in a shared location:
app/Data/Shared/UserData.phpapp/Data/Common/PaginationData.php<?php
namespace App\Data;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
class UserData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
#[Required]
#[StringType]
public string $email,
) {}
}
Spatie\LaravelData\Datapublic visibilitymessages() method for custom error messagesDo NOT manually write validation rules! Use annotations instead.
class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
// BAD - Don't manually define rules!
public function rules(): array
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
];
}
}
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Email;
class UserData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
#[Required]
#[Email]
public string $email,
) {}
}
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Present;
public function __construct(
#[Required]
#[StringType]
public string $name,
#[Nullable]
#[StringType]
public ?string $description,
#[Required]
#[IntegerType]
#[Min(0)]
#[Max(100)]
public int $age,
#[BooleanType]
public bool $isActive = false,
#[Required]
#[Email]
public string $email,
#[Present]
#[Nullable]
public mixed $data,
) {}
Important: Each annotation should be on its own line for better readability and maintainability.
Always use Illuminate\Support\Collection with #[DataCollectionOf] annotation, NOT arrays!
class AlbumData extends Data
{
public function __construct(
// BAD - Don't use array!
public array $songs,
) {}
}
use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\DataCollectionOf;
class AlbumData extends Data
{
public function __construct(
#[DataCollectionOf(SongData::class)]
public Collection $songs,
) {}
}
Why Collection over array:
Always use #[WithCast(EnumCast::class)] for enum properties:
use App\Enums\UserStatusEnum;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\EnumCast;
use Spatie\LaravelData\Attributes\Validation\Required;
class UserData extends Data
{
public function __construct(
#[Required]
#[WithCast(EnumCast::class)]
public UserStatusEnum $status,
) {}
}
Use Optional|null|Type $prop = new Optional pattern:
use Spatie\LaravelData\Optional;
class UserData extends Data
{
public function __construct(
// Required field
#[Required]
#[StringType]
public string $name,
// Optional field - can be omitted, null, or string
public Optional|null|string $nickname = new Optional,
// Optional integer
public Optional|null|int $age = new Optional,
) {}
}
Why this pattern:
null value#[RequiredIf] annotationsUse static messages() method for custom error messages:
use App\Data\LaravelData\Attributes\Validation\VariableKey;
class ObjectDefinitionData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
#[Required]
#[VariableKey]
public string $data_key,
) {}
public static function messages(): array
{
return [
'name.required' => 'The name is required.',
'name.string' => 'The name must be a string.',
'data_key.required' => 'The data key is required.',
'data_key.regex' => VariableKey::getErrorMessage(),
];
}
}
Add #[TypeScript()] attribute to export to frontend:
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript()]
class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
}
This generates TypeScript types in resources/js/types/generated.d.ts.
Add PHPDoc blocks to describe properties with proper Collection typing:
/**
* Filter data for querying data objects
*
* @property LogicalOperatorsEnum $preOperator The logical operator (AND/OR)
* @property Collection<int, SentenceData> $sentences The filter sentences
*/
class FilterData extends Data
{
public function __construct(
#[Required]
#[WithCast(EnumCast::class)]
public LogicalOperatorsEnum $preOperator,
#[Required]
#[DataCollectionOf(SentenceData::class)]
public Collection $sentences,
) {}
}
Important: For Collection properties in PHPDoc, always specify both key and value types: Collection<int, ValueType>
/**
* @property Collection<int, UserData> $users List of users
* @property Collection<int, ObjectDefinitionColumnData> $columns The columns
*/
Add convenience factory methods for common use cases:
class ObjectDefinitionColumnData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $column_key,
#[Required]
#[StringType]
public string $column_name,
#[Required]
#[WithCast(EnumCast::class)]
public ColumnTypeEnum $column_type,
#[BooleanType]
public bool $is_required = false,
) {}
/**
* Create a string column
*/
public static function stringColumn(
string $column_key,
?string $column_name = null,
bool $is_required = false,
): self {
return self::from([
'column_key' => $column_key,
'column_name' => $column_name ?? $column_key,
'column_type' => ColumnTypeEnum::STRING,
'is_required' => $is_required,
]);
}
/**
* Create an integer column
*/
public static function integerColumn(
string $column_key,
?string $column_name = null,
bool $is_required = false,
): self {
return self::from([
'column_key' => $column_key,
'column_name' => $column_name ?? $column_key,
'column_type' => ColumnTypeEnum::INTEGER,
'is_required' => $is_required,
]);
}
}
<?php
namespace App\Data\ObjectDefinition;
use App\Data\LaravelData\Attributes\Validation\UniqueInCollection;
use App\Data\LaravelData\Attributes\Validation\VariableKey;
use App\Enums\ObjectDefinition\ColumnTypeEnum;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\EnumCast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
/**
* Represents an object definition in the system
*
* @property string $name The display name
* @property string $data_key The unique data key (lowercase, alphanumeric, underscores)
* @property Collection<int, ObjectDefinitionColumnData> $columns The columns in this definition
* @property UserAssociationConfigData $user_association_config User association configuration
*/
#[TypeScript()]
class ObjectDefinitionData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
#[Required]
#[VariableKey]
public string $data_key,
#[DataCollectionOf(ObjectDefinitionColumnData::class)]
public Collection $columns,
public UserAssociationConfigData $user_association_config,
#[BooleanType]
public bool $belongs_to_customer_user = false,
#[BooleanType]
public bool $can_be_deleted = true,
#[BooleanType]
public bool $can_be_updated = true,
#[BooleanType]
public bool $is_system_definition = false,
#[Nullable]
#[StringType]
public ?string $primary_title = null,
#[Nullable]
#[StringType]
public ?string $description = null,
#[IntegerType]
public int|Optional $customer_id = new Optional,
#[Nullable]
#[IntegerType]
public ?int $id = null,
) {}
/**
* Get a column by its key
*/
public function getColumnWithKey(string $key): ?ObjectDefinitionColumnData
{
return $this->columns->first(
fn (ObjectDefinitionColumnData $column) => $column->column_key === $key
);
}
/**
* Convert to array suitable for Eloquent model
*/
public function toModelArray(): array
{
return collect($this->toArray())
->except('columns')
->toArray();
}
/**
* Custom validation error messages
*/
public static function messages(): array
{
return [
'name.required' => 'The object definition name is required.',
'name.string' => 'The object definition name must be a string.',
'data_key.required' => 'The data key is required.',
'data_key.regex' => VariableKey::getErrorMessage(),
'columns.unique_in_collection.column_name' => 'Column names must be unique.',
'columns.unique_in_collection.column_key' => 'Column keys must be unique.',
];
}
}
// 1. Properties outside constructor
class UserData extends Data
{
public string $name; // WRONG!
public function __construct() {}
}
// 2. Using array instead of Collection
class AlbumData extends Data
{
public function __construct(
public array $songs, // WRONG!
) {}
}
// 3. Manual rules() method
class UserData extends Data
{
public function __construct(
public string $name,
) {}
public function rules(): array // WRONG!
{
return ['name' => 'required'];
}
}
// 4. Wrong Optional pattern
class UserData extends Data
{
public function __construct(
public ?string $nickname = null, // WRONG - breaks RequiredIf
// OR
public string|Optional $nickname, // WRONG - IntelliSense complains
) {}
}
// 5. Missing enum cast
class UserData extends Data
{
public function __construct(
public UserStatusEnum $status, // WRONG - Missing #[WithCast(EnumCast::class)]
) {}
}
// 6. Wrong PHPDoc Collection format
/**
* @property Collection<UserData> $users // WRONG - Missing key type
*/
// 7. Annotations on same line
class UserData extends Data
{
public function __construct(
#[Required, StringType] // WRONG - Should be on separate lines
public string $name,
) {}
}
use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\EnumCast;
use Spatie\LaravelData\Optional;
// 1. Properties in constructor
class UserData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
) {}
}
// 2. Use Collection with annotation
class AlbumData extends Data
{
public function __construct(
#[DataCollectionOf(SongData::class)]
public Collection $songs,
) {}
}
// 3. Use annotations (no rules() method)
class UserData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
) {}
}
// 4. Correct Optional pattern
class UserData extends Data
{
public function __construct(
public Optional|null|string $nickname = new Optional,
) {}
}
// 5. Include enum cast
class UserData extends Data
{
public function __construct(
#[Required]
#[WithCast(EnumCast::class)]
public UserStatusEnum $status,
) {}
}
// 6. Correct PHPDoc Collection format
/**
* @property Collection<int, UserData> $users List of users
*/
// 7. Annotations on separate lines
class UserData extends Data
{
public function __construct(
#[Required]
#[StringType]
public string $name,
) {}
}
Before considering a Data class complete:
Spatie\LaravelData\DataCollection for collections (NOT array)#[DataCollectionOf(Class::class)] for collectionsOptional|null|Type $prop = new Optional for optional fields#[WithCast(EnumCast::class)] for enumsmessages() method for custom error messages (if needed)Collection<int, Type> format (with key type)#[TypeScript()] if used in frontend// Base
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
// Collections
use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\DataCollectionOf;
// Validation
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Present;
// Casts
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\EnumCast;
// TypeScript
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
docs/development/using-laravel-data.mdapp/Data/LaravelData/Attributes/Validation/app/Data/ for examplesTop 4 mistakes to avoid:
Collection with #[DataCollectionOf]Your goal is to create clean, validated Data classes that are type-safe, well-documented, and follow all Prowi conventions.