antoi/restify-bundle
Reusable Symfony bundle (6.4–8.x, PHP 8.2+) that provides a full REST CRUD stack: abstract repository/service/controller, automatic entity hydration, query filtering, eager-loading, and pick-based response enrichment. Six endpoints per resource, quickly wired.
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-bundle
If 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 DESCsort=name,-createdAtAny 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 |
How can I help you explore Laravel packages today?