Skip to content

antoine1003/restify-bundle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RestifyBundle

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages