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 needsconfig/expressive.phporstubs/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.namespaceandexpressive.suffixbefore relying on implicit lookup or runningmake:expressive - set
expressive.stricttotruewhen Expressive objects should convert back to Eloquent models but never write database state throughsave() - set
expressive.diagnostics.throw_on_unfillabletotrueduring adoption when ignored unfillable properties should fail fast - set
expressive.serialization.casetosnakeonly when serialized keys should be snake case globally
3. Define mappings deliberately
- add
WendellAdriel\Expressive\Concerns\IsExpressiveonly 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\Expressiveand 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\Collectionwith@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 executeget()immediately and return an in-memoryIlluminate\Support\Collection - use
$builder->expressiveChunk($count, $callback, relationships: [...], attributes: [...])for large result sets that can be processed with Laravelchunk(); the count must be at least1 - pass relationship names to
relationshipsinstead of relying on lazy loading; Expressive usesloadMissing()and leaves unloaded relationships asnull - pass accessor keys to
attributesfor virtual properties; Expressive calls Eloquentappend()and leaves unrequested virtual values asnull - 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:generateto scanapp/Models, or pass one or more--pathoptions for explicit model directories - use
--excludewithexpressive:generateto 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--forceonly for the current generation need - remember hidden attributes are generated by default, but serialization still respects the mapped model's
hiddenandvisiblerules - use
php artisan expressive:sync User --model="App\Models\User"to detect drift;--modelis required - use
php artisan expressive:sync --allin CI to scan the configured Expressive namespace and fail when generated classes drift from their mapped models - combine
expressive:sync --all --writeonly when generated classes are safe to rewrite in bulk - add
--writeonly 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(), orjson_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
nullare 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 whenexpressive.strictisfalse:BelongsTo,HasOne,MorphOne,HasMany, andMorphMany - 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, andphp artisan help expressive:sync
Examples
- For a read boundary, add
IsExpressivetoApp\Models\User, runphp artisan make:expressive User --model="App\Models\User", keep#[Relationship] public ?Collection $posts = null;, then returnUser::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 --alland review differences before choosing either manual class edits or safe--writerewrites.
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()whenexpressive.strictis enabled; convert withmodel()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 --forceorexpressive:sync --writeover custom classes without reviewing intentional user edits - do not assume
expressive:sync --allcan decide whether customized class drift is intentional; treat its output as review input - do not document private package classes as application-facing APIs