gts-meghni/laravel-essentials-kit

Kick off a Laravel API with the boilerplate already written: hardened defaults, a JSON response envelope, global exception rendering, packages, and quality gates, generated into your app as code you own.

Maintainers

Package info

github.com/GTS-MEGHNI/laravel-essentials-kit

Homepage

Issues

pkg:composer/gts-meghni/laravel-essentials-kit

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-08-14 15:02 UTC

This package is auto-updated.

Last update: 2026-08-14 17:27:08 UTC


README

Laravel Essentials Kit

Packagist PHP from Packagist Laravel versions GitHub Workflow Status (main) Total Downloads

Kick off a Laravel API with the boilerplate already written: hardened framework defaults, a consistent JSON response envelope, global exception rendering, the packages you always install, and the quality gates you always configure.

Everything this kit installs is written into your application as ordinary, editable code. Nothing runs from inside the package, and nothing is hidden behind configuration you cannot read.

Installation

Install the kit as a development dependency, since it does its work once and then steps aside:

composer require --dev gts-meghni/laravel-essentials-kit

Then run the installer:

php artisan essentials:install

The installer asks before every step, with everything preselected so you deselect what you do not want. Every question is asked first, in one sitting, and only then does anything happen. Composer runs last, so a slow or failing network never interrupts a prompt, and the cleanup step's git safety check sees your working tree as you left it rather than one the installer has already written to.

A non-interactive run does nothing unless you name the steps explicitly with flags.

What It Installs

Framework defaults

Selected defaults are generated into app/Providers/EssentialsServiceProvider.php, which is registered in bootstrap/providers.php. Only the groups you pick are written, so the file contains no dead code.

Feature What it generates
Database safety DB::prohibitDestructiveCommands() in production
Strict models Model::shouldBeStrict() outside production, unwrapped JSON resources
Immutable dates Date::use(CarbonImmutable::class)
Security defaults Forced HTTPS in production, strong password rules, no stray HTTP calls in tests
Slow query logging Per-query and cumulative query time warnings

API layer

Selecting the API feature generates four files and the provider method that wires them:

File Purpose
app/Support/ApiResponse.php success(), error(), paginated(), and noContent() returning a consistent envelope
app/Exceptions/ApiExceptionRenderer.php Converts every uncaught throwable into that envelope
app/Http/Middleware/ForceJsonResponse.php Forces JSON content negotiation so errors never render as HTML
app/Http/Middleware/RequestId.php Adds a request id to the log context and the response headers

The installer wires these into your bootstrap/app.php, which is where Laravel 11+ expects middleware and exception rendering to be configured:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->append(ForceJsonResponse::class);
    $middleware->append(RequestId::class);
})
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(static fn (): bool => true);
    $exceptions->render(new ApiExceptionRenderer);
})

Registering from a service provider instead would look tidier, but it breaks wherever the exception handler is decorated. Collision does exactly that in console and test contexts, which would silently disable the envelope in your feature tests. The patch is skipped if it is already present, and if your bootstrap file has been reshaped the installer says so and leaves it alone.

Responses look like this:

{
    "success": true,
    "message": "Request completed successfully.",
    "data": [],
    "meta": {
        "pagination": {
            "total": 42,
            "count": 15,
            "per_page": 15,
            "current_page": 1,
            "last_page": 3,
            "has_more_pages": true
        }
    }
}
{
    "success": false,
    "message": "The email field is required.",
    "errors": {
        "email": ["The email field is required."]
    }
}

Empty sections are omitted, and a meta.debug block carrying the exception class, file, and line is included outside production only.

To localize error messages per route group, fill in the messages() method in the generated renderer. It maps request patterns to translation keys, per exception category, and the first matching pattern wins.

Client and backoffice route groups

Splits your API surface in two, since most projects serve a client application and an administration panel from the same codebase:

// routes/api.php
Route::prefix('client')
    ->name('client.')
    ->group(base_path('routes/api/client.php'));

Route::prefix('backoffice')
    ->name('backoffice.')
    ->group(base_path('routes/api/backoffice.php'));

Routes then live at /api/client/… and /api/backoffice/…, named client.* and backoffice.*. The two files are generated empty with a header comment, ready for your routes.

Health endpoint

Appends a GET /api/health route to your own routes/api.php, reporting application and database status through the same envelope:

{
    "success": true,
    "message": "Service is healthy.",
    "data": {
        "status": "ok",
        "database": "ok"
    }
}

It requires the API layer feature for ApiResponse. If routes/api.php does not exist yet the installer creates it and adds api: to withRouting() in bootstrap/app.php, so php artisan install:api is not needed first.

The framework's own health: '/up' route is removed by the API-only cleanup step, but only once this route is in place, since a removed probe with nothing behind it is worse than a duplicate one. Keep '/up' if you rely on it answering during php artisan down, which /api/health does not.

One time passwords

An optional feature generating a code generator, a predictable fake, and a cache backed store:

