Skip to content

Repository files navigation

chubbyphp-api

CICoverage StatusMutation testing badgeLatest Stable VersionTotal DownloadsMonthly Downloads

bugscode_smellscoverageduplicated_lines_densitynclocsqale_ratingalert_statusreliability_ratingsecurity_ratingsqale_indexvulnerabilities

Description

A set of CRUD middleware and request handlers for building APIs with PSR-15.

Requirements

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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Model;
useChubbyphp\Api\Model\ModelInterface;
useRamsey\Uuid\Uuid;
finalclass Pet implements ModelInterface
{
privatestring$id;
private\DateTimeInterface$createdAt;
private ?\DateTimeInterface$updatedAt = null;
private ?string$name = null;
private ?string$tag = null;
publicfunction__construct()
{
$this->id = Uuid::uuid4()->toString();
$this->createdAt = new \DateTimeImmutable();
}
publicfunctiongetId(): string { return$this->id; }
publicfunctiongetCreatedAt(): \DateTimeInterface { return$this->createdAt; }
publicfunctionsetUpdatedAt(\DateTimeInterface$updatedAt): void { $this->updatedAt = $updatedAt; }
publicfunctiongetUpdatedAt(): ?\DateTimeInterface { return$this->updatedAt; }
publicfunctionsetName(string$name): void { $this->name = $name; }
publicfunctiongetName(): ?string { return$this->name; }
publicfunctionsetTag(?string$tag): void { $this->tag = $tag; }
publicfunctiongetTag(): ?string { return$this->tag; }
publicfunctionjsonSerialize(): 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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Collection;
useChubbyphp\Api\Collection\AbstractCollection;
finalclass 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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Model;
useApp\Pet\Model\Pet;
useChubbyphp\Api\Dto\Model\ModelRequestInterface;
useChubbyphp\Api\Model\ModelInterface;
finalreadonlyclass PetRequest implements ModelRequestInterface
{
publicfunction__construct(
publicstring$name,
public ?string$tag,
) {}
publicfunctioncreateModel(): ModelInterface
{
$model = newPet();
$model->setName($this->name);
$model->setTag($this->tag);
return$model;
}
publicfunctionupdateModel(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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Model;
useChubbyphp\Api\Dto\Model\ModelResponseInterface;
finalreadonlyclass PetResponse implements ModelResponseInterface
{
publicfunction__construct(
publicstring$id,
publicstring$createdAt,
public ?string$updatedAt,
publicstring$name,
public ?string$tag,
publicstring$_type,
publicarray$_links = [],
) {}
publicfunctionjsonSerialize(): 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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Collection;
useApp\Pet\Collection\PetCollection;
useChubbyphp\Api\Collection\CollectionInterface;
useChubbyphp\Api\Dto\Collection\CollectionRequestInterface;
finalreadonlyclass PetCollectionRequest implements CollectionRequestInterface
{
publicfunction__construct(
publicint$offset,
publicint$limit,
publicPetCollectionFilters$filters,
publicPetCollectionSort$sort
) {}
publicfunctioncreateCollection(): CollectionInterface
{
$collection = newPetCollection();
$collection->setOffset($this->offset);
$collection->setLimit($this->limit);
$collection->setFilters((array) $this->filters);
$collection->setSort((array) $this->sort);
return$collection;
}
}

Collection Response

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Collection;
useChubbyphp\Api\Dto\Collection\CollectionFiltersInterface;
finalreadonlyclass PetCollectionFilters implements CollectionFiltersInterface
{
publicfunction__construct(public ?string$name = null) {}
publicfunctionjsonSerialize(): array
{
return ['name' => $this->name];
}
}
<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Collection;
useChubbyphp\Api\Dto\Collection\CollectionSortInterface;
finalreadonlyclass PetCollectionSort implements CollectionSortInterface
{
publicfunction__construct(public ?string$name = null) {}
publicfunctionjsonSerialize(): array
{
return ['name' => $this->name];
}
}

Extend AbstractReadonlyCollectionResponse for paginated API responses.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Dto\Collection;
useApp\Pet\Dto\Model\PetResponse;
useChubbyphp\Api\Dto\Collection\AbstractReadonlyCollectionResponse;
finalreadonlyclass PetCollectionResponse extends AbstractCollectionResponse
{
publicfunction__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.

<?phpdeclare(strict_types=1);
namespaceApp\Pet\Parsing;
useApp\Pet\Dto\Collection\PetCollectionFilters;
useApp\Pet\Dto\Collection\PetCollectionRequest;
useApp\Pet\Dto\Collection\PetCollectionResponse;
useApp\Pet\Dto\Collection\PetCollectionSort;
useApp\Pet\Dto\Model\PetRequest;
useApp\Pet\Dto\Model\PetResponse;
useChubbyphp\Api\Collection\CollectionInterface;
useChubbyphp\Api\Parsing\ParsingInterface;
useChubbyphp\Framework\Router\UrlGeneratorInterface;
useChubbyphp\Parsing\Enum\Uuid;
useChubbyphp\Parsing\ParserInterface;
useChubbyphp\Parsing\Schema\ObjectSchemaInterface;
usePsr\Http\Message\ServerRequestInterface;
finalclass PetParsing implements ParsingInterface
{
private ?ObjectSchemaInterface$collectionRequestSchema = null;
private ?ObjectSchemaInterface$collectionResponseSchema = null;
private ?ObjectSchemaInterface$modelRequestSchema = null;
private ?ObjectSchemaInterface$modelResponseSchema = null;
publicfunction__construct(
privatereadonlyParserInterface$parser,
privatereadonlyUrlGeneratorInterface$urlGenerator,
) {}
publicfunctiongetCollectionRequestSchema(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;
}
publicfunctiongetCollectionResponseSchema(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(),
];
returnnewPetCollectionResponse(
$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;
}
publicfunctiongetModelRequestSchema(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;
}
publicfunctiongetModelResponseSchema(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) => newPetResponse(
$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.).

<?phpuseChubbyphp\Api\Collection\CollectionInterface;
useChubbyphp\Api\Model\ModelInterface;
useChubbyphp\Api\Repository\RepositoryInterface;
interface RepositoryInterface
{
publicfunctionresolveCollection(CollectionInterface$collection): void;
publicfunctionfindById(string$id): ?ModelInterface;
publicfunctionpersist(ModelInterface$model): void;
publicfunctionremove(ModelInterface$model): void;
publicfunctionflush(): void;
}

Request Handlers

The library provides PSR-15 request handlers for CRUD operations:

HandlerDescription
ListRequestHandlerList collections with pagination, filtering, and sorting
CreateRequestHandlerCreate new models (returns 201)
ReadRequestHandlerRead single models by ID
UpdateRequestHandlerUpdate existing models
DeleteRequestHandlerDelete models (returns 204)

All handlers use content negotiation via accept and contentType request attributes.

Copyright

2026 Dominik Zogg

About

A set of CRUD middleware and request handlers for building APIs with PSR-15.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages