Integrate and use `wendelladriel/laravel-expressive` in Laravel applications. Use this when installing Expressive, configuring class lookup, creating typed Expressive objects, converting Eloquent models or collections, generating or syncing Expressive classes, serializing objects, or persisting Expressive objects back through Eloquent.
php artisan boost:add-skill wendelladriel/laravel-expressive
Save this content to: .claude/skills/expressive-development/SKILL.md
# Expressive Development
Use this skill when a Laravel application needs Typed Objects for Eloquent using Expressive.
## Primary Goal
- apply Expressive through its public Laravel integration points only
- keep Eloquent responsible for querying, relationship semantics, model events, mass assignment, and database writes
- prefer the smallest class, config, command, or mapping change that gives the application typed public properties
## Workflow
### 1. Inspect the Laravel app context
- confirm the app is a Laravel project and identify its Laravel/PHP versions before recommending installation
- inspect the target Eloquent model, casts, accessors, relationships, `$fillable`/`$guarded`, `$hidden`, and `$visible`
- check whether the application already uses DTOs, resources, actions, or domain objects so Expressive fits the existing boundary instead of replacing unrelated patterns
- decide whether the Expressive class should mirror the model shape, expose a smaller application boundary, or be generated and then edited
### 2. Install and configure only what is needed
- install with `composer require wendelladriel/laravel-expressive`; Laravel auto-discovers the service provider
- publish with `php artisan vendor:publish --tag="expressive"` only when the app needs `config/expressive.php` or `stubs/expressive.stub`
- publish only config with `php artisan vendor:publish --tag="expressive-config"` when changing namespace, suffix, strict mode, diagnostics, serialization casing, or generator defaults
- publish only the generator stub with `php artisan vendor:publish --tag="expressive-stubs"` when generated class formatting must be customized
- set `expressive.namespace` and `expressive.suffix` before relying on implicit lookup or running `make:expressive`
- set `expressive.strict` to `true` when Expressive objects should convert back to Eloquent models but never write database state through `save()`
- set `expressive.diagnostics.throw_on_unfillable` to `true` during adoption when ignored unfillable properties should fail fast
- set `expressive.serialization.case` to `snake` only when serialized keys should be snake case globally
### 3. Define mappings deliberately
- add `WendellAdriel\Expressive\Concerns\IsExpressive` only to Eloquent models that should expose `$model->expressive()`
- create Expressive classes under the configured namespace, or run `php artisan make:expressive User --model="App\Models\User"`
- run `php artisan expressive:generate --path="app/Models"` when scaffolding Expressive classes for multiple discovered Eloquent models
- Expressive classes must extend `WendellAdriel\Expressive\Expressive` and expose mapped data as public typed properties
- use `#[Model(User::class)]` on an Expressive class when the model cannot be inferred from namespace and suffix
- use `#[Expressive(UserData::class)]` on a model when it should resolve a specific Expressive class
- use `#[Map('remember_token')]` when a property maps to a non-default Eloquent key; unmapped non-relationship properties default to snake-case keys
- use `#[Relationship]` only for relationship properties, and `#[Virtual]` only for accessor/appended values
- keep relationship and virtual properties nullable because unloaded relationships and unrequested virtual accessors map to `null`
- type single relationships as `?RelatedExpressive`; type many relationships as `?Illuminate\Support\Collection` with `@var Collection<int, RelatedExpressive>|null`
- do not put mapping attributes on private or protected properties; Expressive only maps public non-static properties
### 4. Convert from Eloquent at explicit boundaries
- use `$model->expressive(relationships: [...], attributes: [...])` for a single opted-in model
- use `$eloquentCollection->expressive(relationships: [...], attributes: [...])` to convert an Eloquent collection without mutating the original collection
- use `$builder->expressive(relationships: [...], attributes: [...])` when it is acceptable to execute `get()` immediately and return an in-memory `Illuminate\Support\Collection`
- use `$builder->expressiveChunk($count, $callback, relationships: [...], attributes: [...])` for large result sets that can be processed with Laravel `chunk()`; the count must be at least `1`
- pass relationship names to `relationships` instead of relying on lazy loading; Expressive uses `loadMissing()` and leaves unloaded relationships as `null`
- pass accessor keys to `attributes` for virtual properties; Expressive calls Eloquent `append()` and leaves unrequested virtual values as `null`
- use API resources or transformers when the response contract differs from the Expressive object shape
### 5. Generate and sync classes safely
- use `php artisan make:expressive User --model="App\Models\User"` to generate a starting class from model columns, casts, relationships, and detected accessors
- use `php artisan expressive:generate` to scan `app/Models`, or pass one or more `--path` options for explicit model directories
- use `--exclude` with `expressive:generate` to skip selected model basenames or fully qualified class names
- use `--namespace`, `--suffix`, `--attributes`, `--without-attributes`, `--relationships`, `--without-relationships`, `--include-hidden`, `--exclude-hidden`, `--hint-morph-map`, `--dry-run`, and `--force` only for the current generation need
- remember hidden attributes are generated by default, but serialization still respects the mapped model's `hidden` and `visible` rules
- use `php artisan expressive:sync User --model="App\Models\User"` to detect drift; `--model` is required
- use `php artisan expressive:sync --all` in CI to scan the configured Expressive namespace and fail when generated classes drift from their mapped models
- combine `expressive:sync --all --write` only when generated classes are safe to rewrite in bulk
- add `--write` only for generated classes that are safe to rewrite; if the class has custom methods or intentional shape changes, update it manually
- treat sync output as review input because missing, stale, or type differences may be intentional application design
### 6. Serialize or persist intentionally
- use `$expressive->toArray()`, `$expressive->toJson()`, or `json_encode($expressive)` for direct serialization of initialized public properties
- expect nested Expressive objects and collections to serialize recursively
- expect uninitialized typed properties to be skipped; nullable properties with default `null` are initialized and may appear unless hidden or invisible
- use `$expressive->model()` to create an unsaved Eloquent model filled only with mapped, fillable, non-virtual, non-relationship properties
- use `$expressive->save()` to save the root model and supported direct relationships when `expressive.strict` is `false`: `BelongsTo`, `HasOne`, `MorphOne`, `HasMany`, and `MorphMany`
- keep `BelongsToMany`, morph-to-many, through relationships, pivot semantics, and deletion in explicit Eloquent application code
## Rules, References, and Templates
Read before executing:
- official documentation: `https://laravel-expressive.wendelladriel.com`
- installed package config after publishing: `config/expressive.php`
- installed package stub after publishing: `stubs/expressive.stub`
- target application models under `app/Models/`
- target application Expressive classes under the configured `expressive.namespace`
- Artisan help in the consuming app: `php artisan help make:expressive`, `php artisan help expressive:generate`, and `php artisan help expressive:sync`
## Examples
- For a read boundary, add `IsExpressive` to `App\Models\User`, run `php artisan make:expressive User --model="App\Models\User"`, keep `#[Relationship] public ?Collection $posts = null;`, then return `User::query()->where('active', true)->expressive(relationships: ['posts'])` from an application service.
- For a custom boundary, create `App\Data\PublicUser extends Expressive`, add `#[Model(App\Models\User::class)]`, include only public properties the service needs, and call `$user->expressive(attributes: ['display_name'])` when a nullable `#[Virtual]` property should be populated.
- For write input, hydrate an Expressive object from validated data, call `$object->model()` when application code should decide how to save or strict mode is enabled, or call `$object->save()` only when root fillable attributes and supported direct relationships should be persisted immediately.
- For CI drift checks, run `php artisan expressive:sync --all` and review differences before choosing either manual class edits or safe `--write` rewrites.
## Anti-patterns
- do not use Expressive as a replacement for Eloquent querying, resources, validation, authorization, model events, or relationship APIs
- do not make relationship or virtual properties non-nullable
- do not rely on lazy loading during conversion; request relationships explicitly
- do not expect virtual accessors to populate unless passed through `attributes`
- do not expect `model()` to save records or persist relationships
- do not call `save()` when `expressive.strict` is enabled; convert with `model()` and persist through Eloquent explicitly
- do not expect `save()` to handle many-to-many, morph-to-many, through relationships, pivot sync, detach, attach, or deletion
- do not use `make:expressive --force` or `expressive:sync --write` over custom classes without reviewing intentional user edits
- do not assume `expressive:sync --all` can decide whether customized class drift is intentional; treat its output as review input
- do not document private package classes as application-facing APIs
Use this skill when a Laravel application needs Typed Objects for Eloquent using Expressive.
$fillable/$guarded, $hidden, and $visiblecomposer require wendelladriel/laravel-expressive; Laravel auto-discovers the service providerphp artisan vendor:publish --tag="expressive" only when the app needs config/expressive.php or stubs/expressive.stubphp artisan vendor:publish --tag="expressive-config" when changing namespace, suffix, strict mode, diagnostics, serialization casing, or generator defaultsphp artisan vendor:publish --tag="expressive-stubs" when generated class formatting must be customizedexpressive.namespace and expressive.suffix before relying on implicit lookup or running make:expressiveexpressive.strict to true when Expressive objects should convert back to Eloquent models but never write database state through save()expressive.diagnostics.throw_on_unfillable to true during adoption when ignored unfillable properties should fail fastexpressive.serialization.case to snake only when serialized keys should be snake case globallyWendellAdriel\Expressive\Concerns\IsExpressive only to Eloquent models that should expose $model->expressive()php artisan make:expressive User --model="App\Models\User"php artisan expressive:generate --path="app/Models" when scaffolding Expressive classes for multiple discovered Eloquent modelsWendellAdriel\Expressive\Expressive and expose mapped data as public typed properties#[Model(User::class)] on an Expressive class when the model cannot be inferred from namespace and suffix#[Expressive(UserData::class)] on a model when it should resolve a specific Expressive class#[Map('remember_token')] when a property maps to a non-default Eloquent key; unmapped non-relationship properties default to snake-case keys#[Relationship] only for relationship properties, and #[Virtual] only for accessor/appended valuesnull?RelatedExpressive; type many relationships as ?Illuminate\Support\Collection with @var Collection<int, RelatedExpressive>|null$model->expressive(relationships: [...], attributes: [...]) for a single opted-in model$eloquentCollection->expressive(relationships: [...], attributes: [...]) to convert an Eloquent collection without mutating the original collection$builder->expressive(relationships: [...], attributes: [...]) when it is acceptable to execute get() immediately and return an in-memory Illuminate\Support\Collection$builder->expressiveChunk($count, $callback, relationships: [...], attributes: [...]) for large result sets that can be processed with Laravel chunk(); the count must be at least 1relationships instead of relying on lazy loading; Expressive uses loadMissing() and leaves unloaded relationships as nullattributes for virtual properties; Expressive calls Eloquent append() and leaves unrequested virtual values as nullphp artisan make:expressive User --model="App\Models\User" to generate a starting class from model columns, casts, relationships, and detected accessorsphp artisan expressive:generate to scan app/Models, or pass one or more --path options for explicit model directories--exclude with expressive:generate to skip selected model basenames or fully qualified class names--namespace, --suffix, --attributes, --without-attributes, --relationships, --without-relationships, --include-hidden, --exclude-hidden, --hint-morph-map, --dry-run, and --force only for the current generation needhidden and visible rulesphp artisan expressive:sync User --model="App\Models\User" to detect drift; --model is requiredphp artisan expressive:sync --all in CI to scan the configured Expressive namespace and fail when generated classes drift from their mapped modelsexpressive:sync --all --write only when generated classes are safe to rewrite in bulk--write only for generated classes that are safe to rewrite; if the class has custom methods or intentional shape changes, update it manually$expressive->toArray(), $expressive->toJson(), or json_encode($expressive) for direct serialization of initialized public propertiesnull are initialized and may appear unless hidden or invisible$expressive->model() to create an unsaved Eloquent model filled only with mapped, fillable, non-virtual, non-relationship properties$expressive->save() to save the root model and supported direct relationships when expressive.strict is false: BelongsTo, HasOne, MorphOne, HasMany, and MorphManyBelongsToMany, morph-to-many, through relationships, pivot semantics, and deletion in explicit Eloquent application codeRead before executing:
https://laravel-expressive.wendelladriel.comconfig/expressive.phpstubs/expressive.stubapp/Models/expressive.namespacephp artisan help make:expressive, php artisan help expressive:generate, and php artisan help expressive:syncIsExpressive to App\Models\User, run php artisan make:expressive User --model="App\Models\User", keep #[Relationship] public ?Collection $posts = null;, then return User::query()->where('active', true)->expressive(relationships: ['posts']) from an application service.App\Data\PublicUser extends Expressive, add #[Model(App\Models\User::class)], include only public properties the service needs, and call $user->expressive(attributes: ['display_name']) when a nullable #[Virtual] property should be populated.$object->model() when application code should decide how to save or strict mode is enabled, or call $object->save() only when root fillable attributes and supported direct relationships should be persisted immediately.php artisan expressive:sync --all and review differences before choosing either manual class edits or safe --write rewrites.attributesmodel() to save records or persist relationshipssave() when expressive.strict is enabled; convert with model() and persist through Eloquent explicitlysave() to handle many-to-many, morph-to-many, through relationships, pivot sync, detach, attach, or deletionmake:expressive --force or expressive:sync --write over custom classes without reviewing intentional user editsexpressive:sync --all can decide whether customized class drift is intentional; treat its output as review inputHow can I help you explore Laravel packages today?