app/Support/Otp/OtpGenerator.php
app/Support/Otp/RandomOtpGenerator.php
app/Support/Otp/FakeOtpGenerator.php
app/Support/Otp/OtpStore.php
config/otp.php

OtpGenerator produces digits and nothing else. The lifecycle of storing, expiring, comparing, and counting attempts belongs to OtpStore, so a test can swap in predictable digits without also replacing the behavior it is trying to exercise:

$code = app(OtpStore::class)->issue($user->phone);   // send this yourself

app(OtpStore::class)->verify($user->phone, $request->string('code')->toString());

Both are bound in the generated provider's register() method. verify() consumes the code on success, so a replayed request cannot reuse it, and spends one attempt on failure until the code is thrown away entirely.

Only a hash of the code is cached, and the identifier is hashed into the cache key, so neither a cache dump nor a key listing hands over pending verifications. A six digit code has a million possibilities, which is why OTP_MAX_ATTEMPTS rather than the length is what keeps it safe.

Delivery is deliberately not included. The store returns the code and your application decides how to send it, which keeps the kit free of an SMS dependency.

Defaults live in config/otp.php, so set one of these only to override it:

Key Default Effect
OTP_LENGTH 6 Digits per code
OTP_TTL 300 Seconds a code stays valid
OTP_MAX_ATTEMPTS 5 Wrong guesses a code survives
OTP_STORE default store Cache store holding codes

FakeOtpGenerator returns 111111, which makes verification flows testable and locally usable without an SMS gateway. It is chosen by environment, local and testing only, and there is deliberately no flag to enable it, because a flag reaching production through a copied environment file would turn every code into the same guessable digits. Staging gets real codes for the same reason: a flow should be rehearsed there as it will behave once it ships.

Out of range configuration is refused at construction instead of degrading: an unset OTP_LENGTH reads as zero, and an empty code would verify against anything.

Algerian phone numbers

An optional feature generating app/Support/PhoneNumber.php, which normalizes +213…, 213…, and 0… numbers to canonical E.164, and app/Rules/AlgerianPhoneNumber.php for validation. This one is Algeria specific by design.

Algerian provinces and communes

An optional feature shipping the full administrative division as seed data, since almost every Algerian project needs it and every project rebuilds it by hand:

database/data/algeria.json
database/migrations/0001_01_01_000010_create_provinces_table.php
database/migrations/0001_01_01_000011_create_communes_table.php
database/seeders/AlgeriaGeoSeeder.php
app/Models/Province.php
app/Models/Commune.php

69 provinces and 1559 communes, each name translated into Arabic, French, and English, stored in a name JSON column and cast to an array:

php artisan migrate
php artisan db:seed --class=AlgeriaGeoSeeder

Province::where('code', '16')->first()->translatedName();  // "الجزائر" under the ar locale
$commune->translatedName('fr');
$province->communes;

translatedName() falls back to French rather than to the application fallback locale, because every row is guaranteed to carry French. Re-running the seeder updates rather than duplicates, so it is safe in a deploy script.

Provinces are keyed by their zero padded wilaya code. Codes 01 to 58 are the wilayas proper; 59 to 69 are the delegated administrative districts created in 2019, whose communes are also listed under their parent wilaya. Communes keep the id carried by the dataset instead of an autoincrement, so rows referencing them survive a fresh install.

Packages

The installer offers to install and set up the packages an API project usually needs:

laravel/sanctum, laravel/telescope, spatie/laravel-permission, spatie/laravel-medialibrary, spatie/laravel-activitylog, spatie/laravel-query-builder, maatwebsite/excel, darkaonline/l5-swagger

Two more are asked as questions rather than listed, because they depend on the project rather than on preference: gts-meghni/laravel-satim for SATIM payments, and gts-meghni/laravel-captcha for captcha protection.

Each selected package is required through Composer, then its own publish or install command runs in a fresh process. If Composer fails, the setup commands are skipped and the failure is reported.

Packages that expect something on your User model get it wired automatically, since a published migration without the matching trait leaves the package inert:

Package Added to app/Models/User.php
laravel/sanctum HasApiTokens
spatie/laravel-permission HasRoles
spatie/laravel-medialibrary InteractsWithMedia, implements HasMedia
spatie/laravel-activitylog CausesActivity

Each is added once and skipped if already present, so re-running is safe.

Telescope is installed as a development dependency, and telescope:install registers its published provider in bootstrap/providers.php unconditionally. That provider extends a class the package ships, so a production deploy running composer install --no-dev fatals on every request before the application boots. The installer moves the registration out of the manifest and into AppServiceProvider::register() instead:

if ($this->app->environment('local') && class_exists(\Laravel\Telescope\TelescopeServiceProvider::class)) {
    $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
    $this->app->register(TelescopeServiceProvider::class);
}

The class_exists check is deliberate belt and braces: an environment misconfigured as local in production would otherwise still reach for a class Composer never installed.

OpenAPI documentation

