Routing

← Routing

Ashterix

docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api

en/docs/api/rest-bundle/routing.md new file +126 −0

Routing

From #[Route] to a REST route

An RPC procedure method gets into REST when it has the Symfony\Component\Routing\Attribute\Route attribute. The attribute on the class is optional and sets a common path and name. Methods without #[Route] stay RPC-only.

Part Rule
Path class path + / + method path; an empty method path gives the class path, both empty give /
Name ufo_rest_api_ + class name + _ + method name
Class name name from the class attribute; without it, the procedure alias from #[RPC\Info] in snake_case (shopItems → shop_items)
Method name name from the method attribute; without it, the PHP method name in snake_case (createItem → create_item)
HTTP methods methods from the method attribute; without them, POST
Other #[Route] options requirements, defaults, host etc. are not used: only path, name and methods are read
#[RPC\Info('catalog')]
#[Route('/catalog', name: 'catalog')]
class CatalogApi implements IRpcService
{
    #[Route('/items/{id}', name: 'item', methods: ['GET'])]   // GET  /rest/catalog/items/{id}
    public function item(int $id): array {}

    #[Route('/items')]                                         // POST /rest/catalog/items
    public function createItem(string $title): array {}
}

Path variables ({id}) are passed to the method as parameters with the same name; see Request parameters.

The path is not derived from the RPC method name automatically: without #[Route] the method is not available via REST, and an unknown path returns 404.

Generated routes

For each REST method and API version two Symfony routes are created:

Route Path HTTP methods Purpose
<name>_<version> <path>, for versions other than v1, /<version><path> from the attribute Method call
<name>_<version>_method_not_allowed the same any Priority -10: a wrong HTTP method gets 405 rather than 404

Routes from the bundle's config/router.yaml:

Route Path Purpose
ufo_rest_doc the import prefix OpenAPI, GET
ufo_rest_doc_ver /{ver} (v\d+) OpenAPI for a version
ufo_rest_doc_postman /postman Postman
ufo_rest_doc_postman_ver /{ver}/postman Postman for a version
ufo_rest_api_entrypoint /{path} Catch-all entry point, priority -100: an unknown path returns 404 in the error format
ufo_rest_api_entrypoint_ver /{ver}/{path} The same for versions, priority -90

HEAD is handled by GET routes. OPTIONS is not handled separately: for a method without OPTIONS in methods the response is 405.

Versions

Versions are defined by JsonRpcBundle: #[RPC\Info(version: 2, removedMethods: [...])] (API versions). A version 2 procedure inherits the version 1 methods it does not override or remove.

Version Call Documentation Route name
v1 (default) /rest/catalog/items/1 /rest, /rest/postman ..._v1
v2 /rest/v2/catalog/items/1 /rest/v2, /rest/v2/postman ..._v2

An unknown version returns 404.

API groups and multiple entry points

Import config/router.yaml separately for each entry point. defaults.api_group selects the API group; without it, the group is general. The URL prefix does not depend on the group name.

# config/routes/ufo_rest_api.yaml
rest:
    resource: ../../vendor/ufo-tech/rest-bundle/config/router.yaml
    prefix: /rest
    trailing_slash_on_root: false

rest_internal:
    resource: ../../vendor/ufo-tech/rest-bundle/config/router.yaml
    prefix: /internal
    defaults: { api_group: internal }
    trailing_slash_on_root: false

rest_partners:
    resource: ../../vendor/ufo-tech/rest-bundle/config/router.yaml
    prefix: /partners
    name_prefix: partners_
    defaults: { api_group: internal }
    trailing_slash_on_root: false
#[RPC\Info('catalog')]
#[RPC\APIGroups(['internal', 'public'], false)]
#[Route('/catalog', name: 'catalog')]
class CatalogApi implements IRpcService
{
    #[Route('/list', name: 'list', methods: ['GET'])]
    public function list(): array { return []; }

    #[RPC\APIGroups(['internal'], false)]
    #[Route('/price', name: 'price', methods: ['POST'])]
    public function price(int $price): int { return $price; }
}
  • Group membership of a method is defined by #[RPC\APIGroups]: the method attribute replaces the class attribute, withDefault: true adds general. #[RPC\IgnoreApi] excludes the method completely. For a method to appear in REST, #[Route] is required anyway.
  • Each entry point has its own methods, versions, documentation and token provider.
  • Route names get the group prefix: internal_ufo_rest_doc, internal_ufo_rest_api_catalog_list_v1. name_prefix goes after it: internal_partners_ufo_rest_doc.
  • Routes of the general group also keep the old names as aliases: ufo_rest_doc → general_ufo_rest_doc.
  • Only the route defines the group: an api_group field in the query or body does not change it.

Alongside RPC and other adapters

REST routes are registered through Ufo\JsonRpcBundle\Routing\IApiRouteImport (the RestRouteImport class), so groups and names are handled by the JsonRpcBundle route loader. RPC routes, REST routes and routes of other adapters implementing the same interface can be imported for the same groups in one file without name conflicts.

In 1.1–1.2 this was done by the bundle's own RestYamlFileLoader; it was removed in 1.3 (see the Changelog). Applications that only import config/router.yaml are not affected.

Troubleshooting

  • The method is not in REST: check #[Route] on the method, the API group of the import, #[RPC\IgnoreApi] and the version's removedMethods.
  • 405 for an existing method: the HTTP method is not in the attribute's methods (only POST by default).
  • Route names: bin/console debug:router | grep ufo_rest.