Skip to content

Laresponse

Laravel trait untuk standardized JSON responses dan DTO untuk parsing query params dari frontend

Versi: 0.1.5 Changelog

PHPLaravel


TL;DR

Laresponse menyediakan dua hal: JsonResponseTrait untuk standardize format JSON response dari backend, dan ContainerQuery DTO untuk parsing query params yang dikirim oleh frontend (khususnya dari utils-data-container). Keduanya dirancang bekerja bersama sehingga backend dan frontend berbicara dalam kontrak yang sama.

Namespace:

php
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Bpmlib\Laresponse\DTO\ContainerQuery;
use Bpmlib\Laresponse\DTO\BaseContainerFilter; // extend untuk typed filter per endpoint

Response Methods (Quick Reference):

  • returnAdaptive() — ⭐ DIREKOMENDASIKAN untuk list/collection (auto-detect pagination)
  • returnJson() — ⭐ DIREKOMENDASIKAN untuk single data atau error responses
  • returnPaginateJson() — Explicit untuk LengthAware pagination
  • returnCursorPaginateJson() — Explicit untuk Cursor pagination
  • returnSimplePaginateJson() — Explicit untuk Simple pagination

Installation & Setup

Requirements

PHP Version

  • Minimum: PHP 8.1
  • Recommended: PHP 8.3+

Composer Dependencies

json
{
  "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
  "illuminate/http": "^10.0|^11.0|^12.0|^13.0",
  "illuminate/pagination": "^10.0|^11.0|^12.0|^13.0",
  "illuminate/database": "^10.0|^11.0|^12.0|^13.0"
}

Framework Requirements

  • Laravel: 10.0+, 11.0+, 12.0+, atau 13.0+

Composer Install

bash
composer require bpmlib/laravel-laresponse

Auto-Discovery

Tidak ada service provider atau config file. Trait dan class siap digunakan setelah installation.


Quick Start

Response JSON Standar

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;

class ProductController extends Controller
{
    use JsonResponseTrait;

    public function index()
    {
        return $this->returnAdaptive(Product::paginate(15));
    }

    public function show($id)
    {
        $product = Product::find($id);

        if (!$product) {
            return $this->returnJson([], 404);
        }

        return $this->returnJson($product);
    }
}

ContainerQuery + Response

Parsing query params dari utils-data-container lalu return response yang sesuai:

php
use Bpmlib\Laresponse\DTO\ContainerQuery;

public function index(Request $request)
{
    $q = new ContainerQuery($request);

    $products = Product::query()
        ->when($q->hasSort(), fn($b) => $b->orderBy($q->sortBy, $q->sortDir))
        ->when($q->hasSearch(), fn($b) => $b->where('name', 'like', "%{$q->search}%"))
        ->paginate($q->effectivePerPage());

    return $this->returnAdaptive($products);
}

Core Concepts

Standardized Response Structure

Semua response mengikuti struktur yang sama sehingga frontend selalu bisa expect field yang konsisten.

Base structure:

php
[
    'message' => string,   // Human-readable, otomatis dari HTTP code jika kosong
    'content' => mixed,    // Data utama
    'success' => bool,     // true jika HTTP 2xx
]

Dengan pagination:

php
[
    // + base fields
    'max_page'        => ?int,
    'current_page'    => ?int,
    'per_page'        => ?int,
    'total'           => ?int,
    'has_more'        => ?bool,
    'next_cursor'     => ?string,
    'previous_cursor' => ?string,
]

Field pagination yang tidak relevan diisi null — semua mode mengembalikan key yang sama. Ini memudahkan frontend mendeteksi mode paginasi dari response shape.

Default messages per HTTP code:

CodeMessage
200Sukses
201Sukses dibuat
400Pastikan format request yang Anda kirimkan sesuai
401Anda belum login
403Anda tidak mempunyai akses untuk ini
404Data yang anda cari tidak ada
422Form yang anda kirimkan ada yang tidak valid
500Terjadi kesalahan di server
503Server sedang sibuk

Code di luar tabel menggunakan fallback 'Hai :D'.


ContainerQuery & URL Params

ContainerQuery mem-parse query params yang dihasilkan oleh getterUrlStringAttribute() / getterObjectAttribute() dari utils-data-container. Kontrak params-nya:

perPage=10
page=2
cursor=abc123
useCursor=1
sortBy=name
sortDir=asc
filter[status]=active
filter[tags][]=php
filter[tags][]=js
q=keyword              ← nama param search bisa dikustomisasi (default: 'q')

