Skip to content

Repository files navigation

gaitco/dartvel

license: MIT

Mirror notice: if you're reading this on gaitco/dartvel-laravel, you're looking at an automatically generated, read-only split of the laravel/ package from the gaitco/dartvel monorepo — published so Packagist can find a composer.json at the repo root. Please file issues and PRs upstream, at github.com/gaitco/dartvel, not here.

A contract-enforced API layer for Laravel apps that talk to the dartvel Flutter client: a response envelope, an error_code taxonomy on top of Laravel's own exceptions, pagination meta, locale negotiation, and a ready-made device-registration endpoint for push notifications.

Install

composer require gaitco/dartvel

Contract error responses

RendersContract::register() maps Laravel's built-in exceptions to the contract's error envelope ({"message": ..., "error_code": ..., "errors": ...}) for any request whose path matches config('dartvel.exceptions.paths') (default ['api/*']) and expects JSON.

This happens automatically as soon as the package is installed — the service provider hooks the app's real exception handler (callAfterResolving(ExceptionHandler::class, ...)), so api/* requests get contract-shaped JSON errors with zero configuration. You do not need to call anything in bootstrap/app.php for this to work.

Scope or disable it via config:

// config/dartvel.php
'exceptions' => [
    'enabled' => true,        // set false to disable the renderer entirely
    'paths' => ['api/*'],     // only requests matching these path patterns are rendered
],

This matters if your app has non-Dartvel JSON endpoints (a legacy API, a webhook receiver, an Inertia XHR route) that expect Laravel's own exception format — narrow paths to just the routes your Dartvel client talks to, or set enabled to false and call RendersContract::register() yourself from bootstrap/app.php with your own gating.

Call RendersContract::register($exceptions) yourself only if you want to add your own renderers that should run first:

// bootstrap/app.php
use Gaitco\Dartvel\Exceptions\RendersContract;

->withExceptions(function (Exceptions $exceptions) {
    RendersContract::register($exceptions);

    // your own renderers, if any — see precedence note below
})

Precedence: Laravel resolves render() callbacks first-registered, first-non-null-wins. Your app's own withExceptions() renderers are registered before the package's auto-wired ones (which run via callAfterResolving, after the app boots), so an app-defined renderer for the same exception type always wins over the package's default. You only need RendersContract::register() in bootstrap/app.php if you want the package's mappings to run before some other renderer you're adding for a different exception type.

Exception error_code HTTP status
Illuminate\Validation\ValidationException validation_failed 422
Illuminate\Auth\AuthenticationException unauthenticated 401
Illuminate\Auth\Access\AuthorizationException forbidden 403
Symfony\...\AccessDeniedHttpException forbidden 403
Illuminate\Database\Eloquent\ModelNotFoundException not_found 404
Symfony\...\NotFoundHttpException not_found 404
Symfony\...\TooManyRequestsHttpException rate_limited 429
any other Throwable server_error 500

The 500 case's message is the real exception message when app.debug is true, and the literal "Server error." otherwise.

Known limitation (v0.1): these six messages ("The given data was invalid.", "Unauthenticated.", "This action is unauthorized.", "Resource not found.", "Too many requests.", "Server error.") are English string literals — they are not translated, even for a request whose locale was set by dartvel.locale. Validation field messages ($e->errors(), e.g. "The email field is required.") still go through Laravel's own translator as usual; it is only these six top-level messages that are hardcoded.

Middleware

Two aliases are registered by the service provider:

  • dartvel.contract — stamps every response with the X-Contract-Version header (from config('dartvel.contract_version')), so the Dart client can detect a wire-incompatible backend.
  • dartvel.locale — sets app()->setLocale() from (in order) the X-localization header, the Accept-Language header — each checked against config('dartvel.locales') and used only if it names a listed locale — falling back unconditionally to config('app.locale') if neither header matches.

Append both to your api middleware group in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('api', ['dartvel.locale', 'dartvel.contract']);
})

Controllers: ApiResponse

use Gaitco\Dartvel\Http\ApiResponse;

// {"data": ..., "message": ...}
return ApiResponse::data($post, 'Created successfully.', 201);

