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

Api Resource Bundle Laravel Package

xing-com/api-resource-bundle

Xing Com API Resource Bundle is a Symfony bundle for building resource-oriented REST APIs. It helps expose Doctrine-backed resources through configurable endpoints, with support for routing, serialization, pagination, and consistent API responses, reducing boilerplate for CRUD-style services.

View on GitHub
Deep Wiki
Context7

Getting Started

xing-com/api-resource-bundle is a Symfony bundle, not a Laravel package. It ships resource objects (ApiResource subclasses), a ResourceSerializer/ResourceDeserializer pair built on top of symfony/serializer, and HAL-flavoured output (_links, _embedded). There is no ApiResourceServiceProvider, no config/api-resource.php to publish, and no facade. Treat it as a library you wire up yourself.

Install

composer require xing-com/api-resource-bundle \
    symfony/serializer symfony/property-access symfony/property-info

Check composer.json in the package for the PHP/Symfony constraints before committing — the 2025-09 release targets PHP 8.2+ and Symfony 6/7, and the transitive Symfony components will coexist with Laravel's own symfony/* pins. Run composer why symfony/serializer afterwards to confirm you didn't downgrade anything Laravel needs.

Where to look first

  1. vendor/xing-com/api-resource-bundle/src/ — the source is small; read the base resource class and the serializer/deserializer first.
  2. vendor/xing-com/api-resource-bundle/tests/ — with three stars and zero dependents, the test suite is the only reliable documentation of the attribute syntax and payload shape.
  3. The bundle's DI extension (DependencyInjection/) — it tells you exactly which services and constructor arguments you need to replicate in a Laravel service provider.

Wire it once in a provider

// app/Providers/ApiResourceServiceProvider.php
public function register(): void
{
    $this->app->singleton(ResourceSerializer::class, function ($app) {
        return new ResourceSerializer(
            new Serializer(
                [new ObjectNormalizer(null, null, null, new ReflectionExtractor())],
                [new JsonEncoder()]
            ),
            $app->make(ResourceFactory::class),
        );
    });

    $this->app->singleton(ResourceDeserializer::class, fn ($app) => new ResourceDeserializer(
        $app->make(Serializer::class),
        $app->make(ResourceMetadataFactory::class),
    ));
}

(Adjust namespaces to whatever your installed version exposes — verify against src/ rather than the README.)

First use case: replace a hand-rolled JsonResource on one read endpoint. Define BookResource, build it from an Eloquent model, serialize it, return response()->json($payload). Compare the output against what your client currently consumes before you touch a second endpoint.

Implementation Patterns

1. Resource classes as the contract

The core pattern is a plain PHP object with typed properties and getters. Links are declared with PHP attributes (older docs use annotations — attributes only in current releases).

use Xing\ApiResourceBundle\...\ApiResource;
use Xing\ApiResourceBundle\...\ResourceLink;
use Xing\ApiResourceBundle\...\ResourceCollection;

class BookResource extends ApiResource
{
    public function __construct(
        public string $id,
        public string $title,
        public AuthorResource $author,
        public ResourceCollection $reviews,
    ) {}

    #[ResourceLink(relation: 'self', href: '/books/{id}')]
    public function getSelfLink(): ResourceLink { /* ... */ }
}

Keep resources dumb: no queries, no Auth::user(), no now(). They are DTOs; the moment they touch the container you lose the ability to unit test serialize() output in isolation.

2. A factory, not a toResource() method on models

Eloquent models stay clean; mapping lives in one place:

class BookResourceFactory
{
    public function fromModel(Book $book): BookResource
    {
        return new BookResource(
            id: (string) $book->id,
            title: $book->title,
            author: $this->authorFactory->fromModel($book->author),
            reviews: new ResourceCollection(
                $book->reviews->map(fn ($r) => $this->reviewFactory->fromModel($r))->all()
            ),
        );
    }
}

This is also your natural seam for lazy/eager loading: the factory is where you assert relations are loaded, and where a missing relation fails loudly instead of producing a half-empty payload.

3. Controllers stay thin