Selecting darkaonline/l5-swagger generates a documentation scaffold rather than leaving you with the package's single default definition, because a project serving a client application and an administration panel should not publish one document describing both:

config/l5-swagger.php
app/Http/Middleware/ProtectApiDocs.php
app/OpenApi/Client/OpenApiDefinition.php
app/OpenApi/Client/OpenApiConfig.php
app/OpenApi/Client/Endpoints/HealthEndpoint.php
app/OpenApi/Backoffice/OpenApiDefinition.php
app/OpenApi/Backoffice/OpenApiConfig.php
app/OpenApi/Backoffice/Endpoints/HealthEndpoint.php
app/OpenApi/Schemas/ErrorResponseSchema.php
app/OpenApi/Schemas/ValidationErrorResponseSchema.php
app/OpenApi/Schemas/PaginationSchema.php
app/OpenApi/Schemas/PaginationMetaSchema.php
app/OpenApi/Parameters/AcceptLanguageHeaderParameter.php

Two definitions are configured, each scanning its own directory plus the shared schemas and parameters, so backoffice operations never appear in the client document:

Definition UI JSON
Client /api/documentation/client /api/docs/client
Backoffice /api/documentation/backoffice /api/docs/backoffice

Operations are written as attributes on classes, one final readonly class per endpoint holding nothing but its attribute, which keeps the annotations out of your controllers and lets you delete a document without touching application code. The generated HealthEndpoint classes document the route the health feature appends, and are meant to be copied as the template for the rest. Security schemes are declared on each definition's OpenApiConfig class instead of in the config file, so securityDefinitions is left empty.

The schemas match the envelope the API layer generates, so a documented error or paginated response stays in step with what ApiResponse actually returns. Reference them rather than restating the shape per endpoint:

new OA\Response(
    response: 422,
    description: 'The request was invalid.',
    content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse'),
),

ProtectApiDocs guards the UI, the JSON documents, the assets, and the OAuth2 callback, since documentation exposes your entire API surface and l5-swagger publishes it unauthenticated by default:

Key Default Effect
L5_SWAGGER_ENABLED true Set to false and every documentation route returns 404
L5_SWAGGER_USERNAME empty HTTP basic user demanded before the docs are served
L5_SWAGGER_PASSWORD empty HTTP basic password
L5_SWAGGER_GENERATE_ALWAYS true Rebuild the documents per request

Leaving the credentials empty leaves the docs open, which is why the installer appends all four keys to .env and .env.example: an empty value you can see is likelier to get filled in than a key you never knew existed. Set both in production. Credentials are compared with hash_equals, so a wrong guess does not leak their length in timing.

Set L5_SWAGGER_GENERATE_ALWAYS=false in production and generate during deployment instead, since regenerating on every request parses your whole annotation tree per request:

php artisan l5-swagger:generate --all

Timezone

Optionally sets the application timezone to Africa/Algiers. The Laravel skeleton hardcodes 'timezone' => 'UTC' in config/app.php, so setting APP_TIMEZONE alone does nothing. This step rewrites the entry to read the environment, and declares the key:

'timezone' => env('APP_TIMEZONE', 'Africa/Algiers'),

A deployment can still override it per environment, and a config that already reads env() is left alone.

Quality tooling

Optionally installs Pint, Larastan, Pest with the Laravel and type coverage plugins, and Laravel Boost. It writes pint.json and a phpstan.neon set to level: max, then merges these scripts into your composer.json without touching what is already there:

composer lint         # format
composer lint:check   # verify formatting
composer analyse      # Larastan at level max
composer test:types   # 100% type coverage
composer test:unit    # the suite
composer test         # all of the above, in order

API-only cleanup

Optionally removes what a JSON API does not need: Blade views, frontend assets and build configuration, browser routes, and the view and session configuration files.

This step deletes files permanently, and like every other step it is preselected. It lists every path with a reason before asking, and refuses to run at all when git status is not clean or the directory is not a git repository, so there is always a way back. When routes/web.php is removed, the web: argument is also stripped from withRouting() in bootstrap/app.php, and health: '/up' is stripped once /api/health has replaced it.

Non-Interactive Use

Every step can be named explicitly, which is what CI and scripted setup should do:

php artisan essentials:install \
    --features=database --features=models --features=dates --features=api \
    --packages=sanctum --packages=permission \
    --timezone --tooling \
    --no-interaction
Option Effect
--features=* Features to generate
--packages=* Packages to install
--timezone Set the application timezone to Africa/Algiers
--tooling Install and configure the quality tooling
--cleanup Remove files a JSON API does not need
--all Generate every feature
--force Overwrite existing files, and allow cleanup on a dirty tree

Upgrading

There is no upgrade path, by design. The kit generates code you own, and it does not come back to change it. Upgrading the package only changes what a future essentials:install would write.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Thank you for considering contributing to Laravel Essentials Kit! Please review our contributing guide to get started.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

Laravel Essentials Kit is open-sourced software licensed under the MIT license.