Filter diparse Laravel secara otomatis menjadi nested array, lalu dibersihkan dari nilai kosong (null, string kosong, array kosong) — sama persis dengan deepCleanFilter di sisi JS.

paginationMode() mengembalikan:

  • 'cursor' — jika cursor atau useCursor=1 ada
  • 'numeric' — jika page ada (berlaku untuk paginate() maupun simplePaginate(), backend yang memilih)
  • 'none' — tidak ada params paginasi

Semua nama param di atas bisa dikustomisasi via constructor jika nama param dari frontend berbeda dari kontrak utils-data-container. Lihat selengkapnya


Append Mechanism

Parameter $appends memungkinkan merge data tambahan ke response tanpa bisa menimpa key bawaan.

php
return $this->returnAdaptive($products, appends: [
    'meta' => ['api_version' => '2.0'],
    'message' => 'Ini akan diabaikan', // key protected, tidak bisa di-override
]);

Key yang dilindungi: message, content, success, validation, dan semua key pagination.


Data Mapping Pattern

Mapper dipanggil per item — untuk transformasi presentation layer sebelum data dikirim ke frontend.

php
return $this->returnAdaptive($users, mapper: fn($u) => [
    'id'     => $u->id,
    'name'   => $u->name,
    'avatar' => $u->profile?->avatar_url ?? '/default.png',
]);

Berlaku untuk array, Collection, dan semua tipe Paginator.


API Reference

JsonResponseTrait

Namespace: Bpmlib\Laresponse\Traits\JsonResponseTrait

Contains:


returnAdaptive()

Auto-detect tipe data dan return response yang sesuai. Direkomendasikan untuk semua list/collection responses.

Signature:

