Skip to content

Upgrading to 4.0 ​

4.0 is a major release. This page lists what changes when a project moves from 3.x.

Laravel 12 or 13 ​

4.0 needs Laravel 12 or 13. Composer picks the right line on its own: on a Laravel 10 or 11 project, the usual install command resolves to 3.15, which carries every generator fix released before 4.0.

bash
composer require --dev nameless/laravel-api-generator

Once the project runs Laravel 12 or 13, update the package:

bash
composer require --dev nameless/laravel-api-generator:^4.0

The MCP server still needs laravel/mcp, which asks for Laravel 12.41 or later on the 12.x line.

Regenerate your entities ​

Updating the package changes no file in your project. The changes below reach an entity when you regenerate it. Files you edited by hand since the generator wrote them are kept and named in a modified_file_kept warning. Port the changes described here into them, or regenerate them with --force and redo your edits, since the new DTO only accepts the new requests.

Controllers and routes ​

The controllers receive the model through route model binding, so show(Post $post) replaces show(int|string $id) and its find() call. The variable is named after the parameter that Route::apiResource() registers, usually the lowercase singular ($post, $blogpost) and sometimes not ($medium for Media), because binding matches by name only.

With --soft-deletes, the restore and force-delete routes take the same parameter and call withTrashed(), so a soft deleted model still resolves:

POST   /api/posts/{post}/restore
DELETE /api/posts/{post}/force-delete

Regenerating the entity replaces the {id} routes that 3.x wrote. restore now returns the restored resource instead of a message, and the delete actions answer 204 No Content.

Policies ​

Every controller action now asks the entity's policy first, through Gate::authorize(). The generated policies accept guests (?User $user) and return true, so an API without --auth stays public until you tighten a policy. Returning false from update(), for instance, makes PATCH /api/posts/1 answer 403.

A policy generated by 3.x types its first parameter as User $user, which Laravel reads as "no guests". On an API without authentication, every request would get a 403. Regenerate the policy, or make that parameter nullable.

store and update validate the request before the policy runs, so a client the policy refuses still gets a 422 when its data is invalid. If that matters for your API, move the check into the authorize() method of the request.

Store and Update requests ​

PostRequest becomes StorePostRequest and UpdatePostRequest. The update request prefixes every rule with sometimes, so a PATCH may send only the fields it changes, and its unique rules still ignore the current row. Nullable fields validate with nullable instead of sometimes, which accepts an explicit null.

After regenerating, the old PostRequest.php is no longer used. The generation warns about it (legacy_request) until you move your own rules into the new requests and delete it. delete:fullapi removes it along with the rest.

DTOs and partial updates ​

The DTO is built from $request->validated() and remembers which fields the request sent. Its properties are nullable with a null default, and toArray() returns the sent fields only. The services save $dto->toArray(), so a PATCH leaves the other columns as they are. A DTO you build yourself, as the generated unit tests do, still saves every property.

A field named provided is refused, since the DTO keeps its list of sent fields under that name.

Paginated index ​

The index is paginated, filterable and sortable without any extra package. GET /api/posts?filter[status]=draft&sort=-created_at&page=2&per_page=20 answers data, links and meta, and the service method behind it, getAll(), becomes paginate(array $query). Clients that read the whole list in one call now get 15 items per page, and the plain filters of 3.x (?status=draft) become filter[status]=draft. With --query-builder, filters match exact values instead of Spatie's default partial match.

The page size comes from a new config file, read when you generate. Publish it with php artisan vendor:publish --tag=api-generator-config to change the default of 15 or the cap of 100.

Models and enums ​

On Laravel 13, the model declares its key and its fillable columns with class attributes. The generator reads the Laravel version when it runs, so a Laravel 12 project keeps the $primaryKey, $keyType, $incrementing and $fillable properties:

php
#[Table(key: 'code', keyType: 'string', incrementing: false)]
#[Fillable(['code', 'name'])]
class Country extends Model

#[Table] only appears with a custom primary key. On both versions, the casts move from the $casts property to a casts() method, and --add-fields adds to whichever one the model has.

Enums are named after the entity and the field: status on Post gives App\Enums\PostStatus instead of App\Enums\Status, so two entities with a status field no longer write to the same enum. The enum written by 3.x stays in place. The generation names it in a legacy_enum warning, and you delete it once no code uses it. delete:fullapi removes the enums of the entity it deletes.

Generated code ​

The generated files pass pint --test with the Laravel preset: sorted imports and short class names, routes included. routes/api.php imports each controller, and regenerating an entity rewrites the fully qualified references written by 3.x.

A few behaviors change along the way:

  • json fields validate with array instead of json. Clients send an object or a list, and a JSON-encoded string gets a 422.
  • The DTO casts string fields too, so a date or a code sent as a number, such as 20250101, no longer ends in a 500.
  • The resource of an entity with a custom primary key no longer returns an id key, which was always null.
  • The columns of a morphTo relation are nullable (nullableMorphs), since the generated requests do not fill them. Attach the owner through the relation, for example $post->comments()->create([...]).

Published stubs ​

Stubs published under stubs/vendor/laravel-api-generator keep taking precedence, so compare them with the new ones. php artisan api-generator:validate-stubs points at what a 3.x stub misses:

  • request.stub is no longer read. Copy your changes into request.store.stub and request.update.stub, then delete it.
  • controller.stub and controller.query-builder.stub need {{routeParameter}}, the variable the route binds.
  • dto.stub needs {{attributesFromValidated}} in place of {{attributesFromRequest}}.
  • service.stub and service.query-builder.stub must save $dto->toArray() instead of get_object_vars($dto), or a PATCH would clear the fields it leaves out.
  • policy.stub needs {{modelVariable}}.
  • service.stub and service.query-builder.stub need {{allowedFilters}}, {{allowedSorts}}, {{perPage}} and {{maxPerPage}}.
  • test.unit.stub and test.unit.pest.stub must call paginate(), since getAll() no longer exists.
  • model.stub needs {{members}}, which holds the traits, the properties, casts() and the relations, and {{classAttributes}} right above class, where the Laravel 13 attributes go. A 3.x model stub still gets {{traits}}, {{fillable}} and {{relationships}}, so it keeps generating a working model, with properties.
  • migrations.stub needs {{columns}} for the whole table body. The 3.x placeholders are still filled.
  • request.store.stub and request.update.stub need {{imports}} after the FormRequest import, for Rule and the enums their rules use.

The feature test stubs gain optional placeholders for the new cases: {{patchFields}}, {{patchAssertion}}, {{patchedColumns}}, {{softDeleteTests}}, {{filterField}}, {{primaryKey}} and {{sortAssertion}}.

Removed ​

  • config/laravel-api-generator.php. The generator never loaded it, so its paths, namespaces and field types had no effect. If you copied it into your project, delete it.
  • generateFromJson() and deleteCompleteApi() on ApiGenerationServiceInterface, deprecated since 3.8. Generate with php artisan make:fullapi (a schema file, class_data.json or any other source) and delete with php artisan delete:fullapi.