chubbyphp / chubbyphp-api
A set of CRUD middleware and request handlers for building APIs with PSR-15.
Requires
- php: ^8.3
- chubbyphp/chubbyphp-decode-encode: ^1.4
- chubbyphp/chubbyphp-http-exception: ^1.3.2
- chubbyphp/chubbyphp-parsing: ^2.2
- psr/container: ^1.1.2|^2.0.2
- psr/http-message: ^1.1|^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
- ramsey/uuid: ^4.9.2
Requires (Dev)
- chubbyphp/chubbyphp-dev-helper: dev-master
- chubbyphp/chubbyphp-mock: ^2.1.2
- infection/infection: ^0.32.3
- php-coveralls/php-coveralls: ^2.9.1
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.1.33
- phpunit/phpunit: ^12.5.6
This package is auto-updated.
Last update: 2026-03-22 17:44:56 UTC
README
Description
A set of CRUD middleware and request handlers for building APIs with PSR-15.
Requirements
- php: ^8.3
- chubbyphp/chubbyphp-decode-encode: ^1.4
- chubbyphp/chubbyphp-http-exception: ^1.3.2
- chubbyphp/chubbyphp-parsing: ^2.2
- psr/container: ^1.1.2|^2.0.2
- psr/http-message: ^1.1|^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
- ramsey/uuid: ^4.9.2
Installation
Through Composer as chubbyphp/chubbyphp-api.
composer require chubbyphp/chubbyphp-api "^1.1"
Usage
Model
Implement ModelInterface for your domain models. Models must provide an ID, timestamps, and JSON serialization.
<?php declare(strict_types=1); namespace App\Pet\Model; use Chubbyphp\Api\Model\ModelInterface; use Ramsey\Uuid\Uuid; final class Pet implements ModelInterface { private string $id; private \DateTimeInterface $createdAt; private ?\DateTimeInterface $updatedAt = null; private ?string $name = null; private ?string $tag = null; public function __construct() { $this->id = Uuid::uuid4()->toString(); $this->createdAt = new \DateTimeImmutable(); } public function getId(): string { return $this->id; } public function getCreatedAt(): \DateTimeInterface { return $this->createdAt; } public function setUpdatedAt(\DateTimeInterface $updatedAt): void { $this->updatedAt = $updatedAt; } public function getUpdatedAt(): ?\DateTimeInterface { return $this->updatedAt; } public function setName(string $name): void { $this->name = $name; } public function getName(): ?string { return $this->name; } public function setTag(?string $tag): void { $this->tag = $tag; } public function getTag(): ?string { return $this->tag; } public function jsonSerialize(): array { return [ 'id' => $this->id, 'createdAt' => $this->createdAt, 'updatedAt' => $this->updatedAt, 'name' => $this->name, 'tag' => $this->tag, ]; } }
Collection
Extend AbstractCollection for paginated lists of models with filtering and sorting support.
<?php declare(strict_types=1); namespace App\Pet\Collection; use Chubbyphp\Api\Collection\AbstractCollection; final class PetCollection extends AbstractCollection {}
The abstract class provides: offset, limit, filters, sort, count, and items.
DTOs
Data Transfer Objects for request/response transformations.
Model Request
Implement ModelRequestInterface to handle create and update operations.
<?php declare(strict_types=1); namespace App\Pet\Dto\Model; use App\Pet\Model\Pet; use Chubbyphp\Api\Dto\Model\ModelRequestInterface; use Chubbyphp\Api\Model\ModelInterface; final readonly class PetRequest implements ModelRequestInterface { public function __construct( public string $name, public ?string $tag, ) {} public function createModel(): ModelInterface { $model = new Pet(); $model->setName($this->name); $model->setTag($this->tag); return $model; } public function updateModel(ModelInterface $model): ModelInterface { $model->setUpdatedAt(new \DateTimeImmutable()); $model->setName($this->name); $model->setTag($this->tag); return $model; } }
Model Response
Implement ModelResponseInterface for API responses with HATEOAS links.
<?php declare(strict_types=1); namespace App\Pet\Dto\Model; use Chubbyphp\Api\Dto\Model\ModelResponseInterface; final readonly class PetResponse implements ModelResponseInterface { public function __construct( public string $id, public string $createdAt, public ?string $updatedAt, public string $name, public ?string $tag, public string $_type, public array $_links = [], ) {} public function jsonSerialize(): array { return [ 'id' => $this->id, 'createdAt' => $this->createdAt, 'updatedAt' => $this->updatedAt, 'name' => $this->name, 'tag' => $this->tag, '_type' => $this->_type, '_links' => $this->_links, ]; } }
Collection Request
Implement CollectionRequestInterface with filter and sort classes.
<?php declare(strict_types=1); namespace App\Pet\Dto\Collection; use App\Pet\Collection\PetCollection; use Chubbyphp\Api\Collection\CollectionInterface; use Chubbyphp\Api\Dto\Collection\CollectionRequestInterface; final readonly class PetCollectionRequest implements CollectionRequestInterface { public function __construct( public int $offset, public int $limit, public PetCollectionFilters $filters, public PetCollectionSort $sort ) {} public function createCollection(): CollectionInterface { $collection = new PetCollection(); $collection->setOffset($this->offset); $collection->setLimit($this->limit); $collection->setFilters((array) $this->filters); $collection->setSort((array) $this->sort); return $collection; } }
Collection Response
<?php declare(strict_types=1); namespace App\Pet\Dto\Collection; use Chubbyphp\Api\Dto\Collection\CollectionFiltersInterface; final readonly class PetCollectionFilters implements CollectionFiltersInterface { public function __construct(public ?string $name = null) {} public function jsonSerialize(): array { return ['name' => $this->name]; } }
<?php declare(strict_types=1); namespace App\Pet\Dto\Collection; use Chubbyphp\Api\Dto\Collection\CollectionSortInterface; final readonly class PetCollectionSort implements CollectionSortInterface { public function __construct(public ?string $name = null) {} public function jsonSerialize(): array { return ['name' => $this->name]; } }
Extend AbstractReadonlyCollectionResponse for paginated API responses.
<?php declare(strict_types=1); namespace App\Pet\Dto\Collection; use App\Pet\Dto\Model\PetResponse; use Chubbyphp\Api\Dto\Collection\AbstractReadonlyCollectionResponse; final readonly class PetCollectionResponse extends AbstractCollectionResponse { public function __construct( int $offset, int $limit, PetCollectionFilters $filters, PetCollectionSort $sort, array $items, int $count, string $_type, array $_links = [], ) { parent::__construct( $offset, $limit, $filters, $sort, $items, $count, $_type, $_links, ); } }
Parsing
Implement ParsingInterface to define schemas for request/response transformation using chubbyphp/chubbyphp-parsing.
<?php declare(strict_types=1); namespace App\Pet\Parsing; use App\Pet\Dto\Collection\PetCollectionFilters; use App\Pet\Dto\Collection\PetCollectionRequest; use App\Pet\Dto\Collection\PetCollectionResponse; use App\Pet\Dto\Collection\PetCollectionSort; use App\Pet\Dto\Model\PetRequest; use App\Pet\Dto\Model\PetResponse; use Chubbyphp\Api\Collection\CollectionInterface; use Chubbyphp\Api\Parsing\ParsingInterface; use Chubbyphp\Framework\Router\UrlGeneratorInterface; use Chubbyphp\Parsing\Enum\Uuid; use Chubbyphp\Parsing\ParserInterface; use Chubbyphp\Parsing\Schema\ObjectSchemaInterface; use Psr\Http\Message\ServerRequestInterface; final class PetParsing implements ParsingInterface { private ?ObjectSchemaInterface $collectionRequestSchema = null; private ?ObjectSchemaInterface $collectionResponseSchema = null; private ?ObjectSchemaInterface $modelRequestSchema = null; private ?ObjectSchemaInterface $modelResponseSchema = null; public function __construct( private readonly ParserInterface $parser, private readonly UrlGeneratorInterface $urlGenerator, ) {} public function getCollectionRequestSchema(ServerRequestInterface $request): ObjectSchemaInterface { if (null === $this->collectionRequestSchema) { $p = $this->parser; $this->collectionRequestSchema = $p->object([ 'offset' => $p->union([$p->string()->toInt(), $p->int()->default(0)]), 'limit' => $p->union([ $p->string()->toInt(), $p->int()->default(CollectionInterface::LIMIT), ]), 'filters' => $p->object([ 'name' => $p->string()->nullable()->default(null), ], PetCollectionFilters::class, true)->strict()->default([]), 'sort' => $p->object([ 'name' => $p->union([ $p->const('asc'), $p->const('desc'), ])->nullable()->default(null), ], PetCollectionSort::class, true)->strict()->default([]), ], PetCollectionRequest::class, true)->strict(); } return $this->collectionRequestSchema; } public function getCollectionResponseSchema(ServerRequestInterface $request): ObjectSchemaInterface { if (null === $this->collectionResponseSchema) { $p = $this->parser; $this->collectionResponseSchema = $p->object([ 'offset' => $p->int(), 'limit' => $p->int(), 'filters' => $p->object([ 'name' => $p->string()->nullable(), ], PetCollectionFilters::class, true)->strict(), 'sort' => $p->object([ 'name' => $p->union([ $p->const('asc'), $p->const('desc'), ])->nullable()->default(null), ], PetCollectionSort::class, true)->strict(), 'items' => $p->array($this->getModelResponseSchema($request)), 'count' => $p->int(), '_type' => $p->const('petCollection')->default('petCollection'), ], PetCollectionResponse::class, true) ->strict() ->postParse(function (PetCollectionResponse $petCollectionResponse) { $queryParams = [ 'offset' => $petCollectionResponse->offset, 'limit' => $petCollectionResponse->limit, 'filters' => $petCollectionResponse->filters->jsonSerialize(), 'sort' => $petCollectionResponse->sort->jsonSerialize(), ]; return new PetCollectionResponse( $petCollectionResponse->offset, $petCollectionResponse->limit, $petCollectionResponse->filters, $petCollectionResponse->sort, $petCollectionResponse->items, $petCollectionResponse->count, $petCollectionResponse->_type, [ 'list' => [ 'href' => $this->urlGenerator->generatePath('pet_list', [], $queryParams), 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'GET'], ], 'create' => [ 'href' => $this->urlGenerator->generatePath('pet_create'), 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'POST'], ], ], ); }) ; } return $this->collectionResponseSchema; } public function getModelRequestSchema(ServerRequestInterface $request): ObjectSchemaInterface { if (null === $this->modelRequestSchema) { $p = $this->parser; $this->modelRequestSchema = $p->object([ 'name' => $p->string()->minLength(1), 'tag' => $p->string()->minLength(1)->nullable(), ], PetRequest::class, true)->strict(['id', 'createdAt', 'updatedAt', '_type', '_links']); } return $this->modelRequestSchema; } public function getModelResponseSchema(ServerRequestInterface $request): ObjectSchemaInterface { if (null === $this->modelResponseSchema) { $p = $this->parser; $this->modelResponseSchema = $p->object([ 'id' => $p->string()->uuid(Uuid::v7), 'createdAt' => $p->dateTime()->toString(), 'updatedAt' => $p->dateTime()->nullable()->toString(), 'name' => $p->string(), 'tag' => $p->string()->nullable(), '_type' => $p->const('pet')->default('pet'), ], PetResponse::class, true)->strict() ->postParse( fn (PetResponse $petResponse) => new PetResponse( $petResponse->id, $petResponse->createdAt, $petResponse->updatedAt, $petResponse->name, $petResponse->tag, $petResponse->_type, [ 'read' => [ 'href' => $this->urlGenerator->generatePath('pet_read', ['id' => $petResponse->id]), 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'GET'], ], 'update' => [ 'href' => $this->urlGenerator->generatePath('pet_update', ['id' => $petResponse->id]), 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'PUT'], ], 'delete' => [ 'href' => $this->urlGenerator->generatePath('pet_delete', ['id' => $petResponse->id]), 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'DELETE'], ], ] ) ) ; } return $this->modelResponseSchema; } }
Repository
Implement RepositoryInterface for your persistence layer (Doctrine ORM, ODM, etc.).
<?php use Chubbyphp\Api\Collection\CollectionInterface; use Chubbyphp\Api\Model\ModelInterface; use Chubbyphp\Api\Repository\RepositoryInterface; interface RepositoryInterface { public function resolveCollection(CollectionInterface $collection): void; public function findById(string $id): ?ModelInterface; public function persist(ModelInterface $model): void; public function remove(ModelInterface $model): void; public function flush(): void; }
Request Handlers
The library provides PSR-15 request handlers for CRUD operations:
| Handler | Description |
|---|---|
ListRequestHandler |
List collections with pagination, filtering, and sorting |
CreateRequestHandler |
Create new models (returns 201) |
ReadRequestHandler |
Read single models by ID |
UpdateRequestHandler |
Update existing models |
DeleteRequestHandler |
Delete models (returns 204) |
All handlers use content negotiation via accept and contentType request attributes.
Copyright
2026 Dominik Zogg