public function show(Book $book, BookResourceFactory $factory, ResourceSerializer $serializer)
{
    $payload = $serializer->serialize($factory->fromModel($book));

    return response()->json($payload)
        ->header('Content-Type', 'application/hal+json');
}

Add a small HalResponse helper or a respondWithResource() trait so the media type and status code are set in one place — clients will depend on application/hal+json, and getting it wrong on one endpoint is the classic bug.

4. Collections and pagination

Build one ResourceCollection per page and attach pagination links at the collection level rather than per item:

$collection = new ResourceCollection($resources);
// attach first/prev/next/last + total, mirroring Laravel's paginator

Map Laravel's LengthAwarePaginator onto the collection in a single helper. Keep the query-string contract (page, per_page) identical to the rest of your API so HAL links don't invent a second pagination dialect.

5. Deserialization for writes

try {
    $dto = $deserializer->deserialize($request->json()->all(), CreateBookResource::class);
} catch (/* deserialization/mapping exception */ $e) {
    throw new HttpResponseException(
        response()->json(['error' => 'invalid_payload', 'detail' => $e->getMessage()], 422)
    );
}

Wire this into a FormRequest or a middleware rather than repeating the try/catch. Prefer this only where the input genuinely mirrors a resource (nested writes, PUT-with-representation). For flat create/update payloads, $request->validate() plus a constructor is less machinery for the same result.

6. Embedding strategy

Default to links, opt in to _embedded per endpoint (e.g. ?embed=author). The serializer will happily recurse through Book → Author → Books → Author; decide the depth explicitly in the factory rather than letting the object graph decide for you.

Gotchas and Tips

  • It assumes Symfony DI. Autowiring, tagged services, cache warmup and compiled metadata have no Laravel equivalent. Anything the bundle's extension registers in services.yaml you must bind by hand, and compiler passes have no counterpart at all. Budget an afternoon for wiring, not ten minutes.
  • Two serializers, two attribute systems. Symfony's ObjectNormalizer (property info, #[Groups], #[SerializedName]) is unrelated to Laravel's JsonResource/#[Hidden]. Pick one per endpoint; nesting an ApiResource inside a JsonResource produces double-wrapped payloads that are painful to diagnose.
  • Annotations vs attributes. Most blog posts and old README snippets use @ResourceLink annotations. Current releases expect PHP 8 attributes and there is no annotation reader installed by default — a silently ignored attribute means a link simply disappears from _links, with no error.
  • Circular embedding is the #1 runtime failure. Symptom: 500 or a memory blowup on one endpoint while neighbours are fine. Cap depth in the factory and add a feature test that asserts the full JSON structure, not just a 200.
  • Deserialization is strict about shape. Constructor argument names, nullability and nested types must line up with the payload. If a nested resource type isn't registered with the metadata factory you get a generic mapping exception — log the class name it failed to resolve, not just the message.
  • Debug by dumping the array, not the JSON. dump($serializer->serialize($resource)) then json_encode yourself; it separates serialization bugs from response/encoding bugs.
  • Three stars, zero dependents, no Stack Overflow corpus. You are the maintainer of your integration. Pin the exact version in composer.json, and keep the factory layer thick enough that swapping to spatie/laravel-data, Fractal, or a hand-rolled JsonResource is a one-directory change.
  • Compare before you commit. If you don't specifically need HAL _links/_embedded, Laravel's built-in JsonResource or spatie/laravel-data gives you the same DTO discipline with first-class framework support, validation integration and a community to ask. This bundle earns its place only when a client contract already speaks HAL.
  • Extension points worth knowing: a custom ObjectNormalizer (for Carbon/Money/enums), a bespoke ResourceFactory for a shared resource hierarchy, and substituting your own metadata factory if you need runtime class discovery. All three are plain constructor arguments — no decoration or compiler passes required, which is the bundle's one genuine ergonomic advantage.
  • Cache the metadata you build. There is no Symfony container to compile it into in Laravel; if your metadata factory does reflection per request, wrap it in a singleton plus an in-memory array keyed by class name, and clear it in tests with RefreshDatabase-style resets if you memoize across requests in Octane.
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