php
protected function returnAdaptive(
    Collection|LengthAwarePaginator|CursorPaginator|Paginator|array $content,
    string $message = '',
    int $code = 200,
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentCollection|LengthAwarePaginator|CursorPaginator|Paginator|array-Data yang akan di-return
$messagestring''Custom message; kosong = default per HTTP code
$codeint200HTTP status code
$mapper?callablenullTransform callback Lihat selengkapnya
$appendsarray[]Data tambahan Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options

Returns: JsonResponse


$mapper

Callback untuk transform setiap item sebelum dikirim ke response.

Signature:

php
callable(mixed $item): mixed

Contoh:

php
$this->returnAdaptive($users, mapper: fn($u) => [
    'id'   => $u->id,
    'name' => $u->name,
]);

Use Case: Transformasi presentation layer — hide sensitive fields, compute derived properties, format tanggal.


$appends

Array data tambahan yang di-merge ke response. Built-in keys tidak bisa di-override.

Contoh:

php
$this->returnAdaptive($products, appends: [
    'meta'    => ['version' => '2.0'],
    'filters' => $request->only(['category']),
]);

returnJson()

Base method untuk single data, errors, atau validation errors.

Signature:

php
protected function returnJson(
    mixed $content = [],
    int $code = 200,
    string $message = '',
    array|MessageBag $validationErrors = [],
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentmixed[]Data utama
$codeint200HTTP status code
$messagestring''Custom message
$validationErrorsarray|MessageBag[]Validation errors Lihat selengkapnya
$mapper?callablenullTransform callback — sama dengan returnAdaptive
$appendsarray[]Data tambahan — sama dengan returnAdaptive
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options

Returns: JsonResponse


$validationErrors

Validation errors dari Laravel Validator. Hanya muncul di response jika $code == 422.

Accepted formats:

php
// MessageBag (dari Validator)
$this->returnJson([], 422, validationErrors: $validator->errors());

// Array manual
$this->returnJson([], 422, validationErrors: [
    'email' => ['Email sudah digunakan'],
]);

returnPaginateJson()

Explicit method untuk LengthAwarePaginator (->paginate()).

Signature:

php
protected function returnPaginateJson(
    LengthAwarePaginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentLengthAwarePaginator-Data hasil ->paginate()
$codeint200HTTP status code
$messagestring'Success'Custom message
$mapper?callablenullSama dengan returnAdaptive
$appendsarray[]Sama dengan returnAdaptive
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options

Returns: JsonResponsemax_page, current_page, total, has_more terisi; cursor fields null.


returnCursorPaginateJson()

Explicit method untuk CursorPaginator (->cursorPaginate()).

Signature:

php
protected function returnCursorPaginateJson(
    CursorPaginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentCursorPaginator-Data hasil ->cursorPaginate()
$codeint200HTTP status code
$messagestring'Success'Custom message
$mapper?callablenullSama dengan returnAdaptive
$appendsarray[]Sama dengan returnAdaptive
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options

Returns: JsonResponsenext_cursor, previous_cursor, has_more, per_page terisi; numeric fields null.


returnSimplePaginateJson()

Explicit method untuk Paginator (->simplePaginate()).

Signature:

php
protected function returnSimplePaginateJson(
    Paginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentPaginator-Data hasil ->simplePaginate()
$codeint200HTTP status code
$messagestring'Success'Custom message
$mapper?callablenullSama dengan returnAdaptive
$appendsarray[]Sama dengan returnAdaptive
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options

Returns: JsonResponsecurrent_page, has_more, per_page terisi; total, max_page, cursor fields null.


ContainerQuery

Namespace: Bpmlib\Laresponse\DTO\ContainerQuery

Class untuk mem-parsing query params standar yang dikirim oleh utils-data-container. Semua properties bersifat readonly — nilai ditetapkan di constructor dan tidak bisa diubah.

Contains:


Constructor

NOTE

Parameter kedua diperbarui di v0.1.5 menjadi string|array. Penggunaan string tetap bekerja seperti sebelumnya.

php
public function __construct(Request $request, string|array $param = 'q')

Parameters

NameTypeDefaultDescription
$requestRequest-Incoming HTTP request
$paramstring|array'q'String: set nama param search. Array: override nama param. Lihat selengkapnya

$param

String shorthand mengubah hanya nama param search. Array mengoverride key manapun — key yang tidak didefinisikan tetap menggunakan default kontrak utils-data-container. Dot-notation didukung untuk param nested.

Default keys:

FieldDefault param
perPageperPage
pagepage
cursorcursor
useCursoruseCursor
sortBysortBy
sortDirsortDir
filterfilter
searchq

Contoh:

php
new ContainerQuery($request, 'keyword');                                          // string — search saja
new ContainerQuery($request, ['sortBy' => 'orderBy', 'search' => 'keyword']);    // array — override beberapa key
new ContainerQuery($request, ['sortDir' => 'order.direction']);                   // dot-notation didukung

Properties

PropertyTypeDescription
$perPage?intJumlah item per halaman; null jika tidak dikirim
$page?intNomor halaman (numeric pagination); null jika tidak dikirim
$cursor?stringCursor value; null jika tidak dikirim
$useCursorbooltrue jika frontend set useCursor=1
$sortBy?stringKolom untuk sort; null jika tidak dikirim
$sortDirstringArah sort, selalu 'asc' atau 'desc'
$filterarrayCleaned filter dari filter[*] params
$search?stringSearch term; null jika tidak dikirim atau kosong
$searchParamstringNama param yang digunakan untuk search; derived dari $param

fromRequest()

Static factory — alternatif untuk constructor.

php
public static function fromRequest(Request $request, string|array $param = 'q'): static

paginationMode()

php
public function paginationMode(): string // 'numeric'|'cursor'|'none'

Mendeteksi mode paginasi dari params yang dikirim. 'numeric' berarti ada page — backend yang memilih antara paginate() (LengthAware) atau simplePaginate().


isNumericPagination()

php
public function isNumericPagination(): bool

true jika paginationMode() === 'numeric'.


isCursorPagination()

php
public function isCursorPagination(): bool

true jika paginationMode() === 'cursor'.


isNotPaginated()

php
public function isNotPaginated(): bool

true jika tidak ada params paginasi — gunakan ->get().


hasSort() / hasSearch() / hasCursor() / hasFilter()

php
public function hasSort(): bool    // $sortBy tidak null
public function hasSearch(): bool  // $search tidak null
public function hasCursor(): bool  // $cursor tidak null
public function hasFilter(): bool  // $filter tidak kosong

effectivePerPage()

php
public function effectivePerPage(int $default = 15): int

Mengembalikan $perPage jika ada, atau $default jika tidak.

NameTypeDefaultDescription
$defaultint15Fallback per-page jika tidak dikirim frontend

filterValue()

Mengakses nilai filter dengan dot-notation. Jika $strict = true, $default diabaikan — method akan throw jika key tidak ada.

php
public function filterValue(string $key, mixed $default = null, bool $strict = false): mixed

Parameters

NameTypeDefaultDescription
$keystring-Dot-notation path ke filter (e.g. 'user.role')
$defaultmixednullNilai jika key tidak ada (diabaikan saat $strict = true)
$strictboolfalseJika true, throw OutOfBoundsException saat key tidak ada

Throws: OutOfBoundsException jika key tidak ada dan $strict = true

Contoh:

php
$q->filterValue('status');               // null jika tidak ada
$q->filterValue('status', 'active');     // 'active' sebagai fallback
$q->filterValue('user.role');            // nested: filter[user][role]
$q->filterValue('status', strict: true); // throw jika tidak ada

resolveFilter()

NEW v0.1.5

Hydrate $this->filter ke typed filter DTO. IDE menginfer return type dari class yang dipassing via @template.

php
public function resolveFilter(string $filterClass): BaseContainerFilter

Parameters

NameTypeDefaultDescription
$filterClassclass-string<BaseContainerFilter>-Subclass BaseContainerFilter yang akan di-hydrate

Returns: Instance $filterClass dengan properties terisi dari $this->filter.


defaultParamKeys()

NEW v0.1.5

Protected method yang mengembalikan default mapping nama field DTO ke nama query param. Override di subclass untuk mengubah default secara global tanpa harus pass $param setiap instantiasi.

php
protected function defaultParamKeys(): array

Returns: array<string, string>

Contoh override:

php
class LegacyProductQuery extends ContainerQuery
{
    protected function defaultParamKeys(): array
    {
        return array_merge(parent::defaultParamKeys(), [
            'sortBy'  => 'orderBy',
            'sortDir' => 'orderDirection',
        ]);
    }
}

BaseContainerFilter

NEW v0.1.5

Namespace: Bpmlib\Laresponse\DTO\BaseContainerFilter

Abstract class untuk typed filter DTO per endpoint. Extend dan implementasikan mapFilterFields() untuk mendefinisikan mapping dari raw filter array ke typed properties.

Contains:


mapFilterFields()

php
abstract public static function mapFilterFields(array $data): array;

Menerima cleaned filter array dari ContainerQuery::$filter dan mengembalikan named array yang di-spread ke constructor subclass.

Parameters

NameTypeDefaultDescription
$dataarray-Cleaned filter array dari ContainerQuery::$filter

Returns: Named array untuk new static(...mapFilterFields($data)).

Contoh implementasi:

php
readonly class ProductFilter extends BaseContainerFilter
{
    public function __construct(
        public ?string $status = null,
        public ?string $categoryId = null,
    ) {}

    public static function mapFilterFields(array $data): array
    {
        return [
            'status'     => $data['status'] ?? null,
            'categoryId' => $data['category_id'] ?? null, // snake_case → camelCase
        ];
    }
}

Examples

Contains:


1. Single Data & Error Responses

php
use Bpmlib\Laresponse\Traits\JsonResponseTrait;

class UserController extends Controller
{
    use JsonResponseTrait;

    public function show($id)
    {
        $user = User::find($id);

        if (!$user) {
            return $this->returnJson([], 404);
        }

        return $this->returnJson($user);
    }

    public function destroy($id)
    {
        $product = Product::find($id);

        if (!$product) {
            return $this->returnJson([], 404);
        }

        if (!auth()->user()->can('delete', $product)) {
            return $this->returnJson([], 403);
        }

        $product->delete();
        return $this->returnJson([], 200, 'Product berhasil dihapus');
    }
}

2. List dengan Pagination (Adaptive)

returnAdaptive() menangani semua tipe — array, Collection, dan semua tipe Paginator:

php
public function index()
{
    // LengthAware
    return $this->returnAdaptive(Product::paginate(15));

    // Cursor
    return $this->returnAdaptive(Post::cursorPaginate(20));

    // Simple
    return $this->returnAdaptive(Product::simplePaginate(15));

    // Collection / array
    return $this->returnAdaptive(Product::all());
}

Atau jika tipe bisa berubah secara dinamis:

php
public function index(Request $request)
{
    $results = $request->boolean('all')
        ? Product::all()
        : Product::paginate(15);

    return $this->returnAdaptive($results);
}

3. Explicit Pagination Methods

Gunakan explicit methods jika API contract sudah fix atau ingin type safety:

php
// Admin dashboard: butuh total & max pages
public function adminIndex()
{
    return $this->returnPaginateJson(Product::paginate(15));
}

// Infinite scroll feed
public function feed()
{
    return $this->returnCursorPaginateJson(Post::latest()->cursorPaginate(20));
}

// Mobile listing: tidak butuh total count
public function mobileIndex()
{
    return $this->returnSimplePaginateJson(Product::simplePaginate(15));
}

4. ContainerQuery — Parsing & Response

Parsing semua query params dari utils-data-container sekaligus:

php
use Bpmlib\Laresponse\DTO\ContainerQuery;

public function index(Request $request)
{
    $q = new ContainerQuery($request);

    $query = Product::query()
        ->when($q->hasSort(), fn($b) => $b->orderBy($q->sortBy, $q->sortDir))
        ->when($q->hasSearch(), fn($b) => $b->where('name', 'like', "%{$q->search}%"));

    $results = match ($q->paginationMode()) {
        'cursor'  => $query->cursorPaginate($q->effectivePerPage()),
        'numeric' => $query->paginate($q->effectivePerPage()),
        default   => $query->get(),
    };

    return $this->returnAdaptive($results);
}

5. ContainerQuery — Nested Filter (Ad-hoc)

filterValue() cocok untuk akses cepat atau filter nested dengan dot-notation, tanpa perlu mendefinisikan filter class:

php
public function index(Request $request)
{
    $q = new ContainerQuery($request);

    $products = Product::query()
        ->when($q->filterValue('status'), fn($b, $v) => $b->where('status', $v))
        ->when($q->filterValue('user.role'), fn($b, $v) => $b->where('role', $v)) // nested: filter[user][role]
        ->paginate($q->effectivePerPage());

    return $this->returnAdaptive($products);
}

Untuk filter per endpoint yang typed dan IDE-autocomplete, lihat Example 8.


6. Data Mapper & Appends

Transform data per item dan tambahkan metadata ke response:

php
public function index(Request $request)
{
    $q = new ContainerQuery($request);
    $products = Product::with('category')->paginate($q->effectivePerPage());

    return $this->returnAdaptive(
        content: $products,
        mapper: fn($p) => [
            'id'        => $p->id,
            'name'      => $p->name,
            'price'     => $p->price,
            'category'  => $p->category->name,
            'available' => $p->stock > 0,
        ],
        appends: [
            'meta' => ['timestamp' => now()->toIso8601String()],
        ]
    );
}

7. Validation Errors

Field validation hanya muncul jika $code == 422:

php
public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'name'  => 'required|min:3',
        'email' => 'required|email|unique:users',
    ]);

    if ($validator->fails()) {
        return $this->returnJson(
            content: [],
            code: 422,
            validationErrors: $validator->errors()
        );
    }

    $user = User::create($request->validated());
    return $this->returnJson($user, 201);
}

Output (422):

json
{
  "message": "Form yang anda kirimkan ada yang tidak valid",
  "content": [],
  "success": false,
  "validation": {
    "email": ["The email has already been taken."]
  }
}

8. Typed Filter dengan BaseContainerFilter

Typed filter per endpoint — akses via properties, IDE-autocomplete, tidak ada magic strings:

php
use Bpmlib\Laresponse\DTO\BaseContainerFilter;

readonly class ProductFilter extends BaseContainerFilter
{
    public function __construct(
        public ?string $status = null,
        public ?string $categoryId = null,
        public array $tags = [],
    ) {}

    public static function mapFilterFields(array $data): array
    {
        return [
            'status'     => $data['status'] ?? null,
            'categoryId' => $data['category_id'] ?? null,
            'tags'       => $data['tags'] ?? [],
        ];
    }
}
php
public function index(Request $request)
{
    $q      = new ContainerQuery($request);
    $filter = $q->resolveFilter(ProductFilter::class);

    $products = Product::query()
        ->when($filter->status, fn($b, $v) => $b->where('status', $v))
        ->when($filter->categoryId, fn($b, $v) => $b->where('category_id', $v))
        ->when($filter->tags, fn($b, $v) => $b->whereIn('tag', $v))
        ->when($q->hasSort(), fn($b) => $b->orderBy($q->sortBy, $q->sortDir))
        ->paginate($q->effectivePerPage());

    return $this->returnAdaptive($products);
}

9. Custom Param Keys

Ketika nama param dari frontend tidak sesuai kontrak default utils-data-container:

php
// Per-request: override inline
$q = new ContainerQuery($request, ['sortBy' => 'orderBy', 'sortDir' => 'orderDirection', 'search' => 'keyword']);
php
// Per-class: subclass dengan override permanen — tidak perlu pass param setiap kali
class LegacyProductQuery extends ContainerQuery
{
    protected function defaultParamKeys(): array
    {
        return array_merge(parent::defaultParamKeys(), [
            'sortBy'  => 'orderBy',
            'sortDir' => 'orderDirection',
            'search'  => 'keyword',
        ]);
    }
}

$q = new LegacyProductQuery($request);

Laravel Integration

Penggunaan di Controllers

Pattern yang disarankan — pasang di base controller agar semua controllers punya akses:

php
namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Routing\Controller as BaseController;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;

abstract class Controller extends BaseController
{
    use AuthorizesRequests;
    use JsonResponseTrait;
}

ContainerQuery cukup di-instantiate di dalam method yang membutuhkan:

php
public function index(Request $request)
{
    $q = new ContainerQuery($request);
    // ...
}