alex-kalanis / restful
Nette REST API bundle
Requires
- php: >= 8.1.0
- ext-dom: *
- ext-fileinfo: *
- ext-libxml: *
- ext-simplexml: *
- alex-kalanis/oauth2: ^1.0
- nette/application: ~3.2
- nette/bootstrap: ~3.2
- nette/caching: ~3.3
- nette/di: ~3.2
- nette/http: ~3.3
- nette/robot-loader: ~4.0
- nette/routing: ~3.1
- nette/security: ~3.2
- nette/utils: ~4.0
- tracy/tracy: ^2.10
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.65
- mockery/mockery: ^1.6
- nette/tester: ^2.5
- php-parallel-lint/php-parallel-lint: ~1.4
- rector/rector: ^1.2
- shipmonk/composer-dependency-analyser: ^1.7
This package is auto-updated.
Last update: 2024-12-26 06:30:56 UTC
README
This is repository for adding RestAPI into Nette. Fork of older Drahak repository with refactor to run on php 8.1+.
The main difference is in directory structure, namespaces, tests, static analysis and dependency check.
Content
- Requirements
- Installation & setup
- Neon configuration
- Sample usage
- Simple CRUD resources
- Accessing input data
- Input data validation
- Error presenter
- Security & authentication
- Secure your resources with OAuth2
- JSONP support
- Utilities that make life better
Requirements
This repository requires PHP version 8.1.0 or higher. The production dependencies are Nette framework 3.2.x and my OAuth2 provider (see Secure your resources with OAuth2) - everything has been solved by Composer.
Installation & setup
The easiest way is to use Composer
$ composer require alex-kalanis/restful
Then register the extension by adding this code to bootstrap.php
(before creating container):
kalanis\Restful\DI\RestfulExtension::install($configurator);
or register it in config.neon
:
extensions: restful: kalanis\Restful\DI\RestfulExtension
Neon configuration
You can configure alex-kalanis\Restful library in config.neon in section restful
:
restful: convention: 'snake_case' cacheDir: '%tempDir%/cache' jsonpKey: 'jsonp' prettyPrintKey: 'pretty' routes: generateAtStart: FALSE prefix: resources module: 'RestApi' autoGenerated: TRUE panel: TRUE mappers: myMapper: contentType: 'multipart/form-data' class: \App\MyMapper security: privateKey: 'my-secret-api-key' requestTimeKey: 'timestamp' requestTimeout: 300
cacheDir
: not much to say, just directory where to store cachejsonpKey
: sets query parameter name, which enables JSONP envelope mode. Set this to FALSE if you wish to disable it.prettyPrintKey
: API prints every resource with pretty print by default. You can use this query parameter to disable itconvention
: resource array keys conventions. Currently supported 3 values:snake_case
,camelCase
&PascalCase
which automatically converts resource array keys. You can write your own converter. Just implementkalanis\Restful\Resource\IConverter
interface and tag your service withrestful.converter
.routes.generateAtStart
: generating routes at start of Router (only for auto generated routes and if is Router set over config.neon)routes.prefix
: mask prefix to resource routes (only for auto generated routes)routes.module
: default module to resource routes (only for auto generated routes)routes.autoGenerated
: ifTRUE
the library auto generate resource routes from Presenter action method annotations (see below)routes.panel
: ifTRUE
the resource routes panel will appear in your nette debug barmappers
: replace existing mappers or add new mappers for different content-typessecurity.privateKey
: private key to hash secured requestssecurity.requestTimeKey
: key in request body, where to find request timestamp (see below - Security & authentication)security.requestTimeout
: maximal request timestamp age
Tip: Use gzip compression for your resources. You can enable it simply in neon:
php: zlib.output_compression: yes
Resource routes panel
It is enabled by default but you can disable it by setting restful.routes.panel
to FALSE
.
This panel show you all REST API resources routes (exactly all routes in default route list
which implements IResourceRouter
interface). This is useful e.g. for developers who develop
client application, so they have all API resource routes in one place.
Sample usage
<?php namespace ResourcesModule; use kalanis\Restful\IResource; use kalanis\Restful\Application\UI\ResourcePresenter; /** * SamplePresenter resource * @package ResourcesModule */ class SamplePresenter extends ResourcePresenter { protected $typeMap = array( 'json' => IResource::JSON, 'xml' => IResource::XML ); /** * @GET sample[.<type xml|json>] */ public function actionContent(string $type = 'json'): void { $this->resource->title = 'REST API'; $this->resource->subtitle = ''; $this->sendResource($this->typeMap[$type]); } /** * @GET sample/detail */ public function actionDetail(): void { $this->resource->message = 'Hello world'; } }
Resource output is determined by Accept
header. Library checks the header for
application/xml
, application/json
, application/x-data-url
and
application/www-form-urlencoded
and keep an order in Accept
header.
Note: If you call $presenter->sendResource()
method with a mime type in first
parameter, API will accept only this one.
Also note: There are available annotations @GET
, @POST
, @PUT
, @HEAD
, @DELETE
.
This allows kalanis\Restful library to generate API routes for you so you don't need to do
it manually. But it's not necessary! You can define your routes using IResourceRoute
or
its default implementation such as:
<?php use kalanis\Restful\Application\Routes\ResourceRoute; $anyRouteList[] = new ResourceRoute('sample[.<type xml|json>]', 'Resources:Sample:content', ResourceRoute::GET);
There is only one more parameter unlike the Nette default Route, the request method. This
allows you to generate same URL for e.g. GET and POST method. You can pass this parameter
to route as a flag so you can combine more request methods such as
ResourceRoute::GET | ResourceRoute::POST
to listen on GET and POST request method in the
same route.
You can also define action names dictionary for each request method:
<?php new ResourceRoute('myResourceName', [ 'presenter' => 'MyResourcePresenter', 'action' => [ ResourceRoute::GET => 'content', ResourceRoute::DELETE => 'delete' ] ], ResourceRoute::GET | ResourceRoute::DELETE);
Simple CRUD resources
Well it's nice but in many cases I define only CRUD operations so how can I do it more
intuitively? Use CrudRoute
! This child of ResourceRoute
pre-defines base CRUD
operations for you. Namely, it is Presenter:create
for POST method, Presenter:read
for GET, Presenter:update
for PUT and Presenter:delete
for DELETE. Then your router
will look like this:
<?php new CrudRoute('<module>/crud', 'MyResourcePresenter');
Note the second parameter, metadata. You can define only Presenter not action name. This
is because the action name will be replaced by value from actionDictionary
([CrudRoute::POST => 'create', CrudRoute::GET => 'read', CrudRoute::PUT => 'update', CrudRoute::DELETE => 'delete']
)
which is property of ResourceRoute
so even of CrudRoute
since it is its child. Also
note that we don't have to set flags. Default flags are set to CrudRoute::CRUD
so the
route will match all request methods.
Then you can simple define your CRUD resource presenter:
<?php namespace ResourcesModule; /** * CRUD resource presenter * @package ResourcesModule */ class CrudPresenter extends BasePresenter { public function actionCreate(): void { $this->resource->action = 'Create'; } public function actionRead(): void { $this->resource->action = 'Read'; } public function actionUpdate(): void { $this->resource->action = 'Update'; } public function actionDelete(): void { $this->resource->action = 'Delete'; } }
Note: every request method can be overridden if you specify X-HTTP-Method-Override
header
in request or by adding query parameter __method
to URL.
Let there be relations
Relations are pretty common in RESTful services but how to deal with it in URL? Our goal is
something like this GET /articles/94/comments[/5]
while ID in brackets might be optional.
The route will be as follows:
$router[] = new ResourceRoute('api/v1/articles/<id>/comments[/<commentId>]', [ 'presenter' => 'Articles', 'action' => [ IResourceRouter::GET => 'readComment', IResourceRouter::DELETE => 'deleteComment' ] ], IResourceRouter::GET | IResourceRouter::DELETE);
Request parameters in action name
It's a quite long. Therefore, there is an option how to generalize it. Now it will look like this:
$router[] = new ResourceRoute('api/v1/<presenter>/<id>/<relation>[/<relationId>]', [ 'presenter' => 'Articles', 'action' => [ IResourceRouter::GET => 'read<Relation>', IResourceRouter::DELETE => 'delete<Relation>' ] ], IResourceRouter::GET | IResourceRouter::DELETE);
Much better but still quite long. Let's use CrudRoute
again:
$router[] = new CrudRoute('api/v1/<presenter>/<id>/[<relation>[/<relationId>]]', 'Articles');
This is the shortest way. It works because action dictionary in CrudRoute
is basically
as follows.
[ IResourceRouter::POST => 'create<Relation>', IResourceRouter::GET => 'read<Relation>', IResourceRouter::PUT => 'update<Relation>', IResourceRouter::DELETE => 'delete<Relation>' ]
Also have a look at few examples for this single route:
GET api/v1/articles/94 => Articles:read
DELETE api/v1/articles/94 => Articles:delete
GET api/v1/articles/94/comments => Articles:readComments
GET api/v1/articles/94/comments/5 => Articles:readComments
DELETE api/v1/articles/94/comments/5 => Articles:deleteComments
POST api/v1/articles/94/comments => Articles:createComments
...
Of course you can add more then one parameter to action name and make even longer relations.
Note: if relation or any other parameter in action name does not exist, it will be ignored and name without the parameter will be used.
Also note: parameters in action name are NOT case-sensitive
Accessing input data
If you want to build REST API, you may also want to access query input data for all request
methods (GET, POST, PUT, DELETE and HEAD). So the library defines input parser, which reads
data and parse it to an array. Data are fetched from query string or from request body and
parsed by IMapper
. First the library looks for request body. If it's not empty it checks
Content-Type
header and determines correct mapper (e.g. for application/json
->
JsonMapper
etc.) Then, if request body is empty, try to get POST data and at the end even
URL query data.
<?php namespace ResourcesModule; /** * Sample resource * @package ResourcesModule */ class SamplePresenter extends BasePresenter { /** * @PUT <module>/sample */ public function actionUpdate(): void { $this->resource->message = $this->input->message ?? 'no message'; } }
Good thing about it is that you don't care of request method. Nette Kalanis REST API library
will choose correct Input parser for you but it's still up to you, how to handle it. There
is available InputIterator
so you can iterate through input in presenter or use it in your
own input parser as iterator.
Input data validation
First rule of access to input data: never trust client! Really this is very important
since it is key feature for security. So how to do it right? You may already know Nette Forms
and its validation. Let's do the same in Restful! You can define validation rules for each
input data field. To get field (exactly kalanis\Restful\Validation\IField
), just call
field
method with field name in argument on Input
(in presenter: $this->input
). And
then define rules (almost) like in Nette:
/** * SamplePresenter resource * @package Restful\Api */ class SamplePresenter extends BasePresenter { public function validateCreate(): void { $this->input->field('password') ->addRule(IValidator::MIN_LENGTH, NULL, 3) ->addRule(IValidator::PATTERN, 'Please add at least one number to password', '/.*[0-9].*/'); } public function actionCreate(): void { // some save data insertion } }
That's it! It is not exact the way like Nette but it's pretty similar. At least the base public interface.
Note: the validation method validateCreate
. This new lifecycle method
validate<Action>()
will be processed for each action before the action method
action<Action>()
. It's not required but it's good to use for defining some validation
rules or validate data. In case if validation failed throws exception BadRequestException
with code HTT/1.1 422 (UnproccessableEntity) that can be handled by error presenter.
Error presenter
The simplest but yet powerful way to provide readable error response for clients is to use
$presenter->sendErrorResponse(Exception $e)
method. The simplest error presenter could
look like as follows:
<?php namespace Restful\Api; use kalanis\Restful\Application\UI\ResourcePresenter; /** * Base API ErrorPresenter * @package Restful\Api */ class ErrorPresenter extends ResourcePresenter { /** * Provide error to client * @param \Exception $exception */ public function actionDefault($exception): void { $this->sendErrorResource($exception); } }
Clients can determine preferred format just like in normal API resource. Actually it only adds data from exception to resource and send it to output.
Security & authentication
Restful provides a few ways how to secure your resources:
BasicAuthentication
This is completely basic but yet powerful way how to secure you resources. It's based on
standard Nette user's authentication (if user is not logged in then throws security
exception which is provided to client) therefore it's good for trusted clients (such as
own client-side application etc.) Since this is common Restful contains
SecuredResourcePresenter
as a children of ResourcePresenter
which already handles
BasicAuthentication
for you. See example:
use kalanis\Restful\Application\UI\SecuredResourcePresenter /** * My secured resource presenter */ class ArticlesPresenter extends SecuredResourcePresenter { // all my resources are protected and reachable only for logged user's // you can also add some Authorizator to check user rights }
Tip: Be careful using this authentication (and standard things such as user's Identity). Remember to keep REST API stateless. Being pragmatic, this is not a good approach, but it's the simplest one.
SecuredAuthentication
When third-party clients are connected you have to find another way how to authenticate these
requests. SecuredAuthentication
is more or less the answer. It's based on sending hashed
data with private key. Since the data is already encrypted, it not depends on SSL.
Authentication process is as follows:
Understanding authentication process
- Client: append request timestamp to request body.
- Client: hash all data with
hash_hmac
(sha256 algorithm) and with private key. Then append generated hash to request asX-HTTP-AUTH-TOKEN
header (by default). - Client: sends request to server.
- Server: accepts client's request and calculate hash in the same way as client (using
abstract template class
AuthenticationProcess
) - Server: compares client's hash with hash that it generated in previous step.
- Server: also checks request timestamp and make difference. If it's bigger than 300 (5 minutes) throws exception. (this is to avoid something called Replay Attack)
- Server: catches any
SecurityException
that throwsAuthenticationProcess
and provides error response.
Default AuthenticationProcess
is NullAuthentication
so all requests are unsecured.
You can use SecuredAuthentication
to secure your resources. To do so, just set this
authentication process to AuthenticationContext
in restful.authentication
or
$presenter->authentication
.
<?php namespace ResourcesModule; use kalanis\Restful\Security\Process\SecuredAuthentication; /** * CRUD resource presenter * @package ResourcesModule */ class CrudPresenter extends BasePresenter { #[\Nette\DI\Attributes\Inject] public SecuredAuthentication $securedAuthentication; protected function startup(): void { parent::startup(); $this->authentication->setAuthProcess($this->securedAuthentication); } // your secured resource action }
Never send private key!
Secure your resources with OAuth2
If you want to secure your API resource with OAuth2, you will need OAuth2 provider.
There is already one bundled via Composer.
Just use OAuth2Authentication
which is AuthenticationProcess
. If you wish to use
any other OAuth2 provider, you can write your own AuthenticationProcess
.
<?php namespace Restful\Api; use kalanis\Restful\IResource; use kalanis\Restful\Security\Process\AuthenticationProcess; use kalanis\Restful\Security\Process\OAuth2Authentication; /** * CRUD resource presenter * @package Restful\Api */ class CrudPresenter extends BasePresenter { #[\Nette\DI\Attributes\Inject] public AuthenticationProcess $authenticationProcess; /** * Check presenter requirements * @param $element */ public function checkRequirements($element): void { parent::checkRequirements($element); $this->authentication->setAuthProcess($this->authenticationProcess); } // ... }
Note: this is only Resource server so it handles access token authorization. To generate access token you'll need to create OAuth2 presenter (Resource owner and authorization server
- see OAuth2 documentation).
JSONP support
If you want to access your API resources by JavaScript on remote host, you can't make normal
AJAX request on API. So JSONP is alternative how to do it. In JSONP request you load your
API resource as a JavaScript using standard script
tag in HTML. API wraps JSON string to
a callback function parameter. It's actually pretty simple, but it needs special care. For
example you can't access response headers or status code. You can wrap these headers and
status code to all your resources but this is not good for normal API clients, which can
access header information. The library allows you to add special query parameter jsonp
(name depends on your configuration, this is default value). If you access resource with
?jsonp=callback
API automatically determines JSONP mode and wraps all resources to following
JavaScript:
callback({ "response": { "yourResourceData": "here" }, "status": 200, "headers": { "X-Powered-By": "Nette framework", ... } })
Note : the function name. This is name from jsonp
query parameter. This string is
"webalized" by Nette\Utils\Strings::webalize(jsonp, NULL, FALSE)
. If you set jsonpKey
to FALSE
or NULL
in configuration, you totally disable JSONP mode for all your API
resources. Then you can trigger it manually. Just set IResource
$contentType
property
to IResource::JSONP
.
Also note : if this option is enabled and client adds jsonp
parameter to query string,
no matter what you set to $presenter->resource->contentType
it will produce
JsonpResponse
.
Utilities that make life better
Filtering API requests is rut. That's why it's boring to do it all again. Restful provides
RequestFilter
which parses the most common things for you. In ResourcePresenter
you can
find RequestFilter
in $requestFilter
property.
Paginator
By adding offset
& limit
parameters to query string you can create standard Nette
Paginator
. Your API resource then response with Link
header (where "last page" part
of Link
and X-Total-Count
header are only provided if you set total items count to
a paginator)
Link: <URL_to_next_page>; rel="next",
<URL_to_last_page>; rel="last"
X-Total-Count: 1000
Fields list
In case you want to load just a part of a resource (e.g. it's expensive to load whole
resource data), you should add fields
parameter to query params with list of desired
fields (e.q. fields=user_id,name,email
). In RequestFilter
, you can get this list
(array('user_id', 'name', 'email')
) by calling getFieldsList()
method.
Sort list
If you want to sort data provided by resource you will probably need properties according
to which you sort it. To make it as easy as possible you can get it from sort
query
parameter (such as sort=name,-created_at
) as array('name' => 'ASC', 'created_at' => 'DESC')
by calling RequestFilter
method getSortList()
So that's it. Enjoy and hope you like it!