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

Rest Util Laravel Package

tbbc/rest-util

tbbc/rest-util is a lightweight Laravel utility package for building RESTful APIs, offering helpers for consistent JSON responses, error handling, and request/response formatting to reduce controller boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

tbbc/rest-util is a small Symfony bundle that gives you three things FOSRestBundle users often rebuild by hand:

  1. A View value object — controllers return new View($data, $status, $format) instead of constructing a Response, and a kernel.view listener serializes it for you.
  2. A request body decoder — a kernel.request listener parses JSON/XML bodies based on Content-Type and exposes the decoded array as a request attribute.
  3. An error pipeline — exceptions are converted into a normalized Error payload rendered in the same format as the request, with configurable exception→HTTP status mappings.

Minimum steps:

composer require tbbc/rest-util
// app/AppKernel.php (Symfony 2/3 era)
new Tbbc\RestUtilBundle\TbbcRestUtilBundle(),
# config.yml
tbbc_rest_util:
    view:
        formats:
            json: application/json
            xml:  application/xml

First use case — make one endpoint return a View and confirm it serializes:

use Tbbc\RestUtil\View\View;

class PingController
{
    public function pingAction()
    {
        return new View(['pong' => true, 'ts' => time()], 200, 'json');
    }
}

Hit the route, then break it on purpose (return an invalid format, throw an exception) to see how the listener and error handler react. Where to look first:

  • Resources/config/ (services.xml/*.yml) — the definitive list of services, listener priorities, and the request attribute key used for the decoded body.
  • View/ViewListener.php — how format is resolved, how the serializer is invoked, how headers/status are applied.
  • Error/ (ErrorFactory, ErrorResolver, resolvers) — the exception→status map and payload shape.

This is an unmaintained Symfony 2.x/3.x-era bundle (16 stars, no recent activity). Treat the first hour as a compatibility audit against your Symfony version before planning anything on it.

Implementation Patterns

Controller-as-View-returner

Keep controllers thin: fetch data, return a View, let the listener serialize.

public function showAction($id)
{
    $book = $this->bookRepository->find($id);
    if (!$book) {
        throw new NotFoundHttpException(sprintf('Book %s not found', $id));
    }

    return new View($book, 200, 'json', [
        'X-Resource-Id' => $book->getId(),
        'Cache-Control' => 'private, max-age=60',
    ]);
}

This makes controllers unit-testable without HTTP concerns — assert on $view->getData(), getStatusCode(), getFormat(). It also centralizes serialization (JMS Serializer by default), so serialization groups and versioning live in one place instead of per-controller.

Content negotiation in a listener, not in every action

Resolve the response format once (Accept header / _format / extension) and stash it on the request, then have controllers default to it:

$format = $request->attributes->get('_format', 'json');
return new View($data, 200, $format);

Keep the negotiation logic in a small subscriber or a base controller — don't scatter format checks through actions.

Decoded request bodies

For POST/PUT/PATCH with Content-Type: application/json, the request listener decodes the body so you can validate it as PHP arrays rather than json_decode($request->getContent(), true) in every action. Read the decoded payload from the request attribute documented in services.xml, then hand it to a Symfony Form or a validator:

public function createAction(Request $request)
{
    $payload = $request->attributes->get(/* key from services.xml */);
    $form = $this->createForm(BookType::class, null, ['csrf_protection' => false]);
    $form->submit($payload);
    if (!$form->isValid()) {
        throw new BadRequestHttpException((string) $form->getErrors(true));
    }
    // ...
}

The pattern that pays off: one decode path, one validation path, one error path. Never mix $request->request->all() and the decoded attribute in the same codebase.

Structured errors via the error pipeline

Let domain/HTTP exceptions bubble to the error listener instead of hand-rolling JSON error responses. Map your exception codes to statuses in config, then throw plain exceptions from services:

tbbc_rest_util:
    error:
        resolver:
            exception:
                map:
                    4040: 404   # BookNotFound
                    4090: 409   # DuplicateIsbn

This keeps services framework-agnostic (they throw domain exceptions) while the bundle produces a consistent envelope:

{ "code": 4090, "message": "Duplicate ISBN", "details": { "isbn": "..." } }

For errors that aren't exceptions (validation aggregates, partial failures), build them explicitly with ErrorFactory so the shape never drifts from exception-rendered errors.

Custom decoders and resolvers as extension points

  • Implement the decoder interface and tag the service to support application/vnd.api+json, CSV, or form-encoded bodies.
  • Implement an error resolver and tag it so your DomainException maps to 422 with field-level details, without touching core files.

Both are the sanctioned extension points — prefer them over decorating listeners.

Integration order with FOSRestBundle / JMS

If you already run FOSRestBundle, you have overlapping kernel.view listeners. Pick one serializer path: either let rest-util's ViewListener handle View returns and let FOSRestBundle handle everything else, or drop one of them. Mixing produces "no listener supports the controller result" or double-serialization bugs that are painful to trace.

Gotchas and Tips

  • Maintenance risk is the headline gotcha. Bundle registration, service definition syntax, and JMS Serializer constraints are Symfony 2/3-era. On Symfony 4+ you'll likely need to register services manually, and on 6/7 the bundle may not boot at all. Budget a spike before committing.
  • Listener priorities matter. kernel.view and kernel.exception run in priority order. If Twig's exception listener, FOSRestBundle's, or a profiler listener runs before rest-util's, you'll get HTML error pages or null responses instead of your JSON envelope. Dump $event->getRequest()->attributes and the listener order (debug:event-dispatcher) when responses look wrong.
  • View must be the controller's return value. If you return a Response and expect the listener to post-process it, it won't. The listener only fires when nothing else supported the result, and only for the View type it's configured for.
  • Format strings must exist in config. A View with 'format' => 'yaml' when only json/xml are configured typically fails at serialization time — usually as a 500, not a 406. Validate the negotiated format against your allow-list early.
  • Decoded-body attribute key is version-specific. Don't guess it from the README; grep services.xml and the request listener. If your controllers and any custom listeners disagree on the key, you'll silently get null on writes — the classic symptom is "POST works in tests, null in production."
  • Errors are rendered through the same serializer. An exception thrown by the serializer (circular reference, uninitialized typed property, missing group) inside the error listener will re-enter exception handling. In dev this often surfaces as the framework's HTML exception page rather than your JSON error — check debug mode, profiler, and error logs rather than assuming the bundle is broken.
  • Don't leak internals. Exception messages are passed through by default; a Doctrine\DBAL exception message in a production 500 is a real vulnerability. Map generic exceptions to a safe message in the resolver and log the original.
  • HTTP status defaults are easy to get wrong. Omitting the status on a View or Error typically defaults to 200; a validation error returned as 200 will break every well-behaved API client. Make the status explicit and lint it in code review.
  • Headers set on View are merged, not replaced, onto the final Response — if you also set Content-Type manually you can end up with duplicates or a mismatch between the negotiated format and the header.
  • No service autowiring. Expect to wire ViewListener, decoders, and resolvers by hand (or via services.xml overrides) if you're on a modern Symfony skeleton; the bundle predates autoconfiguration.
  • Testing tip: because View and Error are plain value objects, unit-test controllers and error resolvers without booting the kernel. Reserve WebTestCase for the two integration seams: format negotiation and exception→response rendering.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource