antoi/restify-bundle

Reusable Symfony bundle providing abstract REST CRUD services, entity hydration, and pick-based serialization.

Maintainers

Package info

github.com/antoine1003/restify-bundle

Type:symfony-bundle

pkg:composer/antoi/restify-bundle

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.2 2026-05-05 16:42 UTC

This package is auto-updated.

Last update: 2026-05-05 16:43:13 UTC


README

A reusable Symfony bundle providing a complete REST CRUD stack: abstract service, controller, repository, automatic entity hydration, and pick-based response enrichment.

Compatible with Symfony 6.4, 7.x, and 8.x — requires PHP 8.2+.

Installation

composer require antoi/restify-bundle

If you are not using Symfony Flex, register the bundle manually:

// config/bundles.php
return [
    Antoi\RestifyBundle\RestifyBundle::class => ['all' => true],
];

Full wiring example (User resource)

1. Repository

use Antoi\RestifyBundle\Repository\AbstractRestRepository;

class UserRepository extends AbstractRestRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    // Only these fields may be used as query-param filters
    protected function getFilterableFields(): array
    {
        return ['name', 'email', 'status', 'createdAt'];
    }

    // Eager-load these relations on every list query
    protected function getDefaultJoins(): array
    {
        return ['role'];
    }
}

2. Service

use Antoi\RestifyBundle\Service\AbstractRestService;

class UserService extends AbstractRestService
{
    protected function getEntityClass(): string { return User::class; }

    protected function getWritableFields(): array
    {
        return ['name', 'email', 'role', 'status'];
    }
}

Wire in config/services.yaml (needed because AbstractRestService takes a typed AbstractRestRepository):

App\Service\UserService:
  arguments:
    $repository: '@App\Repository\UserRepository'

3. Controller

use Antoi\RestifyBundle\Controller\AbstractRestController;

#[Route('/api/users')]
class UserController extends AbstractRestController
{
    public function __construct(
        SerializerInterface $serializer,
        ValidatorInterface  $validator,
        PickResolver        $pickResolver,
        UserService         $service,
    ) {
        parent::__construct($serializer, $validator, $pickResolver, $service);
    }

    protected function getReadGroups(): array  { return ['user:read']; }
    protected function getListGroups(): array  { return ['user:list']; }
}

That's it. Six endpoints are registered automatically.

Endpoints

Method Path Action Description
GET / list() Paginated, filterable list
GET /{id} show() Single resource
POST / create() Create + validate + persist
PUT /{id} update() Full replace + validate + persist
PATCH /{id} patch() Partial update + validate + save
DELETE /{id} delete() Remove

Query parameters

Pagination & sorting

GET /api/users?page=2&limit=10&sort=name,-createdAt
  • sort=nameORDER BY name ASC
  • sort=-createdAtORDER BY createdAt DESC
  • Multiple fields: sort=name,-createdAt

Filtering

Any query param not in [page, limit, sort, pick] is forwarded to the repository as a filter.

GET /api/users?status=active&createdAt_gte=2024-01-01&name_like=john

Supported operators (append as suffix):

Suffix SQL equivalent
(none) = :value
_gte >= :value
_gt > :value
_lte <= :value
_lt < :value
_neq != :value
_like LIKE %value%
_in IN (a, b, c)
=null IS NULL

Pick — extra properties outside serialization groups

GET /api/users/42?pick=ip,lastLogin.ip,roles.name

Enriches the serialized response with additional properties resolved from the entity:

  • ip$user->getIp()
  • lastLogin.ip$user->getLastLogin()->getIp() (deep traversal)
  • roles.name[$role->getName(), ...] for each role in the collection

Response shapes

Single resource

{
  "success": true,
  "data": { "id": 1, "name": "John" }
}

Paginated list

{
  "success": true,
  "data": [ ... ],
  "meta": { "total": 42, "page": 1, "limit": 20, "pages": 3 }
}

Validation error (422)

{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "email": ["This value is not a valid email address."]
  }
}

Not found (404)

{ "success": false, "message": "User with ID \"99\" was not found." }

Wire InvalidPayloadException (400) and ResourceNotFoundException (404) to your API error listener to produce these shapes automatically.

Components reference

Class Namespace Description
AbstractRestRepository …\Repository Paginator, dynamic filters, operator suffixes
AbstractRestService …\Service Decode → filter fields → hydrate → persist
AbstractRestController …\Controller Full CRUD actions, serialization, pick merging
EntityHydrator …\Service Scalar + datetime + ManyToOne + ManyToMany
PickResolver …\Service Dot-notation deep property resolver
ApiResponse …\DTO { success, data, message } envelope
PaginatedResponse …\DTO { success, data, meta } envelope
InvalidPayloadException …\Exception 400 — bad JSON or wrong field type
ResourceNotFoundException …\Exception 404 — entity not found