Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Heimdal Laravel Package

optimus/heimdal

Laravel package for multi-database applications: run migrations, seeders, and artisan commands across multiple tenant or secondary database connections, keeping schemas in sync without duplicating configuration or wiring per-connection logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Heimdal is a small, focused package: it replaces Laravel's default HTML/JSON exception rendering with a consistent JSON error envelope for APIs. The core idea is an exception-to-response map in config, plus optional formatters when you need custom payloads.

  1. Install and publish config:
composer require optimus/heimdal
php artisan vendor:publish --provider="Optimus\Heimdal\Providers\HeimdalServiceProvider"

If you're on Laravel 5.5+ the provider is auto-discovered; otherwise register it in config/app.php. After publishing, open config/heimdal.php first — this is where the package's behavior lives.

  1. Mix the package trait into your application exception handler:
namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as BaseHandler;
use Optimus\Heimdal\ExceptionHandler as HeimdalHandler;

class Handler extends BaseHandler
{
    use HeimdalHandler;
}

The trait takes over render() so exceptions become JSON. Your existing report() behavior is left alone, which is important: logging/Sentry still works while the response shape changes.

  1. Run one request through it. The quickest first use case is a findOrFail call:
Route::get('/api/orders/{order}', function ($id) {
    return Order::findOrFail($id);
});

With Accept: application/json, you should get a 404 JSON body rather than an HTML error page:

{
  "errors": [
    {
      "status": 404,
      "code": "RESOURCE_NOT_FOUND",
      "title": "Resource not found",
      "detail": "..."
    }
  ]
}

That's the day-one win: every API consumer gets the same error contract for validation, auth, authorization, missing models, and uncaught exceptions.

Implementation Patterns

Treat the config map as your API error contract. Map every exception you intentionally throw, and map framework exceptions explicitly. A typical starting map:

'handlers' => [
    Exception::class => [
        'status_code' => 500,
        'error_code' => 'INTERNAL_SERVER_ERROR',
        'error_message' => 'An internal server error occurred',
    ],
    Illuminate\Database\Eloquent\ModelNotFoundException::class => [
        'status_code' => 404,
        'error_code' => 'RESOURCE_NOT_FOUND',
        'error_message' => 'Resource not found',
    ],
    Illuminate\Auth\AuthenticationException::class => [
        'status_code' => 401,
        'error_code' => 'UNAUTHENTICATED',
        'error_message' => 'Unauthenticated',
    ],
    Illuminate\Auth\Access\AuthorizationException::class => [
        'status_code' => 403,
        'error_code' => 'FORBIDDEN',
        'error_message' => 'This action is unauthorized',
    ],
    Illuminate\Validation\ValidationException::class => [
        'status_code' => 422,
        'error_code' => 'VALIDATION_ERROR',
        'error_message' => 'The given data was invalid',
    ],
],

Keep error_code values stable and machine-readable; clients should branch on codes, never on messages. Treat adding a new code as an API change.

Use domain exceptions instead of controller return response()->json(...) error paths. Define PaymentFailedException, InsufficientStockException, etc., map each to a status and code, and throw them from services. This keeps controllers thin and makes the error surface auditable in one file.

class PaymentFailedException extends Exception
{
    protected $message = 'The payment was declined.';
}

// config/heimdal.php
App\Exceptions\PaymentFailedException::class => [
    'status_code' => 402,
    'error_code' => 'PAYMENT_FAILED',
    'error_message' => 'Payment failed',
],

Custom formatters are the extension point for richer payloads. When the default {status, code, title, detail} shape isn't enough — field-level validation details, a meta object with a gateway reference, a source.pointer for JSON:API — create a formatter extending the package's base formatter and point the handler entry at it. Look in src/Formatters for the exact contract and copy the validation formatter as a starting point. Keep formatters dumb: extract data from the exception, don't query the database or call services.

Let validation and auth exceptions bubble. Don't catch ValidationException in controllers to reshape it; map it once. If you use Form Requests, $request->validate() already throws it. The only thing to verify is that your clients send Accept: application/json (or that you have middleware forcing JSON for /api/*), since content negotiation still drives whether the request is treated as an API call.

Test the contract, not the package. Feature tests should assert status and code:

$this->getJson('/api/orders/999')
    ->assertStatus(404)
    ->assertJsonPath('errors.0.code', 'RESOURCE_NOT_FOUND');

$this->postJson('/api/orders', [])
    ->assertStatus(422)
    ->assertJsonPath('errors.0.code', 'VALIDATION_ERROR');

This gives you a regression suite you can keep if you later swap Heimdal for a custom handler.

Reporting stays where it is. Keep report() (or Laravel's default reporting) doing logging and Sentry integration. Heimdal only changes rendering. If you need to suppress reporting for expected domain exceptions, do it in report() with $this->shouldntReport() semantics, not in Heimdal config.

Consider the package boundary. Heimdal is a thin convenience layer (122 stars, zero listed dependents, last release January 2025). That's not a reason to avoid it, but keep your error codes and JSON envelope defined in your own docs/tests. If the package becomes unmaintained, replacing the trait with ~50 lines in Handler::render() should be a mechanical change, not a rewrite.

Gotchas and Tips

  • Content negotiation bites first. Without Accept: application/json, requests may still render HTML. Add middleware that forces JSON for API routes, or ensure clients always send the header. This is the most common "Heimdal isn't working" cause.
  • Mapper specificity matters. Match order and class hierarchy determine which handler wins. Put concrete exceptions (ModelNotFoundException) before broad ones (Exception, RuntimeException). A generic Exception entry that also matches a specific case will silently shadow it.
  • APP_DEBUG leaks internals. Debug mode typically adds trace/previous-exception detail. Confirm production has APP_DEBUG=false and that unexpected 500 responses return only the generic message. Never put $e->getMessage() into the default 500 detail.
  • Config caching. After editing config/heimdal.php, run php artisan config:cache (or clear it in dev). Avoid closures in config values — they break config caching.
  • 404 ambiguity. NotFoundHttpException (no route) and ModelNotFoundException (no record) are different exceptions. Map both if clients need to distinguish routing errors from missing resources.
  • 429s and throttling. If you use rate limiting, map ThrottleRequestsException (or the framework equivalent for your version) to a 429 with a RATE_LIMITED code, otherwise it falls into the generic 500 bucket.
  • Middleware and CORS. Exceptions thrown before your CORS middleware runs won't get CORS headers, so browser clients see opaque failures. Keep CORS high in the stack and test 401/422 responses from a browser origin.
  • Don't double-handle. Once Heimdal owns render(), remove ad-hoc try/catch blocks that return error JSON from controllers; mixed envelopes are worse than either style alone. Also check that your Handler doesn't define its own render() that conflicts with the trait — trait methods lose to methods defined directly on the class.
  • Validation payload shape. Confirm what the validation formatter emits (per-field errors vs a single detail string) and document it. If it doesn't match your frontend's expectations, that's the first place to write a custom formatter.
  • Version pinning. Check the package's composer.json against your Laravel/PHP version before adopting, and pin a minor version. With low dependents and a recent release, you're effectively the QA.
  • Queue and console contexts. Heimdal is an HTTP response formatter. Don't rely on it for job failures or Artisan output; handle those separately.
  • Localization. If your API returns user-facing messages, decide whether translations belong in config error_message values, in formatters using trans(), or in the client. Mixing all three produces inconsistent responses.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels