Skip to content

JSON Resources

JSON Resources provide a transformation layer between your Eloquent models and the JSON responses sent to your application's API consumers. They allow you to control exactly how your models and model collections are serialized to JSON.

Introduction

Resources sit between your models and controllers, allowing you to transform your models into JSON in a consistent, reusable way. This is especially useful for APIs where you need to control what data is exposed and how it's formatted.

Basic Resources

Creating Resources

Create resource classes manually in your app/Http/Resources directory:

php
<?php

namespace App\Http\Resources;

use AvelPress\Http\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Using Resources

Use resources in your controllers:

php
use App\Http\Resources\UserResource;
use App\Models\User;

class UserController extends Controller
{
    public function show($request)
    {
        $user = User::find($request->get_param('id'));
        return new UserResource($user);
    }

    public function index()
    {
        $users = User::all();
        return UserResource::collection($users);
    }
}

Resource Collections

Automatic Collections

Use the collection method to transform collections of models:

php
public function index()
{
    $users = User::all();
    
    // This automatically wraps each user in a UserResource
    return UserResource::collection($users);
}

Custom Collection Classes

For more control over collections, create dedicated collection classes:

php
<?php

namespace App\Http\Resources;

use AvelPress\Http\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     */
    public function toArray(): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->collection->count(),
                'fetched_at' => current_time('mysql'),
            ],
        ];
    }
}

Using custom collections:

php
public function index()
{
    $users = User::all();
    return new UserCollection($users);
}

Data Transformation

Basic Transformations

Transform model attributes in various ways:

php
class PostResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'content' => wp_trim_words($this->content, 50), // Excerpt
            'status' => ucfirst($this->status), // Capitalize
            'published_at' => $this->created_at,
            'author' => [
                'id' => $this->author_id,
                'name' => $this->author_name,
            ],
            'url' => home_url("/posts/{$this->slug}"),
            'edit_url' => admin_url("post.php?post={$this->id}&action=edit"),
        ];
    }
}

Conditional Attributes

Include attributes conditionally:

php
class UserResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            
            // Only include admin fields if user is admin
            'role' => $this->when(current_user_can('manage_options'), $this->role),
            'permissions' => $this->when(current_user_can('manage_options'), $this->permissions),
            
            // Include sensitive data only for the user themselves
            'phone' => $this->when($this->id === get_current_user_id(), $this->phone),
            'address' => $this->when($this->id === get_current_user_id(), $this->address),
            
            'created_at' => $this->created_at,
        ];
    }
}

Computed Properties

Add computed properties that don't exist on the model:

php
class ProductResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'sale_price' => $this->sale_price,
            
            // Computed properties
            'is_on_sale' => $this->sale_price > 0 && $this->sale_price < $this->price,
            'discount_percentage' => $this->calculateDiscountPercentage(),
            'price_formatted' => wc_price($this->price),
            'availability' => $this->stock_quantity > 0 ? 'in_stock' : 'out_of_stock',
            'rating_average' => $this->reviews()->avg('rating'),
            'total_sales' => $this->getTotalSales(),
        ];
    }

    private function calculateDiscountPercentage()
    {
        if ($this->sale_price > 0 && $this->price > 0) {
            return round((($this->price - $this->sale_price) / $this->price) * 100, 2);
        }
        return 0;
    }
}

Relationships

Transform related models using their own resources:

php
class PostResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            
            // Include author as a resource
            'author' => new UserResource($this->author),
            
            // Include category as a resource
            'category' => new CategoryResource($this->category),
            
            // Include comments collection
            'comments' => CommentResource::collection($this->comments),
            
            // Include tags
            'tags' => TagResource::collection($this->tags),
        ];
    }
}

Conditional Relationships

Load relationships conditionally to avoid N+1 queries:

php
class PostResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            
            // Only include author if loaded
            'author' => $this->whenLoaded('author', function () {
                return new UserResource($this->author);
            }),
            
            // Include comments only if requested
            'comments' => $this->when(
                request()->get('include_comments'),
                CommentResource::collection($this->comments)
            ),
            
            // Include metadata only for authenticated users
            'meta' => $this->when(is_user_logged_in(), [
                'views' => $this->view_count,
                'likes' => $this->like_count,
            ]),
        ];
    }
}

WordPress Integration

WordPress-Specific Resources

Create resources that work with WordPress data:

php
class WPPostResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->ID,
            'title' => $this->post_title,
            'content' => apply_filters('the_content', $this->post_content),
            'excerpt' => get_the_excerpt($this->ID),
            'slug' => $this->post_name,
            'status' => $this->post_status,
            'type' => $this->post_type,
            'published_at' => $this->post_date,
            'modified_at' => $this->post_modified,
            
            // WordPress-specific data
            'permalink' => get_permalink($this->ID),
            'edit_link' => get_edit_post_link($this->ID),
            'featured_image' => $this->getFeaturedImage(),
            'author' => $this->getAuthorData(),
            'categories' => $this->getCategories(),
            'tags' => $this->getTags(),
            'meta' => $this->getMetaData(),
        ];
    }

    private function getFeaturedImage()
    {
        $thumbnail_id = get_post_thumbnail_id($this->ID);
        if ($thumbnail_id) {
            return [
                'id' => $thumbnail_id,
                'url' => wp_get_attachment_image_url($thumbnail_id, 'full'),
                'thumbnail' => wp_get_attachment_image_url($thumbnail_id, 'thumbnail'),
                'alt' => get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true),
            ];
        }
        return null;
    }

    private function getAuthorData()
    {
        $author = get_user_by('id', $this->post_author);
        return [
            'id' => $author->ID,
            'name' => $author->display_name,
            'avatar' => get_avatar_url($author->ID),
            'bio' => get_user_meta($author->ID, 'description', true),
        ];
    }
}

Custom Field Resources

Handle ACF or custom fields:

php
class PageResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            
            // ACF fields
            'hero_section' => $this->getHeroSection(),
            'features' => $this->getFeatures(),
            'testimonials' => $this->getTestimonials(),
            
            // Custom meta
            'seo' => $this->getSeoData(),
        ];
    }

    private function getHeroSection()
    {
        return [
            'title' => get_field('hero_title', $this->id),
            'subtitle' => get_field('hero_subtitle', $this->id),
            'background_image' => $this->getImageField('hero_background'),
            'cta_button' => get_field('hero_cta', $this->id),
        ];
    }

    private function getImageField($field_name)
    {
        $image = get_field($field_name, $this->id);
        if ($image) {
            return [
                'url' => $image['url'],
                'alt' => $image['alt'],
                'width' => $image['width'],
                'height' => $image['height'],
            ];
        }
        return null;
    }
}

Real-World Examples

Quote Resource (from Infixs Mega ERP)

php
<?php

namespace Infixs\MegaErp\App\Modules\Quote\Http\Resources;

use AvelPress\Http\Json\JsonResource;

class QuoteResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'customer' => [
                'id' => $this->customer_id,
                'name' => $this->customer_name,
                'email' => $this->customer_email,
                'phone' => $this->customer_phone,
                'type' => $this->customer_type,
                'document' => $this->customer_document,
            ],
            'items' => QuoteItemResource::collection($this->whenLoaded('items')),
            'payment_methods' => QuotePaymentMethodResource::collection($this->whenLoaded('paymentMethods')),
            'totals' => [
                'subtotal' => $this->calculateSubtotal(),
                'discount' => $this->calculateDiscount(),
                'total' => $this->calculateTotal(),
            ],
            'status' => $this->status,
            'notes' => $this->notes,
            'valid_until' => $this->valid_until,
            'created_at' => $this->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
            
            // Links
            'links' => [
                'self' => "/wp-json/infixs-mega-erp/v1/quotes/{$this->id}",
                'pdf' => "/wp-json/infixs-mega-erp/v1/quotes/{$this->id}/pdf",
                'edit' => admin_url("admin.php?page=quote-edit&id={$this->id}"),
            ],
        ];
    }

    private function calculateSubtotal()
    {
        return $this->items->sum(function ($item) {
            return $item->quantity * $item->price;
        });
    }

    private function calculateDiscount()
    {
        return $this->items->sum(function ($item) {
            return $item->discount;
        });
    }

    private function calculateTotal()
    {
        return $this->calculateSubtotal() - $this->calculateDiscount();
    }
}

Product Resource with WooCommerce Integration

php
class ProductResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->description,
            'short_description' => $this->short_description,
            
            // Pricing
            'price' => $this->price,
            'regular_price' => $this->regular_price,
            'sale_price' => $this->sale_price,
            'price_formatted' => wc_price($this->price),
            'on_sale' => $this->is_on_sale(),
            
            // Stock
            'sku' => $this->sku,
            'stock_quantity' => $this->stock_quantity,
            'stock_status' => $this->stock_status,
            'manage_stock' => $this->manage_stock,
            'in_stock' => $this->is_in_stock(),
            
            // Images
            'images' => $this->getProductImages(),
            'featured_image' => $this->getFeaturedImage(),
            
            // Categories and tags
            'categories' => $this->getProductCategories(),
            'tags' => $this->getProductTags(),
            
            // Attributes
            'attributes' => $this->getProductAttributes(),
            'variations' => $this->when($this->is_type('variable'), 
                ProductVariationResource::collection($this->get_available_variations())
            ),
            
            // URLs
            'permalink' => get_permalink($this->id),
            'add_to_cart_url' => $this->add_to_cart_url(),
            
            // Metadata
            'weight' => $this->weight,
            'dimensions' => [
                'length' => $this->length,
                'width' => $this->width,
                'height' => $this->height,
            ],
            'shipping_class' => $this->shipping_class,
            
            'created_at' => $this->date_created->format('Y-m-d H:i:s'),
            'updated_at' => $this->date_modified->format('Y-m-d H:i:s'),
        ];
    }

    private function getProductImages()
    {
        $images = [];
        $attachment_ids = $this->get_gallery_image_ids();
        
        foreach ($attachment_ids as $attachment_id) {
            $images[] = [
                'id' => $attachment_id,
                'src' => wp_get_attachment_image_url($attachment_id, 'full'),
                'thumbnail' => wp_get_attachment_image_url($attachment_id, 'thumbnail'),
                'alt' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
            ];
        }
        
        return $images;
    }
}

Best Practices

1. Keep Resources Focused

php
// Good - focused on what the API consumer needs
class UserResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'avatar' => $this->avatar_url,
        ];
    }
}

// Avoid - exposing internal/sensitive data
class UserResource extends JsonResource
{
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'password' => $this->password, // Never expose!
            'internal_notes' => $this->internal_notes, // Internal only
            // ... all model attributes
        ];
    }
}

2. Use Conditional Loading

php
public function toArray(): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        
        // Only load when explicitly requested
        'author' => $this->whenLoaded('author', function () {
            return new UserResource($this->author);
        }),
        
        // Only include for authenticated users
        'edit_url' => $this->when(is_user_logged_in(), 
            admin_url("post.php?post={$this->id}&action=edit")
        ),
    ];
}

3. Normalize Data Formats

php
public function toArray(): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        
        // Consistent date format
        'created_at' => $this->created_at->format('Y-m-d H:i:s'),
        'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
        
        // Consistent boolean format
        'is_active' => (bool) $this->active,
        'is_featured' => (bool) $this->featured,
        
        // Consistent number format
        'price' => (float) $this->price,
        'quantity' => (int) $this->quantity,
    ];
}

4. Provide Useful Metadata

php
class PostCollection extends ResourceCollection
{
    public function toArray(): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->collection->count(),
                'page' => request()->get('page', 1),
                'per_page' => request()->get('per_page', 15),
                'generated_at' => current_time('mysql'),
                'cache_expires' => current_time('mysql', 1) + 3600, // 1 hour
            ],
            'links' => [
                'self' => request()->url(),
                'first' => $this->generatePageUrl(1),
                'prev' => $this->generatePrevPageUrl(),
                'next' => $this->generateNextPageUrl(),
            ],
        ];
    }
}

JSON Resources in AvelPress provide a powerful way to transform your data into consistent, well-structured API responses while maintaining full control over what data is exposed and how it's formatted.

Released under the MIT License.