marko/api
Transform entities into consistent JSON API responses --- define resource classes once and use them everywhere in your API controllers. Resource classes map your domain entities to JSON output, keeping serialization logic out of controllers and models. Every response wraps its payload in a data key for consistency. Collections add a meta key automatically when pagination is attached. Conditional fields let you include or omit data based on runtime context without cluttering your toArray() logic.
Installation
Section titled “Installation”composer require marko/apiDefining a Resource
Section titled “Defining a Resource”Extend JsonResource and implement toArray() to define the field mapping:
use Marko\Api\Resource\JsonResource;
class PostResource extends JsonResource{ public function toArray(): array { return [ 'title' => $this->resource->title, 'slug' => $this->resource->slug, 'body' => $this->resource->body, 'author' => $this->resource->author, ]; }}Using a Resource in a Controller
Section titled “Using a Resource in a Controller”Call toResponse() to get a Response with the data wrapped in {"data": {...}}:
use Marko\Routing\Attributes\Get;use Marko\Routing\Http\Response;
class PostController{ public function __construct( private PostRepository $postRepository, ) {}
#[Get('/posts/{slug}')] public function show( string $slug, ): Response { $post = $this->postRepository->findBySlug($slug);
return new PostResource($post)->toResponse(); }}The response body:
{ "data": { "title": "Hello World", "slug": "hello-world", "body": "...", "author": "Jane" }}Resource Collections with Pagination
Section titled “Resource Collections with Pagination”Pass an array of items and the resource class to ResourceCollection. Chain withPagination() to append pagination meta automatically:
use Marko\Api\Resource\ResourceCollection;use Marko\Routing\Attributes\Get;use Marko\Routing\Http\Response;
class PostController{ public function __construct( private PostRepository $postRepository, ) {}
#[Get('/posts')] public function index(): Response { $paginator = $this->postRepository->paginate(perPage: 15);
return new ResourceCollection( $paginator->items(), PostResource::class, ) ->withPagination($paginator) ->toResponse(); }}The response body:
{ "data": [ { "title": "Hello World", "slug": "hello-world", "body": "...", "author": "Jane" } ], "meta": { "page": 1, "per_page": 15, "total": 42, "total_pages": 3 }}Use additional() to merge extra keys into meta:
use Marko\Api\Resource\ResourceCollection;
return new ResourceCollection($items, PostResource::class) ->withPagination($paginator) ->additional(['category' => 'news']) ->toResponse();Conditional Fields
Section titled “Conditional Fields”Use when() to include a field only when a condition is true. When false, the field is omitted entirely from the response:
use Marko\Api\Resource\JsonResource;
class PostResource extends JsonResource{ public function toArray(): array { return [ 'title' => $this->resource->title, 'slug' => $this->resource->slug, 'body' => $this->resource->body, 'author' => $this->resource->author, 'edit_url' => $this->when( $this->resource->isEditable, '/posts/' . $this->resource->slug . '/edit', ), ]; }}When isEditable is false, edit_url does not appear in the output at all.
Omitting Fields with MissingValue
Section titled “Omitting Fields with MissingValue”Use missing() as a sentinel to unconditionally exclude a field. This is useful when building resources dynamically:
use Marko\Api\Resource\JsonResource;
class PostResource extends JsonResource{ public function toArray(): array { return [ 'title' => $this->resource->title, 'slug' => $this->resource->slug, 'internal' => $this->missing(), ]; }}internal is always omitted from the JSON output.
Customization
Section titled “Customization”To override the resource response format application-wide, use a Preference to extend JsonResource or ResourceCollection:
use Marko\Core\Attributes\Preference;use Marko\Api\Resource\JsonResource;use Marko\Routing\Http\Response;
#[Preference(replaces: JsonResource::class)]class WrappedJsonResource extends JsonResource{ public function toResponse(): Response { return Response::json([ 'data' => $this->filterArray($this->toArray()), 'version' => '1.0', ]); }}API Reference
Section titled “API Reference”JsonResource (abstract)
Section titled “JsonResource (abstract)”use Marko\Api\Contracts\ResourceInterface;use Marko\Api\Value\ConditionalValue;use Marko\Api\Value\MissingValue;use Marko\Routing\Http\Response;
abstract class JsonResource implements ResourceInterface{ public function __construct(public readonly mixed $resource); abstract public function toArray(): array; public function toResponse(): Response; protected function when(bool $condition, mixed $value): ConditionalValue; protected function missing(): MissingValue;}ResourceCollection
Section titled “ResourceCollection”use Marko\Api\Contracts\ResourceCollectionInterface;use Marko\Pagination\Contracts\PaginatorInterface;use Marko\Routing\Http\Response;
class ResourceCollection implements ResourceCollectionInterface{ public function __construct(array $items, string $resourceClass); public function toArray(): array; public function toResponse(): Response; public function withPagination(PaginatorInterface $paginator): static; public function additional(array $meta): static;}ResourceInterface
Section titled “ResourceInterface”use Marko\Routing\Http\Response;
interface ResourceInterface{ public function toArray(): array; public function toResponse(): Response;}ResourceCollectionInterface
Section titled “ResourceCollectionInterface”use Marko\Pagination\Contracts\PaginatorInterface;use Marko\Routing\Http\Response;
interface ResourceCollectionInterface{ public function toArray(): array; public function toResponse(): Response; public function withPagination(PaginatorInterface $paginator): static;}ConditionalValue
Section titled “ConditionalValue”class ConditionalValue{ public function __construct(public readonly bool $condition, public readonly mixed $value); public function resolve(): mixed;}MissingValue
Section titled “MissingValue”class MissingValue {}ApiResourceException
Section titled “ApiResourceException”use Marko\Api\Exceptions\ApiResourceException;
class ApiResourceException extends Exception{ public function __construct(string $message, string $context = '', string $suggestion = '', ...); public function getContext(): string; public function getSuggestion(): string;}