// {"data": [...], "message": null, "meta": {"current_page": ..., ...}}
return ApiResponse::paginated(Post::query()->paginate());

// {"message": ..., "error_code": ..., "errors": {...}}
return ApiResponse::error('The given data was invalid.', 'validation_failed', 422, $errors);

ApiResponse::data() resolves an Illuminate\Http\Resources\Json\JsonResource passed as $data into the data key (not merged as a sibling). ApiResponse::paginated() accepts either a LengthAwarePaginator or an AnonymousResourceCollection (e.g. PostResource::collection($paginator)).

Devices module

Publishing is not required — the migration and POST api/devices route load automatically. The migration creates a devices table (unique_id unique, fcm_token, type, locale, timezone, notifications_settings JSON, nullable polymorphic owner). The route is configured via config('dartvel.devices'):

// config/dartvel.php
'devices' => [
    'route_prefix' => 'api/devices',
    'middleware' => ['api', 'dartvel.contract'],
],

route_prefix is registered bare — it does not inherit an /api prefix from Laravel's routing (the 'middleware' => [...] entry above applies middleware, not a URL prefix). The default 'api/devices' matches the Dart quickstart's baseUrl (https://api.example.com/api) plus the Dart client's default DeviceRegistrar path: '/devices' — see Dart README's Quickstart. If your baseUrl does not already end in /api, adjust route_prefix (and/or the Dart side's path) so the two agree on a URL, e.g.:

'devices' => [
    'route_prefix' => 'devices',
    'middleware' => ['api', 'dartvel.contract'],
],

The default middleware also includes dartvel.contract, so the package's own endpoint stamps X-Contract-Version even before you wire that middleware into your app's own api group (see Middleware below).

If the request is authenticated, DeviceController attaches the current user as the device's polymorphic owner. Give your User model the inverse relation with HasDevices:

use Gaitco\Dartvel\Devices\HasDevices;

class User extends Authenticatable
{
    use HasDevices; // adds $user->devices(): MorphMany
}

Auth stubs

Requires laravel/sanctum — install and configure it first. Then publish the auth scaffold:

php artisan vendor:publish --tag=dartvel-auth

This writes three files you own and can edit freely:

  • app/Http/Controllers/Auth/AuthController.phplogin (issues a Sanctum token via createToken()), me, logout
  • app/Http/Requests/Auth/LoginRequest.php
  • routes/dartvel-auth.phpPOST auth/login, and auth:sanctum-gated GET auth/me / POST auth/logout

Remember to require __DIR__.'/../routes/dartvel-auth.php'; from your routes/api.php (or register it in bootstrap/app.php) — publishing does not wire the route file in for you. OTP endpoints (otp/request, otp/verify) are not generated — the stub has a TODO(app) marker where to add them if your app uses OTP auth.

Config

php artisan vendor:publish --tag=dartvel-config
// config/dartvel.php
return [
    'contract_version' => '1',   // stamped as X-Contract-Version by dartvel.contract
    'locales' => ['en'],         // locales dartvel.locale will accept
    'exceptions' => [
        'enabled' => true,       // set false to disable RendersContract entirely
        'paths' => ['api/*'],    // only these path patterns get contract-shaped errors
    ],
    'devices' => [
        'route_prefix' => 'api/devices',
        'middleware' => ['api', 'dartvel.contract'],
    ],
];

The contract

How dartvel fits together

Envelope key Present on Meaning
data every success response the payload — object, array, or null
message every response human-readable message, or null
meta paginated success responses current_page, last_page, per_page, total, has_more
error_code every error response machine-readable taxonomy — see table above
errors validation_failed only {"field": ["message", ...]}

X-Contract-Version's major segment (config('dartvel.contract_version'), default "1") must match the Dart client's compiled-in major version — a mismatch makes every client call throw ContractViolationException instead of misparsing an incompatible wire format. See the golden fixtures in contract/ at the repo root — both packages' test suites read the same files, so drift between them fails CI. Contract-versioning policy: root README.

About

READ-ONLY subtree split of the laravel/ package from gaitco/dartvel. Do not open PRs here — contribute at github.com/gaitco/dartvel.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages