dantepiazza / laravel-base
Base package for Laravel microservices: inter-service communication, JWT middleware, response helpers and notifications.
Requires
- php: ^8.3
- dantepiazza/laravel-api-response: ^1.0.0
- firebase/php-jwt: ^7.0.5
- knuckleswtf/scribe: ^4.0|^5.0
- laravel/framework: ^11.0|^12.0|^13.0
- sentry/sentry-laravel: ^4.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
Base package for Laravel backend projects. Provides a pre-configured bootstrap/app.php, standardized middleware, structured API responses, webhook handling, error tracking, and API documentation — all publishable via a single command.
Requirements
- PHP 8.3+
- Laravel 11, 12 or 13
dantepiazza/laravel-api-response ^1.0sentry/sentry-laravel ^4.0- A configured queue driver (Redis recommended) for webhook processing
Installation
composer require dantepiazza/laravel-base
Publish all stubs at once:
php artisan vendor:publish --tag=laravel-base
Or publish individually:
| Tag | Publishes |
|---|---|
laravel-base |
Everything below at once |
laravel-base-bootstrap |
bootstrap/app.php |
laravel-base-logging |
config/logging.php |
laravel-base-cors |
config/cors.php |
laravel-base-sentry |
config/sentry.php |
laravel-base-controller |
app/Http/Controllers/Controller.php |
laravel-base-env |
.env.example |
laravel-base-docker |
Dockerfile, docker-compose.yml, docker-compose.override.yml and all Docker config files |
Scribe config is merged from the package by default — no need to publish it unless you need to customize it:
php artisan ms:publish-scribe
# Overwrite without prompt:
php artisan ms:publish-scribe --force
Features
1. Base Controller — dantepiazza/laravel-api-response
This package requires dantepiazza/laravel-api-response and pre-wires it into the base Controller stub. After publishing, every controller in your project gets ApiResponse injected automatically:
// app/Http/Controllers/Controller.php (published stub) namespace App\Http\Controllers; use DantePiazza\LaravelApiResponse\ApiResponse; abstract class Controller { public function __construct(protected ApiResponse $api) {} }
Usage in any controller:
class InvoiceController extends Controller { public function index(): JsonResponse { return $this->api->success('Invoices retrieved.')->data(Invoice::paginate(20))->response(); } public function store(StoreInvoiceRequest $request): JsonResponse { return $this->api->created('Invoice created.')->data(Invoice::create($request->validated()))->response(); } public function show(Invoice $invoice): JsonResponse { return $this->api->success()->data($invoice)->response(); } public function destroy(Invoice $invoice): JsonResponse { $invoice->delete(); return $this->api->success('Invoice deleted.')->response(); } }
2. Middleware
ForceJsonResponse
Forces Accept: application/json on every request so Laravel always returns JSON errors. Register it globally in bootstrap/app.php:
$middleware->api(append: [ \DantePiazza\LaravelBase\Http\Middleware\ForceJsonResponse::class, ]);
GlobalHeaders
Attaches standard headers to every API response. Register it in bootstrap/app.php:
$middleware->api(append: [ \DantePiazza\LaravelBase\Http\Middleware\GlobalHeaders::class, ]);
Headers added to every response:
| Header | Description |
|---|---|
X-Maintenance |
Value of config('app.maintenance') |
X-Cache-Last-Update |
ISO 8601 timestamp of last cache invalidation — see CacheLastUpdate below |
X-Correlation-ID |
Trace ID — reused from the incoming request or generated as UUID v4 |
The X-Correlation-ID is also injected back into the request object so any controller or service can read it:
$correlationId = $request->header('X-Correlation-ID'); // or $correlationId = request()->header('X-Correlation-ID');
CacheLastUpdate — runtime-updatable cache invalidation timestamp
SPA clients cache lookup data (categories, settings, etc.) in localStorage and use X-Cache-Last-Update to decide whether to refetch it. Because that needs to change the moment backoffice data changes — not at deploy time — the value isn't config, it's stored in the application cache and updated at runtime:
use DantePiazza\LaravelBase\Support\CacheLastUpdate; // Read the current value (defaults to now() the first time it's read) CacheLastUpdate::get(); // Bump it — call this from an observer/listener whenever data that SPA // clients cache locally is created, updated or deleted CacheLastUpdate::touch();
class CategoryObserver { public function saved(Category $category): void { CacheLastUpdate::touch(); } }
GlobalHeaders calls CacheLastUpdate::get() automatically on every response — no extra wiring needed.
3. Webhook handling
The package ships a generic WebhookController that receives webhooks from any source, verifies the signature, and dispatches a Laravel event asynchronously.
Route setup
The controller expects a {source} route parameter that identifies the origin:
use DantePiazza\LaravelBase\Http\Controllers\WebhookController; Route::post('webhooks/{source}', [WebhookController::class, 'handle']);
This dispatches events named webhook.{source}.{event_type}, so each origin is isolated:
| URL called | Event dispatched |
|---|---|
POST /webhooks/convoy |
webhook.convoy.invoice.created |
POST /webhooks/stripe |
webhook.stripe.charge.succeeded |
POST /webhooks/github |
webhook.github.push |
Signature verification
By default the controller rejects all requests (verifySignature returns false). To enable a source, extend the controller and override the method:
namespace App\Http\Controllers; use Illuminate\Http\Request; use DantePiazza\LaravelBase\Http\Controllers\WebhookController as BaseWebhookController; class ConvoyWebhookController extends BaseWebhookController { protected function verifySignature(Request $request, string $source): bool { return hash_equals( hash_hmac('sha256', $request->getContent(), config('services.convoy.secret')), (string) $request->header('X-Convoy-Signature') ); } }
Then point the route to your controller:
Route::post('webhooks/convoy', [ConvoyWebhookController::class, 'handle']);
Listening to webhook events
use Illuminate\Support\Facades\Event; // Single event Event::listen('webhook.stripe.charge.succeeded', function (array $payload) { $chargeId = $payload['data']['id']; }); // Wildcard — all events from one source Event::listen('webhook.convoy.*', ConvoyWebhookListener::class); // Wildcard — every webhook regardless of source Event::listen('webhook.*', GeneralWebhookListener::class);
Payload passed to every listener:
[
'source' => 'stripe',
'event_type' => 'charge.succeeded',
'data' => [...], // $body['data'] if present, otherwise full body
'headers' => [...], // all request headers
'raw' => [...], // full decoded request body
]
4. Exception handling (bootstrap/app.php)
The published bootstrap/app.php includes a full exception handler wired to dantepiazza/laravel-api-response:
| Exception | HTTP status | Response |
|---|---|---|
ValidationException |
422 | Field-level errors via validationError() |
AuthenticationException |
401 | unauthorized() |
AccessDeniedHttpException |
403 | forbidden() |
NotFoundHttpException / ModelNotFoundException |
404 | notFound() |
ThrottleRequestsException |
429 | tooManyRequests() |
Throwable (catchall) |
500 | server_error with message |
Rate limiters included out of the box:
| Limiter | Limit | Keyed by |
|---|---|---|
api |
120 req/min | user ID or IP |
auth |
10 req/min | IP |
microservices |
600 req/min | IP |
5. GlitchTip / Sentry error tracking
Uses sentry/sentry-laravel, which is fully compatible with GlitchTip. Publish the config and set the DSN:
php artisan vendor:publish --tag=laravel-base-sentry
GLITCHTIP_DSN=https://key@glitchtip.yourdomain.com/1
The X-Correlation-ID on every request can be used to correlate GlitchTip events with specific API calls.
6. Logging
The published config/logging.php includes a pre-configured stack with daily file rotation and optional Slack alerts for critical errors:
LOG_STACK=daily,slack LOG_LEVEL=error LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... LOG_SLACK_LEVEL=critical
7. API Documentation (Scribe)
Scribe is configured out of the box — the package merges its own config/scribe.php automatically, no publishing needed.
To generate docs:
php artisan scribe:generate
To customize the config for a specific project:
php artisan ms:publish-scribe
# Edit config/scribe.php, then regenerate
8. Docker
A production-ready Docker setup is included. Publish it with:
php artisan vendor:publish --tag=laravel-base-docker
Publishes:
Dockerfile
docker-compose.yml
docker-compose.override.yml
docker/
├── mysql/my.cnf
├── nginx/default.conf
├── nginx/nginx.conf
├── php/php.ini
├── php/php-fpm.conf
└── supervisor/supervisord.conf
docker-compose.yml is the production compose file (uses a pre-built image via APP_IMAGE). docker-compose.override.yml is the local development override — it builds the image from source and mounts the project directory for live reload.
9. CI/CD — GitHub Actions deploy workflow
The package's own repository ships .github/workflows/deploy.yml: on every push to main or development it builds the app image, pushes it to ghcr.io, then SSHes into the target server to pull and restart it. This workflow is for the package's own reference app / VPS — copy it into a consuming project if you want the same pipeline there.
Configure it under the repo's Settings → Secrets and variables → Actions:
| Name | Kind | Description |
|---|---|---|
SERVER_SSH_KEY |
Secret | Private SSH key used to connect to the server. The only sensitive value. |
SERVER_HOST |
Variable | Server hostname or IP. |
SERVER_USER |
Variable | SSH user. |
SERVER_SSH_PORT |
Variable | SSH port. |
SERVER_PROD_PATH |
Variable | Absolute path on the server where main deploys (uses the sha-<short-sha> image tag). |
SERVER_DEV_PATH |
Variable | Absolute path on the server where development deploys (uses the latest image tag). |
None of these values are secret except the SSH key, so they're set as repository variables, not secrets — that keeps them visible/diffable in the Actions UI instead of hidden.
Package structure
dantepiazza/laravel-base/
├── composer.json
├── README.md
├── src/
│ ├── BaseServiceProvider.php
│ ├── Console/Commands/
│ │ └── PublishScribeConfig.php # php artisan ms:publish-scribe
│ ├── Http/
│ │ ├── Controllers/
│ │ │ └── WebhookController.php # Generic webhook receiver
│ │ └── Middleware/
│ │ ├── ForceJsonResponse.php
│ │ └── GlobalHeaders.php
│ └── Support/
│ └── CacheLastUpdate.php # Runtime-updatable X-Cache-Last-Update value
├── stubs/
│ ├── .env.example
│ ├── app/Http/Controllers/
│ │ └── Controller.php # Base controller with ApiResponse injected
│ ├── bootstrap/
│ │ └── app.php # Pre-configured with exception handling and rate limiters
│ ├── config/
│ │ ├── cors.php
│ │ ├── logging.php
│ │ ├── scribe.php
│ │ └── sentry.php
│ └── routes/
│ └── api.php
└── docker/
├── Dockerfile
├── docker-compose.yml
├── docker-compose.override.yml
├── mysql/my.cnf
├── nginx/default.conf
├── nginx/nginx.conf
├── php/php.ini
├── php/php-fpm.conf
└── supervisor/supervisord.conf