Medools Router
Index:, (*1)
Intro
A Router framework to bootstrap RESTful APIs based on
Medools., (*2)
Routes
Basically, all routes are formatted as:, (*3)
-
/resource
: Requests a collection of resources, (*4)
-
/resource/id
: Requests a specific resource, (*5)
See bellow details about the resource name., (*6)
The resource id
depends on the PRIMARY_KEY defined in
the model. Usually it is an integer, but if there is a composite PRIMARY_KEY
,
each column must be in the id
, separated by primary_key_separator
, e.g.
/resource/1-1
., (*7)
Also, resources can be nested:, (*8)
-
/resource/id/resource_1
: Requests a collection of resource_1
with
resource(id)
in the appropriate foreign column*, (*9)
-
/resource/id/resource_1/offset
: Requests a specific resource from the
collection in the previous topic. offset
works as collection[offset - 1]
,
i.e. it is not resource_1
's id, and has 1-based index, (*10)
Nesting is only allowed in resources (not in collections: /resource/resource_1
is wrong), but the route can end in a collection. And there is no hard limit of
nesting levels, the only requirement is that the last resource (or collection)
has a foreign key to the previous resource, that has a foreign key to the
previous one, and so on. The id
is only used in the first resource., (*11)
* If resource_1
has multiple foreign columns for resource
, only the first
one is used. As a work around, you can use
Collection query parameters to filter the correct column, (*12)
Root Route
When requesting the /
route, a count of all resources is returned. If the
meta
config is not empty, it is also included. An OPTIONS
request just lists
all implemented methods., (*13)
Install
Open a terminal in your project directory and run:, (*14)
composer require aryelgois/medools-router
, (*15)
Setup
Medools requires a config file to connect to your database.
(see more here), (*16)
Additionally, you will need some configurations and a resources list for your
API., (*17)
Also, if using authentication, you will need a secret file (or multiple files)
and a Sign up system (or an administrator will add valid credentials)., (*18)
Configurations
The Router accepts an array of configurations that will be passed to properties
in it. The array may contain:, (*19)
-
always_cache
(boolean): If GET
and HEAD
requests always check cache
headers (default: true
), (*20)
-
always_expand
(boolean): If foreign models are always expanded in a GET
resource request (default: false
), (*21)
-
authentication
(mixed[]|null): Specify how to authenticate the requests.
If null
, the authentication is disabled. If set, can contain the keys:, (*22)
-
secret
(string) required: Path to secret used to sign the
JWT tokens. It can be a file or a directory, (*23)
If it is a directory, the Router expects it to contain secrets named after
their expiration time, in a unix timestamp. Only the first file is used,
and the tokens expire at maximum in the same stamp as the secret. You can
use a cron job to keep generating more secrets, (*24)
DO NOT keep the secret in your web server's public directory,
neither let git track it, (*25)
-
realm
(string): Sent in WWW-Authenticate
Header, (*26)
-
algorithm
(string): Hash algorithm used to sign the tokens (default:
HS256
), (*27)
-
claims
(mixed[]): Static JWT claims to be included in every token.
Some claims are already defined by the Router: iss
, iat
, exp
and
user
, (*28)
-
expiration
(int): Expiration duration (in seconds) used to calculate
the exp
claim. It is limited by the secret timestamp (when secret
is a
directory), (*29)
-
verify
(boolean): If the authentication's email must be verified
(default: false
), (*30)
-
cache_method
(string): Function used to hash the Response Body, for HTTP
caching GET
and HEAD
requests. It receives a string with the body
serialized (default: md5
), (*31)
-
default_content_type
(mixed[]): Default content type for GET
requests.
It is combined with resource's GET
handlers, (*32)
The default is application/json
with internal handlers, (*33)
-
default_filters
(string|string[]|null): Default value for resources'
filters. See more in Resources list filters
option, (*34)
-
default_publicity
(boolean|string[]|string): Default value for resources'
public
option, (*35)
-
false
: All resources are private by default. It only has effect if
authentication
is not null
, (*36)
-
true
: All resources are public by default. It has the same effect as not
defining authentication
, but if it is defined, this value is useful to
make most resources public and some private, (*37)
-
string[]
: List of methods that are always public, (*38)
-
string
: The same as an array with one item, (*39)
-
extensions
(string[]): Map of file extensions and a related content type, (*40)
It allows overriding browser's Accept
header with an extension in the URL.
Fill it with extensions for custom content types your resources use, (*41)
Note that it is only checked by safe methods and is only useful to resources
that define custom GET
handlers. Using unknown extensions or when they are
not expected may invalidate the route, (*42)
-
implemented_methods
(string[]): List of HTTP methods implemented by the
API. You can limit which methods can be used, but to add more methods you
would need to extend or modify the Router class, (*43)
-
meta
(mixed[]|null): Contains information to be included in the response
for the root route. You can use it to add links to your project or
documentation, (*44)
-
per_page
(integer): Limit how many resources can be returned in a
collection request. If 0
, no pagination is done (default: 20
), (*45)
-
primary_key_separator
(string): Separator used in composite
PRIMARY_KEY (default: -
), (*46)
It does not need to be a single character, since it is used as explode
delimiter. But it must not be contained in the id itself, and can not contain
forward slash /
, (*47)
-
zlib_compression
(boolean): If should enable zlib compression when
appropriate (default: true
), (*48)
Resources list
A map of resource names to Medools Models, and optionally to specific
configurations for that resource., (*49)
The resource names should be plural. You should choose the casing convention to
be camelCase
or snake_case
. Prefer keeping consistent with the columns
casing in your models., (*50)
The value mapped can be either a string with the Fully Qualified Model Class, or
an array with:, (*51)
-
model
(string) required: Fully Qualified Model Class, (*52)
-
methods
(string|string[]): Allowed HTTP methods. Defaults to
implemented_methods
. OPTIONS
is implicitly included, (*53)
-
handlers
(mixed[]): Map of HTTP methods to php functions that process the
Request, (*54)
-
Several levels of arrays are allowed but not required, in the order:
HTTP method => Content type => Resource kind
(resource or collection). The
leaves must be the function names, (*55)
-
These functions receive a Resource
and must be capable of generating all
the output (both Headers and Body) for the Response. Exceptions are catched
by the Router, (*56)
-
The same handlers for GET
are used for HEAD
requests, and a HEAD
key
is ignored. Content types for GET
are related to the accepted Response,
while other methods use them with the Request's payload, (*57)
-
GET
content types enable their related extensions in the route endpoint, (*58)
-
All methods implicitly have an internal application/json
handler. If a
method defines a single handler (i.e. a string
) or if a application/json
key is set, the internal handler is disabled for that method (unless using
the magic value __DEFAULT__
), (*59)
-
When defining a single Resource kind (or setting one to null
), requesting
the disabled one is not acceptable, (*60)
-
filters
(string|string[]): List of columns that can be used as query
parameters to filter collection requests. It also accepts a string of a
special fields group (see fields query parameter),
or ALL
to allow filtering on any column. It replaces the default_filters
config, (*61)
-
cache
(boolean): If caching headers should be sent. It overrides the
always_cache
config, (*62)
-
max_age
(int): Cache-Control
max-age (seconds). Tells how long the cache
is considered fresh until it becomes stale and the client needs to validate
with a request to the server, (*63)
-
public
(boolean|string[]|string): Behaves like default_publicity
, and is
only used if authentication
config is set, (*64)
Usage
Follow the example to see how to use this framework in your web server., (*65)
When a request is done, your web server must redirect to a php script. If you
are using Apache, there is already a .htaccess
in the example that does the
job., (*66)
First, the script requires Composer's autoloader and loads Medools config. Then
it gathers request data:, (*67)
-
Method
: A HTTP method, (*68)
-
URI
: Requested route. It must not contain the path to the api directory, but
query parameters must remain in the URI, (*69)
-
Headers
: Headers in the request. Used headers are:, (*70)
-
Authorization
: Contains credentials for authenticating the request.
Possible types are Basic and Bearer, (*71)
-
X-HTTP-Method-Override
: If your clients can not work with PUT
, PATCH
or DELETE
, they can use it to replace POST
method. It is enabled by
ENABLE_METHOD_OVERRIDE
, (*72)
-
Content-Type
: Data in the request payload is expected to be
application/json
by default. Resources may specify more types they read
with external handlers, (*73)
-
Accept
: The Router responses are, by default, in application/json
. But
resources may define more types they output, associated to external handlers, (*74)
-
If-None-Match
: If caching headers are enabled, it is checked to see if a
stale cache can still be used, (*75)
-
Body
: Raw data from the payload that will be parsed, (*76)
-
URL
: URL that access the Router. It is used to create links to other
resources, (*77)
Finally, a Router object is created with a resources list, optional
configurations and all that data from the request. It will do its best to
solve the route and give a response., (*78)
The resources list and configurations can be stored in a external file, like
.json
or .yml
or something else. Remember to add a library to parse that
file before passing to the Router., (*79)
You can also configure a subdomain like api.example.com
to handle the routes.
It is up to you., (*80)
SECURITY NOTE: It is highly recommended that you use SSL to protect your
data transactions, (*81)
HTTP Methods
The following HTTP methods are implemented by the Router class:, (*82)
-
GET
: By default, responses contain a JSON representation of the requested
route, (*83)
-
Resource requests include a Link
header listing the location of foreign
models, (*84)
-
Collection requests include a Link
header for pagination and a
X-Total-Count
counting all resources (ignoring pagination, but considering
filters), unless per_page
is 0, (*85)
-
Also, caching headers are sent, if enabled, (*86)
Different content types can be configured per resource and
it is chosen based on request's Accept
header. They will not send the
headers listed previously (unless the external handler sends by itself), (*87)
-
HEAD
: Does the same processing for GET
, but only send headers (even for
external handlers), (*88)
-
OPTIONS
: Lists allowed methods for the requested route in Allow
header. It
is a special method that is always allowed, if implemented, (*89)
-
POST
: Creates a new resource inside the collection with data in the request
payload. A route to the new resource is in Location
header. This method is
not allowed for resource routes, (*90)
-
PATCH
: Sets one or more columns to a specific value, then responses with a
GET
of the current state, (*91)
-
PUT
: Replaces the requested resource with data in the request payload (all
required columns must be passed), or create a new one if it does not exist.
The response is a GET
of the current state. This method is not allowed for
collection routes, (*92)
The PRIMARY_KEY
columns can be omitted if they are in the route, i.e. a
route without nested resources. In this case, the same columns in the payload
have preference over the route, (*93)
-
DELETE
: Removes the resource or collection's resources from the Database, (*94)
Some notes:, (*95)
-
Pagination is disabled by default for a collection PATCH
or DELETE
, but
the client can explicitly use it with page
or per_page
query parameters, (*96)
-
A Content-Location
Header can be sent, implying an updated or better route
to the requested entity, (*97)
Query parameters
The client can use some query parameters for a more precise request:, (*98)
Collection request
-
sort
: List of comma separated columns to be sorted. A possible unary
negative implies descending sort order, (*99)
Example: ?sort=-priority,stamp
sorts in descending order of priority.
Within a specific priority, older entries are ordered first, (*100)
-
page
: Collection page the client wants to access. It has 1-based index, (*101)
-
per_page
: An integer limiting how many entries should be returned. It
overrides per_page
config. As syntax sugar, all
is the same as 0
, (*102)
-
filters: Defined in resources list, allows a fine control of the collection.
Additional operators can make advanced filters:, (*103)
Operator |
Medoo counterpart |
Name |
gt |
> |
Greater Than |
ge |
>= |
Greater or Equal to |
lt |
< |
Lesser Than |
le |
<= |
Lesser or Equal to |
ne |
! |
Not Equal |
bw |
<> |
BetWeen |
nw |
>< |
Not betWeen |
lk |
~ |
LiKe |
nk |
!~ |
Not liKe |
rx |
REGEXP |
RegeXp |
Examples:, (*104)
-
A simple string represents a list of comma separated values:, (*105)
?id=1,2,3
→
['id' => ['1', '2', '3']]
, (*106)
-
A numeric array can also be used:, (*107)
?filter[]=1&filter[]=2&filter[]=foo,bar
→
['filter' => ['1', '2', 'foo,bar']]
, (*108)
Helps including a comma in the item itself, (*109)
-
bw
and nw
accept two values separated by comma. ne
, lk
and nk
accept
one or more values separated in the same way:, (*110)
?id[bw]=100,200
→
['id[<>]' => ['100', '200']]
, (*111)
-
They also accept their multiple values in an array:, (*112)
?filter[ne][]=1&filter[ne][]=2&filter[ne][]=foo,bar
→
['filter[!]' => ['1', '2', 'foo,bar']]
, (*113)
-
Passing NULL
to ne
or directly to the filter will work:, (*114)
?active=NULL
→
['active' => null]
, (*115)
?active[ne]=NULL
→
['active[!]' => null]
, (*116)
Notes:, (*117)
-
Filters must be enabled by default_filters
or in resource's filters
, (*118)
-
Filters affect the pagination and the X-Total-Count
Header, (*119)
-
Avoid columns with the same name as other query parameters, (*120)
Resource request
-
expand
: Its existence forces foreign models to be expanded, unless its value
is false
, which is useful if always_expand
is true
Collection or Resource
-
fields
: List of comma separated columns to include in the response. It also
limits which foreign models are listed in Link
Header, if not expanding them, (*121)
There are special fields group that are replaced by the corresponding resource
columns. They have the same names as the Medools constants:, (*122)
PRIMARY_KEY
AUTO_INCREMENT
STAMP_COLUMNS
OPTIONAL_COLUMNS
FOREIGN_KEYS
SOFT_DELETE
Notes:, (*123)
-
Omitting or giving an empty fields
parameter results in all fields in the
response. But passing a query that solves to nothing (like
?fields=FOREIGN_KEYS
in a resource without foreigns, or simply
?fields=,
) results in no fields being sent, (*124)
-
It does not affect the order the fields are sent (also, in JSON objects are
unordered) and repeated fields (maybe because of the groups) are ok, (*125)
Cache
If enabled, caching headers ETag
and Cache-Control
are sent with successful
responses that have a body., (*126)
This functionality can be enabled globally or per resource., (*127)
If the client sends If-None-Match
Header, it is tested to provide a
304 Not Modified
response., (*128)
Authentication and Authorization
By default, all resources are public. Defining the authentication
config makes
all resources private and to access them the client must authenticate. In this
case, some resources may define themselves as public, or allow a few methods for
unauthenticated requests., (*129)
To authenticate, first the client must provide some credentials to the Router
with a Basic Authorization
Header. In the example, it is at the /auth
route. If authenticated, the server sends back a JWT token. This token must be
sent to all the other routes with a Bearer Authorization
Header until it
expires. When it happens, the client must authenticate again., (*130)
Authentication credentials ("Accounts") are stored in the authentications
table, with the columns:, (*131)
-
id
: Unique id sent in the token, (*132)
-
username
: Because of HTTP Basic Authentication limitations, it must not
contain the colon character (:
), (*133)
-
password
: Contains an encrypted representation of the account's password, (*134)
-
email
: For contacting the account user, (*135)
-
verified
: Tells if the email was verified, (*136)
-
enabled
: Useful to block an account but keep its data, (*137)
-
update
and stamp
: Just some timestamps, (*138)
Another table, authorizations
, maps accounts to authorized resources. It can
also list authorized methods and can include a filter passed directly to Medoo,
to limit which entries are authorized., (*139)
These two tables are be filled by your Sign up system, and controlled by an
user panel or an administrator., (*140)
Errors
When possible, the Router will return an error response with an appropriate HTTP
Status code and a JSON payload with code
and message
keys. Other keys may
appear, but are optional., (*141)
Error table:, (*142)
Code |
Name |
Status |
Description |
0 |
ERROR_UNKNOWN_ERROR |
500 |
Unknown error placeholder |
1 |
ERROR_INTERNAL_SERVER |
500 |
The Router detected a Server error |
2 |
ERROR_INVALID_CREDENTIALS |
401 |
Authorization Header is invalid |
3 |
ERROR_UNAUTHENTICATED |
401 |
Request could not be authenticated |
4 |
ERROR_INVALID_TOKEN |
401 |
Authorization token is invalid |
5 |
ERROR_METHOD_NOT_IMPLEMENTED |
501 |
Requested Method is not implemented in the Router |
6 |
ERROR_UNAUTHORIZED |
403 |
Requested resource is not public and the client does not have authorization to access it |
7 |
ERROR_INVALID_RESOURCE |
400 |
Route contains invalid resource |
8 |
ERROR_INVALID_RESOURCE_ID |
400 |
Route contains invalid resource id |
9 |
ERROR_INVALID_RESOURCE_OFFSET |
400 |
Route contains invalid resource offset |
10 |
ERROR_INVALID_RESOURCE_FOREIGN |
400 |
Nested resource does not have foreign key to previous resource |
11 |
ERROR_RESOURCE_NOT_FOUND |
404 |
Requested resource does not exist in the Database |
12 |
ERROR_UNSUPPORTED_MEDIA_TYPE |
415 |
Request's Content-Type is not supported |
13 |
ERROR_INVALID_PAYLOAD |
400 |
Request's Content-Type has syntax error or is invalid |
14 |
ERROR_METHOD_NOT_ALLOWED |
405 |
Requested Method is not allowed for Route |
15 |
ERROR_NOT_ACCEPTABLE |
406 |
Requested resource can not generate content complying to Accept header |
16 |
ERROR_INVALID_QUERY_PARAMETER |
400 |
Query parameter has invalid data |
17 |
ERROR_UNKNOWN_FIELDS |
400 |
Resource does not have requested fields |
18 |
ERROR_READONLY_RESOURCE |
400 |
Resource can not be modified |
19 |
ERROR_FOREIGN_CONSTRAINT |
400 |
Foreign constraint fails |
If an error or exception was not handled correctly, the response body is
unpredictable and may depend on error_reporting., (*143)
Extending
You can extend the Router class to hack its code. Just remember to pass the
correct class to the Controller., (*144)