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+.
composer require antoi/restify-bundleIf you are not using Symfony Flex, register the bundle manually:
// config/bundles.php
return [
Antoi\RestifyBundle\RestifyBundle::class => ['all' => true],
];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'];
}
}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'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.
| 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 |
GET /api/users?page=2&limit=10&sort=name,-createdAt
sort=name→ORDER BY name ASCsort=-createdAt→ORDER BY createdAt DESC- Multiple fields:
sort=name,-createdAt
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 |
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
{
"success": true,
"data": { "id": 1, "name": "John" }
}{
"success": true,
"data": [ ... ],
"meta": { "total": 42, "page": 1, "limit": 20, "pages": 3 }
}{
"success": false,
"message": "Validation failed.",
"errors": {
"email": ["This value is not a valid email address."]
}
}{ "success": false, "message": "User with ID \"99\" was not found." }Wire
InvalidPayloadException(400) andResourceNotFoundException(404) to your API error listener to produce these shapes automatically.
| 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 |