Laresponse (bpmlib/laravel-laresponse)
Laravel trait untuk standardized JSON responses dengan support pagination dan validation errors
Versi: 0.1.2
TL;DR
Laravel Laresponse menyediakan trait JsonResponseTrait yang standardize format JSON responses di Laravel. Trait ini menangani berbagai tipe responses: single data, lists, pagination (LengthAware, Cursor, Simple), validation errors, dan data transformation. Semua response mengikuti struktur consistent dengan message, content, dan success fields.
Namespace:
use Bpmlib\Laresponse\Traits\JsonResponseTrait;Response Methods (Quick Reference):
returnAdaptive()- ⭐ RECOMMENDED untuk list/collection responses (auto-detect pagination type)returnJson()- ⭐ RECOMMENDED untuk single data atau error responsesreturnPaginateJson()- Explicit untuk LengthAware paginationreturnCursorPaginateJson()- Explicit untuk Cursor paginationreturnSimplePaginateJson()- Explicit untuk Simple pagination
Response Format:
{
"message": "Sukses",
"content": { ... },
"success": true,
"validation": { ... }
}Installation & Setup
Requirements
PHP Version
- Minimum: PHP 8.1
- Recommended: PHP 8.3+
Composer Dependencies
Package ini memerlukan Laravel components:
{
"illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/http": "^10.0|^11.0|^12.0",
"illuminate/pagination": "^10.0|^11.0|^12.0",
"illuminate/database": "^10.0|^11.0|^12.0"
}Framework Requirements
- Laravel: 10.0+, 11.0+, atau 12.0+
Composer Install
composer require bpmlib/laravel-laresponseAuto-Discovery
Tidak ada service provider atau config file. Trait langsung ready to use setelah installation.
Quick Start
Basic Usage - Single Data
Contoh paling sederhana untuk single resource response.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;
class UserController extends Controller
{
use JsonResponseTrait;
public function show($id)
{
$user = User::find($id);
if (!$user) {
return $this->returnJson([], 404, 'User not found');
}
return $this->returnJson($user);
}
}Output (Success - 200):
{
"message": "Sukses",
"content": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"success": true
}Output (Error - 404):
{
"message": "User not found",
"content": [],
"success": false
}Key Points:
returnJson()untuk single data atau error responses- HTTP code menentukan
successfield automatically - Custom message atau gunakan default message per status code
Basic Usage - List Data (Recommended Pattern)
Gunakan returnAdaptive() untuk semua list/collection responses. Method ini auto-detect tipe pagination.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
class ProductController extends Controller
{
use JsonResponseTrait;
public function index()
{
// Option 1: Auto-detect Array/Collection
$products = Product::all();
return $this->returnAdaptive($products);
// Option 2: Auto-detect LengthAwarePaginator
$products = Product::paginate(15);
return $this->returnAdaptive($products);
// Option 3: Auto-detect CursorPaginator
$products = Product::cursorPaginate(15);
return $this->returnAdaptive($products);
// Option 4: Auto-detect Paginator (Simple)
$products = Product::simplePaginate(15);
return $this->returnAdaptive($products);
}
}Output (LengthAware Pagination):
{
"message": "Sukses",
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" }
],
"success": true,
"max_page": 10,
"current_page": 1,
"per_page": 15,
"total": 150,
"has_more": true,
"next_cursor": null,
"previous_cursor": null
}Key Points:
- Satu method untuk semua tipe list (array, Collection, atau pagination)
- Auto-detect pagination type dan return appropriate metadata
- Consistent response structure untuk berbagai tipe data
With Validation Errors
Handle validation errors dengan returnJson() untuk status 422.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
class UserController extends Controller
{
use JsonResponseTrait;
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|min:3',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
if ($validator->fails()) {
return $this->returnJson(
content: [],
code: 422,
message: 'Validasi gagal',
validationErrors: $validator->errors()
);
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
]);
return $this->returnJson($user, 201, 'User berhasil dibuat');
}
}Output (Validation Failed - 422):
{
"message": "Validasi gagal",
"content": [],
"success": false,
"validation": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
}
}Output (Success - 201):
{
"message": "User berhasil dibuat",
"content": {
"id": 5,
"name": "Jane Smith",
"email": "jane@example.com"
},
"success": true
}Key Points:
validationErrorsparameter exclusive untukreturnJson()- Validation errors hanya muncul di response jika HTTP code = 422
- Accepts
MessageBagatauarrayformat
With Data Mapper
Transform data sebelum dikirim ke frontend menggunakan $mapper callback.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;
class UserController extends Controller
{
use JsonResponseTrait;
public function index()
{
$users = User::with('profile')->paginate(10);
return $this->returnAdaptive(
content: $users,
mapper: function($user) {
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'avatar' => $user->profile?->avatar_url ?? '/default-avatar.png',
'member_since' => $user->created_at->diffForHumans(),
'verified' => (bool) $user->email_verified_at,
];
}
);
}
}Output:
{
"message": "Sukses",
"content": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"avatar": "https://example.com/avatars/john.jpg",
"member_since": "2 months ago",
"verified": true
}
],
"success": true,
"max_page": 5,
"current_page": 1,
"per_page": 10,
"total": 50,
"has_more": true
}Key Points:
- Mapper callback applied ke setiap item di array/Collection/Paginator
- Works dengan
returnJson()danreturnAdaptive() - Clean separation: data transformation di response layer, bukan di Model
- Useful untuk hide sensitive fields atau compute derived properties
Core Concepts
Standardized Response Structure
Semua responses mengikuti struktur consistent untuk memudahkan frontend parsing.
Base Structure:
[
'message' => string, // Human-readable message
'content' => mixed, // Main response data
'success' => bool, // Auto-determined dari HTTP code (2xx = true)
]Extended Structure (Pagination):
[
'message' => string,
'content' => array,
'success' => bool,
// LengthAware Pagination
'max_page' => int,
'current_page' => int,
'per_page' => int,
'total' => int,
'has_more' => bool,
// Cursor Pagination
'next_cursor' => ?string,
'previous_cursor' => ?string,
]Extended Structure (Validation Errors):
[
'message' => string,
'content' => [],
'success' => false,
'validation' => [
'field_name' => ['error message 1', 'error message 2']
]
]Implications:
- Frontend selalu bisa expect
message,content,successfields successfield eliminate need untuk check HTTP status di frontend- Pagination metadata consistent across different pagination types (unused fields set to
null)
Method Selection Guide
Library ini menyediakan multiple methods dengan different trade-offs. Pahami kapan menggunakan masing-masing.
returnAdaptive() vs Specific Methods
returnAdaptive() (Recommended Default):
- ✅ Auto-detect tipe pagination (LengthAware, Cursor, Simple, atau plain array)
- ✅ Flexible untuk berbagai tipe data
- ✅ Single method untuk semua list responses
- ✅ Best untuk rapid development dan dynamic queries
- ⚠️ Sedikit overhead untuk type checking (negligible untuk most cases)
Specific Methods (returnXxxPaginateJson):
- ✅ Explicit type - no auto-detection overhead
- ✅ Type safety jika kamu yakin exact pagination type
- ✅ Useful untuk strict API contracts (OpenAPI/Swagger)
- ✅ Code clarity - explicit intent
- ⚠️ Harus tahu pagination type sebelumnya
When to Use returnAdaptive()
Use cases:
- ✅ Default choice untuk list responses
- ✅ Working dengan dynamic queries yang bisa paginate atau tidak
- ✅ Prototyping atau rapid development
- ✅ Pagination type bisa berubah based on requirements
- ✅ Flexible API responses (mobile + web dengan different pagination needs)
Example:
// Query bisa return paginated atau all items based on request
public function index(Request $request)
{
$query = Product::query();
if ($request->has('paginate')) {
$products = $query->paginate($request->input('per_page', 15));
} else {
$products = $query->get();
}
// returnAdaptive handles both cases
return $this->returnAdaptive($products);
}When to Use returnJson()
Use cases:
- ✅ Single resource responses (
User::find(),Product::first()) - ✅ Error responses (404, 500, 403, dll)
- ✅ Validation errors (422) - exclusive feature
- ✅ Success responses dengan single item
- ✅ Empty success responses (204, 201 after create/delete)
Example:
// Single resource
public function show($id)
{
$user = User::find($id);
return $this->returnJson($user);
}
// Error response
public function destroy($id)
{
$product = Product::find($id);
if (!$product) {
return $this->returnJson([], 404, 'Product not found');
}
$product->delete();
return $this->returnJson([], 200, 'Product deleted successfully');
}
// Validation error (exclusive to returnJson)
public function store(Request $request)
{
$validator = Validator::make($request->all(), [...]);
if ($validator->fails()) {
return $this->returnJson([], 422, validationErrors: $validator->errors());
}
// ...
}When to Use Specific Pagination Methods
Use cases:
- ✅ API contract explicitly requires specific pagination type
- ✅ Performance critical endpoints (high traffic, avoid type checking)
- ✅ Code clarity preferred over flexibility
- ✅ Team convention uses explicit typing
- ✅ Documented API (OpenAPI/Swagger) dengan fixed pagination schema
Example:
// API contract guarantees LengthAware pagination
public function index()
{
$products = Product::paginate(15); // Always LengthAwarePaginator
return $this->returnPaginateJson($products);
}
// Infinite scroll API always uses Cursor
public function feed()
{
$posts = Post::latest()->cursorPaginate(20);
return $this->returnCursorPaginateJson($posts);
}Decision Flowchart
Start
|
+-- Single data/error? -> returnJson()
|
+-- Validation error (422)? -> returnJson() (exclusive)
|
+-- List/Collection?
| |
| +-- Flexible/unknown pagination? -> returnAdaptive()
| |
| +-- Strict API contract? -> returnXxxPaginateJson()
| |
| +-- Performance critical? -> returnXxxPaginateJson()
|
+-- Default -> returnAdaptive()Automatic Message Resolution
Trait menyediakan default messages untuk common HTTP status codes, eliminate boilerplate message strings.
Default Messages:
[
200 => 'Sukses',
201 => 'Sukses dibuat',
400 => 'Pastikan format request yang Anda kirimkan sesuai',
401 => 'Anda belum login',
403 => 'Anda tidak mempunyai akses untuk ini',
404 => 'Data yang anda cari tidak ada',
422 => 'Form yang anda kirimkan ada yang tidak valid',
500 => 'Terjadi kesalahan di server',
503 => 'Server sedang sibuk',
]How it works:
// Empty message - uses default
return $this->returnJson($user, 404);
// Output: { "message": "Data yang anda cari tidak ada", ... }
// Custom message - overrides default
return $this->returnJson($user, 404, 'User tidak ditemukan');
// Output: { "message": "User tidak ditemukan", ... }
// Status code not in dictionary - fallback message
return $this->returnJson($user, 418);
// Output: { "message": "Hai :D", ... }Benefits:
- Consistent messages across application
- Less boilerplate code
- Easy to customize per-response when needed
- Fallback message untuk unknown status codes
Data Mapping Pattern
Mapper callback memungkinkan data transformation sebelum dikirim ke response, clean separation antara data layer dan presentation layer.
How it works:
Mapper callback dipanggil untuk setiap item:
- Array (list):
array_map($mapper, $array) - Array (associative):
$mapper($array) - Collection:
$collection->map($mapper) - Paginator:
$paginator->getCollection()->map($mapper)
Common use cases:
- Hide sensitive fields:
return $this->returnAdaptive($users, mapper: fn($u) => [
'id' => $u->id,
'name' => $u->name,
// Hide: email, password_hash, api_token
]);- Compute derived properties:
return $this->returnAdaptive($products, mapper: fn($p) => [
'id' => $p->id,
'name' => $p->name,
'price' => $p->price,
'discount_price' => $p->price * 0.9,
'is_available' => $p->stock > 0,
]);- Format dates/timestamps:
return $this->returnAdaptive($orders, mapper: fn($o) => [
'id' => $o->id,
'total' => $o->total,
'created' => $o->created_at->format('Y-m-d H:i:s'),
'created_human' => $o->created_at->diffForHumans(),
]);- Include related data:
return $this->returnAdaptive($posts->load('author'), mapper: fn($p) => [
'id' => $p->id,
'title' => $p->title,
'author' => [
'id' => $p->author->id,
'name' => $p->author->name,
],
]);Best practices:
- Use mapper untuk presentation logic, bukan business logic
- Keep mappers simple dan readable
- Extract ke dedicated methods jika mapper complex:
public function index()
{
return $this->returnAdaptive(
Product::paginate(15),
mapper: [$this, 'mapProductForApi']
);
}
private function mapProductForApi(Product $product): array
{
return [
'id' => $product->id,
'name' => $product->name,
// ... complex mapping logic
];
}Append Mechanism
Parameter $appends memungkinkan merge additional data ke response tanpa override built-in keys.
How it works:
$appends = [
'meta' => ['api_version' => '2.0'],
'message' => 'This will be ignored', // Cannot override
];
return $this->returnJson($data, appends: $appends);Built-in keys (message, content, success, validation, pagination metadata) cannot be overridden oleh $appends. System menggunakan array_diff_key() untuk filter out conflicts.
Common use cases:
- API metadata:
return $this->returnAdaptive($products, appends: [
'meta' => [
'api_version' => '2.0',
'timestamp' => now()->toIso8601String(),
'server_time' => now()->timestamp,
],
]);- Applied filters info:
return $this->returnAdaptive($products, appends: [
'filters' => [
'category' => request('category'),
'min_price' => request('min_price'),
'max_price' => request('max_price'),
],
]);- Aggregated data:
return $this->returnAdaptive($orders, appends: [
'summary' => [
'total_amount' => $orders->sum('total'),
'average_amount' => $orders->avg('total'),
'count' => $orders->count(),
],
]);- Debug information (development only):
if (app()->environment('local')) {
$appends['debug'] = [
'query_count' => DB::getQueryLog(),
'memory_usage' => memory_get_peak_usage(true),
];
}
return $this->returnAdaptive($data, appends: $appends);Protection mechanism:
// These keys are protected and cannot be overridden:
$protected = [
'message', 'content', 'success', 'validation',
'max_page', 'current_page', 'per_page', 'total', 'has_more',
'next_cursor', 'previous_cursor',
];API Reference
JsonResponseTrait
Trait untuk standardize JSON responses di Laravel controllers.
Namespace: Bpmlib\Laresponse\Traits\JsonResponseTrait
Contains:
- returnAdaptive() ⭐ RECOMMENDED
- returnJson() ⭐ RECOMMENDED
- returnPaginateJson()
- returnCursorPaginateJson()
- returnSimplePaginateJson()
returnAdaptive()
Auto-detect tipe data (array, Collection, LengthAware, Cursor, Simple) dan return appropriate response format.
Use cases:
- Default choice untuk list/collection responses
- Dynamic queries yang bisa return different pagination types
- Flexible APIs yang support multiple response formats
Signature:
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
): JsonResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$content | Collection|LengthAwarePaginator|CursorPaginator|Paginator|array | required | Data yang akan di-return Lihat selengkapnya |
$message | string | '' | Custom message atau gunakan default per HTTP code |
$code | int | 200 | HTTP status code |
$mapper | ?callable | null | Callback untuk transform data Lihat selengkapnya |
$appends | array | [] | Additional data untuk merge ke response Lihat selengkapnya |
$returnHeaders | array | [] | Custom HTTP headers |
$returnOptions | int | JSON_THROW_ON_ERROR | JSON encoding options |
$content
Union type parameter yang accept berbagai tipe data dan auto-detect appropriate response format.
Accepted Types:
Illuminate\Support\Collection-> CallsreturnJson()internallyIlluminate\Database\Eloquent\Collection-> CallsreturnJson()internallyIlluminate\Pagination\LengthAwarePaginator-> CallsreturnPaginateJson()Illuminate\Pagination\CursorPaginator-> CallsreturnCursorPaginateJson()Illuminate\Pagination\Paginator-> CallsreturnSimplePaginateJson()array-> CallsreturnJson()internally
Contoh:
// Array
return $this->returnAdaptive(['item1', 'item2']);
// Collection
return $this->returnAdaptive(collect([...]));
// LengthAware
return $this->returnAdaptive(User::paginate(10));
// Cursor
return $this->returnAdaptive(Post::cursorPaginate(20));
// Simple
return $this->returnAdaptive(Product::simplePaginate(15));$mapper
Callback function untuk transform setiap item sebelum dikirim ke response.
Signature:
callable(mixed $item, ?int $index = null): mixedParameters:
$item- Current item dari array/Collection$index- Index position (optional, untuk array only)
Contoh:
// Simple transformation
return $this->returnAdaptive($users, mapper: function($user) {
return [
'id' => $user->id,
'name' => $user->name,
];
});
// With index (array only)
return $this->returnAdaptive($items, mapper: function($item, $index) {
return [
'position' => $index + 1,
'data' => $item,
];
});
// Arrow function syntax
return $this->returnAdaptive(
$products,
mapper: fn($p) => ['id' => $p->id, 'name' => $p->name]
);Use Case:
Gunakan mapper untuk presentation-layer transformations: hide sensitive fields, compute derived properties, format dates, include related data.
$appends
Array of additional data untuk merge ke response. Built-in keys tidak bisa di-override.
Structure:
[
'custom_key' => mixed,
'another_key' => mixed,
]Protected Keys (Cannot Override):
message,content,success,validationmax_page,current_page,per_page,total,has_morenext_cursor,previous_cursor
Contoh:
return $this->returnAdaptive($products, appends: [
'meta' => [
'version' => '2.0',
'timestamp' => now()->toIso8601String(),
],
'filters' => request()->only(['category', 'price_min']),
]);Use Case:
Gunakan untuk API metadata, filter information, aggregated data, atau debug info (development only).
Returns: JsonResponse
Auto-Detection Logic:
if ($content instanceof LengthAwarePaginator) {
return $this->returnPaginateJson(...);
}
if ($content instanceof CursorPaginator) {
return $this->returnCursorPaginateJson(...);
}
if ($content instanceof Paginator) {
return $this->returnSimplePaginateJson(...);
}
// Default: array or Collection
return $this->returnJson(...);Why Use This Over Specific Methods:
- ✅ Single method untuk all list types
- ✅ Flexible untuk changing requirements
- ✅ Less code duplication
- ✅ Easier refactoring (change pagination type tanpa change method call)
returnJson()
Base method untuk single data atau error responses dengan optional validation errors.
Use cases:
- Single resource responses (show, store, update)
- Error responses (404, 500, 403)
- Validation errors (422) - exclusive feature
- Empty success responses
Signature:
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
): JsonResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$content | mixed | [] | Data utama response |
$code | int | 200 | HTTP status code |
$message | string | '' | Custom message atau default |
$validationErrors | array|MessageBag | [] | Validation errors Lihat selengkapnya |
$mapper | ?callable | null | Transform callback Lihat selengkapnya |
$appends | array | [] | Additional response data Lihat selengkapnya |
$returnHeaders | array | [] | Custom HTTP headers |
$returnOptions | int | JSON_THROW_ON_ERROR | JSON encoding options |
$validationErrors
Validation errors dari Laravel Validator. Hanya muncul di response jika $code == 422.
Signature:
array|Illuminate\Support\MessageBag $validationErrorsAccepted Formats:
// MessageBag (from Validator)
$validator = Validator::make(...);
return $this->returnJson([], 422, validationErrors: $validator->errors());
// Array format
return $this->returnJson([], 422, validationErrors: [
'email' => ['Email sudah digunakan'],
'password' => ['Password minimal 8 karakter'],
]);Contoh:
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
if ($validator->fails()) {
return $this->returnJson(
content: [],
code: 422,
message: 'Validasi gagal',
validationErrors: $validator->errors()
);
}
// ... create user
}Output:
{
"message": "Validasi gagal",
"content": [],
"success": false,
"validation": {
"email": ["The email has already been taken."],
"password": ["The password must be at least 8 characters."]
}
}Use Case:
Exclusive untuk validation error responses (422). Field validation tidak muncul untuk HTTP codes lain.
$mapper
Same as returnAdaptive() - see $mapper section above.
$appends
Same as returnAdaptive() - see $appends section above.
Returns: JsonResponse
Response Structure:
[
'message' => string,
'content' => mixed,
'success' => bool, // true if 200-299
'validation' => array|null, // only if code == 422
]Validation Errors Feature:
Unique feature yang hanya tersedia di returnJson():
- Parameter
$validationErrorsacceptMessageBagatauarray - Field
validationhanya muncul jika$code == 422 - Other pagination methods tidak support validation errors
returnPaginateJson()
Explicit method untuk LengthAware pagination responses.
Use cases:
- API contract requires page-based pagination dengan total count
- Admin dashboards butuh total pages untuk UI controls
- Performance critical - skip auto-detection
- Explicit code clarity preferred
Signature:
protected function returnPaginateJson(
LengthAwarePaginator $content,
int $code = 200,
string $message = 'Success',
?callable $mapper = null,
array $appends = [],
array $returnHeaders = [],
int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$content | LengthAwarePaginator | required | LengthAware paginated data |
$code | int | 200 | HTTP status code |
$message | string | 'Success' | Custom message atau default |
$mapper | ?callable | null | Transform callback Lihat selengkapnya |
$appends | array | [] | Additional response data Lihat selengkapnya |
$returnHeaders | array | [] | Custom HTTP headers |
$returnOptions | int | JSON_THROW_ON_ERROR | JSON encoding options |
$mapper
Same pattern as other methods - see $mapper section.
$appends
Same pattern as other methods - see $appends section.
Returns: JsonResponse
Response Structure:
[
'message' => string,
'content' => array,
'success' => bool,
'max_page' => int,
'current_page' => int,
'per_page' => int,
'total' => int,
'has_more' => bool,
'next_cursor' => null, // Always null (unused)
'previous_cursor' => null, // Always null (unused)
]When to Use This Directly:
- ✅ Frontend needs total count dan max pages
- ✅ Building pagination UI dengan numbered pages
- ✅ API documentation specifies LengthAware pagination
- ✅ Performance: avoid type checking overhead
Advantages Over returnAdaptive():
- Type-safe: guarantees LengthAwarePaginator input
- No runtime type detection
- Clear intent di code
returnCursorPaginateJson()
Explicit method untuk Cursor pagination responses (infinite scroll).
Use cases:
- Infinite scroll implementations
- Real-time feeds (social media, news)
- Large datasets tanpa total count overhead
- Performance: stateless pagination
Signature:
protected function returnCursorPaginateJson(
CursorPaginator $content,
int $code = 200,
string $message = 'Success',
?callable $mapper = null,
array $appends = [],
array $returnHeaders = [],
int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$content | CursorPaginator | required | Cursor paginated data |
$code | int | 200 | HTTP status code |
$message | string | 'Success' | Custom message atau default |
$mapper | ?callable | null | Transform callback Lihat selengkapnya |
$appends | array | [] | Additional response data Lihat selengkapnya |
$returnHeaders | array | [] | Custom HTTP headers |
$returnOptions | int | JSON_THROW_ON_ERROR | JSON encoding options |
$mapper
Same pattern as other methods - see $mapper section.
$appends
Same pattern as other methods - see $appends section.
Returns: JsonResponse
Response Structure:
[
'message' => string,
'content' => array,
'success' => bool,
'per_page' => int,
'has_more' => bool,
'next_cursor' => ?string,
'previous_cursor' => ?string,
'current_page' => null, // Always null (unused)
'max_page' => null, // Always null (unused)
'total' => null, // Always null (unused)
]Cursor Pagination Specifics:
next_cursor: Encoded cursor untuk next page (null jika no more pages)previous_cursor: Encoded cursor untuk previous page (null jika first page)- No
total,max_page,current_page- stateless pagination
When to Use This Directly:
- ✅ Infinite scroll UI pattern
- ✅ Real-time feeds dengan constantly changing data
- ✅ Large datasets (millions of rows) - better performance than LengthAware
- ✅ API contract specifies cursor-based pagination
returnSimplePaginateJson()
Explicit method untuk Simple pagination responses (lightweight, no total count).
Use cases:
- Simple pagination tanpa total count overhead
- "Next/Previous" navigation only
- Lightweight APIs untuk mobile
- Don't need total pages information
Signature:
protected function returnSimplePaginateJson(
Paginator $content,
int $code = 200,
string $message = 'Success',
?callable $mapper = null,
array $appends = [],
array $returnHeaders = [],
int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$content | Paginator | required | Simple paginated data |
$code | int | 200 | HTTP status code |
$message | string | 'Success' | Custom message atau default |
$mapper | ?callable | null | Transform callback Lihat selengkapnya |
$appends | array | [] | Additional response data Lihat selengkapnya |
$returnHeaders | array | [] | Custom HTTP headers |
$returnOptions | int | JSON_THROW_ON_ERROR | JSON encoding options |
$mapper
Same pattern as other methods - see $mapper section.
$appends
Same pattern as other methods - see $appends section.
Returns: JsonResponse
Response Structure:
[
'message' => string,
'content' => array,
'success' => bool,
'per_page' => int,
'current_page' => int,
'has_more' => bool,
'total' => null, // Always null (unused)
'max_page' => null, // Always null (unused)
'next_cursor' => null, // Always null (unused)
'previous_cursor' => null, // Always null (unused)
]Simple Pagination Specifics:
- Lightweight: doesn't query total count
- Only knows
has_more(next page exists atau tidak) - No
totalataumax_page- faster queries - Best untuk mobile apps atau simple listings
When to Use This Directly:
- ✅ Simple "Next/Previous" navigation
- ✅ Don't need total count information
- ✅ Performance optimization (avoid COUNT query)
- ✅ Mobile-first APIs dengan limited data needs
Examples
Contains:
- 1. Error Responses (404, 422, 500)
- 2. Explicit Pagination Methods
- 3. Custom Headers & Appends
- 4. Comparison - Adaptive vs Explicit
1. Error Responses (404, 422, 500)
Berbagai error response patterns dengan HTTP codes berbeda.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use App\Models\Product;
class ProductController extends Controller
{
use JsonResponseTrait;
// 404 Not Found
public function show($id)
{
$product = Product::find($id);
if (!$product) {
return $this->returnJson([], 404, 'Product tidak ditemukan');
}
return $this->returnJson($product);
}
// 422 Validation Error (Exclusive Feature)
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|min:3',
'price' => 'required|numeric|min:0',
'category_id' => 'required|exists:categories,id',
]);
if ($validator->fails()) {
return $this->returnJson(
content: [],
code: 422,
message: 'Data produk tidak valid',
validationErrors: $validator->errors()
);
}
$product = Product::create($request->all());
return $this->returnJson($product, 201, 'Product berhasil dibuat');
}
// 500 Server Error
public function process($id)
{
try {
$product = Product::findOrFail($id);
// Complex processing that might fail
$result = app('SomeComplexService')->processProduct($product);
return $this->returnJson($result, 200, 'Processing berhasil');
} catch (\Exception $e) {
Log::error('Product processing failed', [
'product_id' => $id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->returnJson(
content: [],
code: 500,
message: 'Terjadi kesalahan saat memproses produk'
);
}
}
// 403 Forbidden
public function delete($id)
{
$product = Product::find($id);
if (!$product) {
return $this->returnJson([], 404, 'Product tidak ditemukan');
}
if (!auth()->user()->can('delete', $product)) {
return $this->returnJson(
[],
403,
'Anda tidak memiliki izin untuk menghapus produk ini'
);
}
$product->delete();
return $this->returnJson([], 200, 'Product berhasil dihapus');
}
}Output Examples:
404 Response:
{
"message": "Product tidak ditemukan",
"content": [],
"success": false
}422 Response (Validation):
{
"message": "Data produk tidak valid",
"content": [],
"success": false,
"validation": {
"name": ["The name field is required."],
"price": ["The price must be a number.", "The price must be at least 0."]
}
}500 Response:
{
"message": "Terjadi kesalahan saat memproses produk",
"content": [],
"success": false
}Key Takeaways:
returnJson()untuk semua error responses- Validation errors exclusive feature (422 only)
- Custom messages improve UX
- Log errors untuk debugging (500)
- Use appropriate HTTP status codes
2. Explicit Pagination Methods
Kapan dan bagaimana menggunakan explicit pagination methods dibanding returnAdaptive().
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use App\Models\Post;
use Illuminate\Http\Request;
class ProductController extends Controller
{
use JsonResponseTrait;
/**
* LengthAware Pagination - When you need total count
*
* Use case: Admin dashboards, reports, numbered pagination UI
*/
public function indexWithTotal()
{
$products = Product::with('category')
->where('status', 'active')
->paginate(15);
// Explicit: Type-safe, no auto-detection overhead
return $this->returnPaginateJson(
$products,
mapper: fn($p) => [
'id' => $p->id,
'name' => $p->name,
'price' => $p->price,
'category' => $p->category->name,
]
);
}
/**
* Cursor Pagination - For infinite scroll
*
* Use case: Social feeds, real-time data, large datasets
*/
public function indexInfiniteScroll(Request $request)
{
$posts = Post::with('author')
->latest()
->cursorPaginate(20);
// Explicit: Clear intent for cursor-based pagination
return $this->returnCursorPaginateJson(
$posts,
mapper: fn($p) => [
'id' => $p->id,
'title' => $p->title,
'excerpt' => $p->excerpt,
'author' => $p->author->name,
'published' => $p->created_at->diffForHumans(),
]
);
}
/**
* Simple Pagination - Lightweight, no total count
*
* Use case: Mobile apps, simple listings, performance optimization
*/
public function indexSimple()
{
$products = Product::select('id', 'name', 'price')
->where('featured', true)
->simplePaginate(15);
// Explicit: Lightweight, skip COUNT query
return $this->returnSimplePaginateJson($products);
}
/**
* Comparison: Adaptive vs Explicit
*/
public function indexAdaptive()
{
// Option 1: Adaptive (Flexible)
$products = Product::paginate(15);
return $this->returnAdaptive($products);
// Auto-detects: calls returnPaginateJson() internally
// Option 2: Explicit (Type-safe)
// return $this->returnPaginateJson($products);
// Direct call, no type checking
}
}Response Structures Comparison:
LengthAware (returnPaginateJson):
{
"message": "Sukses",
"content": [
{ "id": 1, "name": "Product 1", "price": 100000 }
],
"success": true,
"max_page": 10,
"current_page": 1,
"per_page": 15,
"total": 150,
"has_more": true,
"next_cursor": null,
"previous_cursor": null
}Cursor (returnCursorPaginateJson):
{
"message": "Sukses",
"content": [
{ "id": 1, "title": "Post Title" }
],
"success": true,
"per_page": 20,
"has_more": true,
"next_cursor": "eyJpZCI6MjAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"previous_cursor": null,
"current_page": null,
"max_page": null,
"total": null
}Simple (returnSimplePaginateJson):
{
"message": "Sukses",
"content": [
{ "id": 1, "name": "Product 1" }
],
"success": true,
"per_page": 15,
"current_page": 1,
"has_more": true,
"total": null,
"max_page": null,
"next_cursor": null,
"previous_cursor": null
}When to Use Explicit Methods:
| Method | Use When | Benefits |
|---|---|---|
returnPaginateJson() | Need total count, numbered pages | Full pagination metadata |
returnCursorPaginateJson() | Infinite scroll, real-time feeds | Stateless, better performance |
returnSimplePaginateJson() | Simple navigation, mobile apps | Lightweight, no COUNT query |
returnAdaptive() | Default, flexible, prototyping | Auto-detect, single method |
Key Takeaways:
- Explicit methods provide type safety dan clear intent
- Use when API contract requires specific pagination type
- Performance benefit: skip type detection overhead
- Adaptive method remains default untuk flexibility
3. Custom Headers & Appends
Advanced usage: Custom HTTP headers dan additional response data.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProductController extends Controller
{
use JsonResponseTrait;
/**
* Example: API with metadata, filters, and custom headers
*/
public function index(Request $request)
{
$query = Product::query();
// Apply filters
if ($request->has('category')) {
$query->where('category_id', $request->category);
}
if ($request->has('min_price')) {
$query->where('price', '>=', $request->min_price);
}
if ($request->has('max_price')) {
$query->where('price', '<=', $request->max_price);
}
$products = $query->paginate(15);
// Build appends data
$appends = [
'meta' => [
'api_version' => '2.0',
'timestamp' => now()->toIso8601String(),
'server_time' => now()->timestamp,
],
'applied_filters' => $request->only(['category', 'min_price', 'max_price']),
'aggregations' => [
'total_value' => $products->sum('price'),
'average_price' => $products->avg('price'),
'min_price' => $products->min('price'),
'max_price' => $products->max('price'),
],
];
// Add debug info in development
if (app()->environment('local')) {
$appends['debug'] = [
'query_count' => count(DB::getQueryLog()),
'memory_usage' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB',
];
}
return $this->returnAdaptive(
content: $products,
mapper: fn($p) => [
'id' => $p->id,
'name' => $p->name,
'price' => $p->price,
],
appends: $appends,
returnHeaders: [
'X-API-Version' => '2.0',
'X-RateLimit-Limit' => '100',
'X-RateLimit-Remaining' => '95',
'X-Total-Count' => $products->total(),
]
);
}
/**
* Example: Attempting to override built-in keys (won't work)
*/
public function testProtection()
{
$products = Product::take(3)->get();
return $this->returnAdaptive(
content: $products,
appends: [
// ❌ These will be IGNORED (protected keys)
'message' => 'This will not override',
'success' => false,
'content' => ['fake data'],
// ✅ These will be ADDED (custom keys)
'custom_field' => 'This will appear',
'meta' => ['info' => 'This works'],
]
);
}
}Output Example:
{
"message": "Sukses",
"content": [
{ "id": 1, "name": "Product A", "price": 100000 },
{ "id": 2, "name": "Product B", "price": 150000 }
],
"success": true,
"max_page": 5,
"current_page": 1,
"per_page": 15,
"total": 75,
"has_more": true,
"meta": {
"api_version": "2.0",
"timestamp": "2024-12-26T10:30:00+00:00",
"server_time": 1703588400
},
"applied_filters": {
"category": "electronics",
"min_price": "50000",
"max_price": "200000"
},
"aggregations": {
"total_value": 1500000,
"average_price": 125000,
"min_price": 50000,
"max_price": 200000
},
"debug": {
"query_count": 3,
"memory_usage": "12.5 MB"
}
}Response Headers:
X-API-Version: 2.0
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-Total-Count: 75
Content-Type: application/jsonKey Takeaways:
appendstidak bisa override built-in keys (message, content, success, pagination metadata)returnHeadersuntuk custom HTTP headers (API versioning, rate limiting, etc.)- Useful untuk:
- API metadata dan versioning
- Applied filters information
- Aggregated statistics
- Debug information (development only)
- Rate limiting headers
- Protection mechanism ensures response structure consistency
4. Comparison - Adaptive vs Explicit
Side-by-side comparison kapan menggunakan returnAdaptive() vs explicit pagination methods.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use Illuminate\Http\Request;
class ComparisonController extends Controller
{
use JsonResponseTrait;
/**
* Scenario 1: Default Case - Use Adaptive
*
* Best for: General CRUD endpoints, flexible requirements
*/
public function scenarioDefault()
{
$products = Product::paginate(15);
// ✅ RECOMMENDED: Adaptive
return $this->returnAdaptive($products);
// - Auto-detects LengthAwarePaginator
// - Calls returnPaginateJson() internally
// - Flexible if pagination type changes later
}
/**
* Scenario 2: Dynamic Pagination - Use Adaptive
*
* Best for: Query might return different types based on conditions
*/
public function scenarioDynamic(Request $request)
{
$query = Product::query();
// Pagination type depends on request
if ($request->boolean('all')) {
// Return all items (Collection)
$products = $query->get();
} elseif ($request->boolean('cursor')) {
// Cursor pagination
$products = $query->cursorPaginate(20);
} else {
// Default: LengthAware pagination
$products = $query->paginate(15);
}
// ✅ RECOMMENDED: Adaptive handles all cases
return $this->returnAdaptive($products);
}
/**
* Scenario 3: Strict API Contract - Use Explicit
*
* Best for: Documented APIs (OpenAPI/Swagger), strict typing
*/
public function scenarioStrictContract()
{
// API documentation guarantees LengthAwarePaginator
$products = Product::paginate(15);
// ✅ RECOMMENDED: Explicit for strict contracts
return $this->returnPaginateJson($products);
// - Type-safe: ensures LengthAwarePaginator
// - Clear intent in code
// - Matches API documentation
}
/**
* Scenario 4: Performance Critical - Use Explicit
*
* Best for: High traffic endpoints, micro-optimization needed
*/
public function scenarioPerformance()
{
$products = Product::cursorPaginate(20);
// ✅ RECOMMENDED: Explicit for performance
return $this->returnCursorPaginateJson($products);
// - No type checking overhead
// - Direct method call
// - Measurable improvement at scale
}
/**
* Scenario 5: Team Convention - Depends
*
* Follow team standards
*/
public function scenarioTeamConvention()
{
$products = Product::paginate(15);
// Team prefers explicit typing
return $this->returnPaginateJson($products);
// OR: Team prefers flexibility
// return $this->returnAdaptive($products);
}
/**
* Comparison: Both produce identical output
*/
public function comparison()
{
$products = Product::paginate(15);
// Approach 1: Adaptive (Flexible)
$response1 = $this->returnAdaptive($products);
// Approach 2: Explicit (Type-safe)
$response2 = $this->returnPaginateJson($products);
// Both produce IDENTICAL JSON output
// Difference is in type checking and intent
return $response1; // or $response2 - same result
}
}Identical Output (Both Methods):
{
"message": "Sukses",
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" }
],
"success": true,
"max_page": 10,
"current_page": 1,
"per_page": 15,
"total": 150,
"has_more": true,
"next_cursor": null,
"previous_cursor": null
}Decision Matrix:
| Factor | Use returnAdaptive() | Use Explicit Method |
|---|---|---|
| Flexibility | ✅ High - handles any type | ❌ Low - locked to one type |
| Type Safety | ⚠️ Runtime detection | ✅ Compile-time guarantee |
| Performance | ⚠️ Tiny overhead | ✅ Direct call, no overhead |
| Code Clarity | ⚠️ Intent less clear | ✅ Clear intent |
| Refactoring | ✅ Easy - change query only | ⚠️ Must change method call |
| API Contract | ⚠️ Flexible (pro/con) | ✅ Strict contract |
| Best For | Prototyping, CRUD, dynamic | Production, documented APIs |
Code Comparison:
// ✅ Adaptive: One method, handles all
public function index(Request $request)
{
$products = $request->boolean('all')
? Product::all()
: Product::paginate(15);
return $this->returnAdaptive($products); // Works for both
}
// ⚠️ Explicit: Must know type ahead
public function index(Request $request)
{
if ($request->boolean('all')) {
$products = Product::all();
return $this->returnJson($products); // Different method
} else {
$products = Product::paginate(15);
return $this->returnPaginateJson($products); // Different method
}
}Performance Benchmark (Hypothetical):
returnAdaptive(): 0.15ms (includes type checking)
returnPaginateJson(): 0.12ms (direct call)
Difference: 0.03ms (negligible untuk most apps)
At 10,000 req/s: 300ms total overhead per secondRecommendation:
- Default: Use
returnAdaptive()untuk flexibility - Switch to explicit when:
- API contract is documented dan fixed
- Performance profiling shows bottleneck
- Team convention prefers explicit typing
- Large-scale production APIs
Key Takeaways:
- Both methods produce identical output
- Adaptive = flexibility, Explicit = type safety
- Performance difference negligible untuk most cases
- Choose based on project needs dan team convention
- You can mix both approaches di different endpoints
Laravel Integration
Using in Controllers
Trait dirancang untuk digunakan di Laravel controllers. Import dan gunakan di base controller atau individual controllers.
Base Controller Pattern (Recommended):
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
abstract class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
use JsonResponseTrait; // Available to all controllers
}Setelah setup di base controller, semua controllers otomatis punya access:
<?php
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller
{
// JsonResponseTrait methods available here
public function index()
{
return $this->returnAdaptive(User::paginate(15));
}
public function show($id)
{
$user = User::find($id);
if (!$user) {
return $this->returnJson([], 404);
}
return $this->returnJson($user);
}
}Individual Controller Pattern:
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
class ProductController extends Controller
{
use JsonResponseTrait; // Only this controller has access
public function index()
{
return $this->returnAdaptive(Product::paginate(10));
}
}Using in API Resources
Combine dengan Laravel API Resources untuk complex data transformations.
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;
use App\Http\Resources\UserResource;
class UserController extends Controller
{
use JsonResponseTrait;
/**
* Option 1: API Resource + returnAdaptive
*/
public function index()
{
$users = User::with('posts', 'profile')->paginate(15);
return $this->returnAdaptive(
$users,
mapper: fn($user) => new UserResource($user)
);
}
/**
* Option 2: API Resource collection directly
*/
public function indexWithCollection()
{
$users = User::paginate(15);
// Transform with Resource, then wrap in returnAdaptive
$transformed = UserResource::collection($users);
return $this->returnAdaptive($transformed);
}
/**
* Option 3: Manual mapping (simple cases)
*/
public function indexSimple()
{
return $this->returnAdaptive(
User::paginate(15),
mapper: fn($u) => [
'id' => $u->id,
'name' => $u->name,
'email' => $u->email,
]
);
}
}UserResource Example:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts_count' => $this->posts_count,
'profile' => [
'avatar' => $this->profile?->avatar_url,
'bio' => $this->profile?->bio,
],
'created_at' => $this->created_at->toIso8601String(),
];
}
}Best Practices
Error Handling Pattern
Consistent error handling across application:
<?php
namespace App\Http\Controllers;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
class BaseApiController extends Controller
{
use JsonResponseTrait;
/**
* Handle common exceptions
*/
protected function handleException(\Exception $e)
{
if ($e instanceof ModelNotFoundException) {
return $this->returnJson([], 404, 'Resource not found');
}
if ($e instanceof ValidationException) {
return $this->returnJson(
[],
422,
'Validation failed',
validationErrors: $e->errors()
);
}
// Default: 500 error
return $this->returnJson([], 500, 'Internal server error');
}
}Usage:
public function show($id)
{
try {
$user = User::findOrFail($id);
return $this->returnJson($user);
} catch (\Exception $e) {
return $this->handleException($e);
}
}Consistent Response Pattern Across API
Establish team conventions:
/**
* Team Convention Example
*/
// ✅ DO: Use returnAdaptive for lists
public function index()
{
return $this->returnAdaptive(Product::paginate(15));
}
// ✅ DO: Use returnJson for single resources
public function show($id)
{
return $this->returnJson(Product::find($id));
}
// ✅ DO: Use returnJson for errors
public function destroy($id)
{
$product = Product::find($id);
if (!$product) {
return $this->returnJson([], 404);
}
$product->delete();
return $this->returnJson([], 200, 'Deleted successfully');
}
// ✅ DO: Use mapper for transformations
public function index()
{
return $this->returnAdaptive(
User::paginate(15),
mapper: [$this, 'transformUser']
);
}
private function transformUser($user)
{
return [
'id' => $user->id,
'name' => $user->name,
];
}When to Use Adaptive vs Explicit
Use returnAdaptive() by default:
// ✅ General CRUD endpoints
public function index()
{
return $this->returnAdaptive(Product::paginate(15));
}
// ✅ Dynamic queries
public function search(Request $request)
{
$query = Product::query();
if ($request->has('filter')) {
$query->where(...);
}
$results = $request->boolean('paginate')
? $query->paginate(15)
: $query->get();
return $this->returnAdaptive($results);
}Use explicit methods when:
// ✅ Documented API contracts
/**
* @OA\Get(
* path="/api/products",
* @OA\Response(response=200, description="LengthAware pagination")
* )
*/
public function index()
{
return $this->returnPaginateJson(Product::paginate(15));
}
// ✅ Performance critical (high traffic)
public function feed()
{
// Direct call, no type checking
return $this->returnCursorPaginateJson(
Post::latest()->cursorPaginate(20)
);
}
// ✅ Team convention prefers explicit
public function index()
{
// Clear, explicit, type-safe
return $this->returnPaginateJson(User::paginate(10));
}