chubbyphp / chubbyphp-api
A set of CRUD middleware and request handlers for building APIs with PSR-15.
Installs: 45
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/chubbyphp/chubbyphp-api
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-01-18 19:30:41 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.0"
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 class PetRequest implements ModelRequestInterface { public string $name; public ?string $tag = null; 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 class PetResponse implements ModelResponseInterface { public string $id; public string $createdAt; public ?string $updatedAt = null; public string $name; public ?string $tag = null; 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 class PetCollectionRequest implements CollectionRequestInterface { 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 class PetCollectionFilters implements CollectionFiltersInterface { 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 class PetCollectionSort implements CollectionSortInterface { public ?string $name = null; public function jsonSerialize(): array { return ['name' => $this->name]; } }
Extend AbstractCollectionResponse 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\AbstractCollectionResponse; final class PetCollectionResponse extends AbstractCollectionResponse { public PetCollectionFilters $filters; public PetCollectionSort $sort; public array $items; protected function getFilters(): PetCollectionFilters { return $this->filters; } protected function getSort(): PetCollectionSort { return $this->sort; } }
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, PetCollectionRequest, PetCollectionResponse, PetCollectionSort}; use App\Pet\Dto\Model\{PetRequest, PetResponse}; use Chubbyphp\Api\Collection\CollectionInterface; use Chubbyphp\Api\Parsing\ParsingInterface; use Chubbyphp\Framework\Router\UrlGeneratorInterface; use Chubbyphp\Parsing\ParserInterface; use Chubbyphp\Parsing\Schema\ObjectSchemaInterface; use Psr\Http\Message\ServerRequestInterface; final class PetParsing implements ParsingInterface { public function __construct( private readonly ParserInterface $parser, private readonly UrlGeneratorInterface $urlGenerator, ) {} public function getCollectionRequestSchema(ServerRequestInterface $request): ObjectSchemaInterface { $p = $this->parser; return $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)->strict()->default([]), 'sort' => $p->object([ 'name' => $p->union([$p->literal('asc'), $p->literal('desc')])->nullable()->default(null), ], PetCollectionSort::class)->strict()->default([]), ], PetCollectionRequest::class)->strict(); } public function getCollectionResponseSchema(ServerRequestInterface $request): ObjectSchemaInterface { $p = $this->parser; return $p->object([ 'offset' => $p->int(), 'limit' => $p->int(), 'filters' => $p->object(['name' => $p->string()->nullable()], PetCollectionFilters::class)->strict(), 'sort' => $p->object([ 'name' => $p->union([$p->literal('asc'), $p->literal('desc')])->nullable()->default(null), ], PetCollectionSort::class)->strict(), 'items' => $p->array($this->getModelResponseSchema($request)), 'count' => $p->int(), '_type' => $p->literal('petCollection')->default('petCollection'), ], PetCollectionResponse::class)->strict()->postParse(fn ($r) => $this->addCollectionLinks($r)); } public function getModelRequestSchema(ServerRequestInterface $request): ObjectSchemaInterface { $p = $this->parser; return $p->object([ 'name' => $p->string()->minLength(1), 'tag' => $p->string()->minLength(1)->nullable(), ], PetRequest::class)->strict(['id', 'createdAt', 'updatedAt', '_type', '_links']); } public function getModelResponseSchema(ServerRequestInterface $request): ObjectSchemaInterface { $p = $this->parser; return $p->object([ 'id' => $p->string(), 'createdAt' => $p->dateTime()->toString(), 'updatedAt' => $p->dateTime()->nullable()->toString(), 'name' => $p->string(), 'tag' => $p->string()->nullable(), '_type' => $p->literal('pet')->default('pet'), ], PetResponse::class)->strict()->postParse(fn ($r) => $this->addModelLinks($r)); } private function addCollectionLinks(PetCollectionResponse $response): PetCollectionResponse { $queryParams = [ 'offset' => $response->offset, 'limit' => $response->limit, 'filters' => $response->filters->jsonSerialize(), 'sort' => $response->sort->jsonSerialize(), ]; $response->_links = [ 'list' => $this->link($this->urlGenerator->generatePath('pet_list', [], $queryParams), 'GET'), 'create' => $this->link($this->urlGenerator->generatePath('pet_create'), 'POST'), ]; return $response; } private function addModelLinks(PetResponse $response): PetResponse { $response->_links = [ 'read' => $this->link($this->urlGenerator->generatePath('pet_read', ['id' => $response->id]), 'GET'), 'update' => $this->link($this->urlGenerator->generatePath('pet_update', ['id' => $response->id]), 'PUT'), 'delete' => $this->link($this->urlGenerator->generatePath('pet_delete', ['id' => $response->id]), 'DELETE'), ]; return $response; } private function link(string $href, string $method): array { return ['href' => $href, 'templated' => false, 'rel' => [], 'attributes' => ['method' => $method]]; } }
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