Maurits van der Schee 7562ca591c Add v2 | 6 years ago | |
---|---|---|
docker | 6 years ago | |
src | 6 years ago | |
tests | 6 years ago | |
CONTRIBUTING.md | 6 years ago | |
LICENSE | 6 years ago | |
README.md | 6 years ago | |
api.php | 6 years ago | |
build.php | 6 years ago | |
composer.json | 6 years ago | |
test.php | 6 years ago |
Single file PHP 7 script that adds a REST API to a MySQL 5.5 InnoDB database. PostgreSQL 9.1 and MS SQL Server 2012 are fully supported.
Related projects:
There are also ports of this script in:
There are also proof-of-concept ports of this script that only support basic REST CRUD functionality in: PHP, Java, Go, C# .net core, Node.js and Python.
This is a single file application! Upload “api.php” somewhere and enjoy!
For local development you may run PHP’s built-in web server:
php -S localhost:8080
Test the script by opening the following URL:
http://localhost:8080/api.php/records/posts/1
Dont forget to modify the configuration at the bottom of the file.
Edit the following lines in the bottom of the file “api.php”:
$config = new Config([
'username' => 'xxx',
'password' => 'xxx',
'database' => 'xxx',
]);
These are all the configuration options and their default value between brackets:
The code resides in the “src” directory. You can access it at the URL:
http://localhost:8080/src/records/posts/1
You can compile all files into a single “api.php” file using:
php build.php
NB: The script appends the classes in alphabetical order (directories first).
These limitation were also present in v1:
These features match features in v1:
NB: No checkmark means: not yet implemented. Striken means: will not be implemented.
These features are new and were not included in v1.
You can enable the following middleware using the “middlewares” config parameter:
The “middlewares” config parameter is a comma separated list of enabled middlewares. You can tune the middleware behavior using middleware specific configuration parameters:
If you don’t specify these parameters in the configuration, then the default values (between brackets) are used.
TreeQL allow you to create a Tree of JSON objects based on your SQL database structure (relations).
It is loosely based on the REST standard and also inspired by json:api.
The example posts table has only a a few fields:
posts
=======
id
title
content
created
The CRUD + List operations below act on this table.
If you want to create a record the request can be written in URL format as:
POST /records/posts
You have to send a body containing:
{
"title": "Black is the new red",
"content": "This is the second post.",
"created": "2018-03-06T21:34:01Z"
}
And it will return the value of the primary key of the newly created record:
2
To read a record from this table the request can be written in URL format as:
GET /records/posts/1
Where “1” is the value of the primary key of the record that you want to read. It will return:
{
"id": 1
"title": "Hello world!",
"content": "Welcome to the first post.",
"created": "2018-03-05T20:12:56Z"
}
On read operations you may apply includes.
To update a record in this table the request can be written in URL format as:
PUT /records/posts/1
Where “1” is the value of the primary key of the record that you want to update. Send as a body:
{
"title": "Adjusted title!"
}
This adjusts the title of the post. And the return value is the number of rows that are set:
1
If you want to delete a record from this table the request can be written in URL format as:
DELETE /records/posts/1
And it will return the number of deleted rows:
1
To list records from this table the request can be written in URL format as:
GET /records/posts
It will return:
{
"records":[
{
"id": 1,
"title": "Hello world!",
"content": "Welcome to the first post.",
"created": "2018-03-05T20:12:56Z"
}
]
}
On list operations you may apply filters and includes.
Filters provide search functionality, on list calls, using the “filter” parameter. You need to specify the column name, a comma, the match type, another commma and the value you want to filter on. These are supported match types:
You can negate all filters by prepending a “n” character, so that “eq” becomes “neq”. Examples of filter usage are:
GET /records/categories?filter=name,eq,Internet
GET /records/categories?filter=name,sw,Inter
GET /records/categories?filter=id,le,1
GET /records/categories?filter=id,ngt,2
GET /records/categories?filter=id,bt,1,1
Output:
{
"records":[
{
"id": 1
"name": "Internet"
}
]
}
In the next section we dive deeper into how you can apply multiple filters on a single list call.
Filters can be a by applied by repeating the “filter” parameter in the URL. For example the following URL:
GET /records/categories?filter=id,gt,1&filter=id,lt,3
will request all categories “where id > 1 and id < 3”. If you wanted “where id = 2 or id = 4” you should write:
GET /records/categories?filter1=id,eq,2&filter2=id,eq,4
As you see we added a number to the “filter” parameter to indicate that “OR” instead of “AND” should be applied. Note that you can also repeat “filter1” and create an “AND” within an “OR”. Since you can also go one level deeper by adding a letter (a-f) you can create almost any reasonably complex condition tree.
NB: You can only filter on the requested table (not on it’s included) and filters are only applied on list calls.
Let’s say that you have a posts table that has comments (made by users) and the posts can have tags.
posts comments users post_tags tags
======= ======== ======= ========= =======
id id id id id
title post_id username post_id name
content user_id phone tag_id
created message
When you want to list posts with their comments users and tags you can ask for two “tree” paths:
posts -> comments -> users
posts -> post_tags -> tags
These paths have the same root and this request can be written in URL format as:
GET /records/posts?include=comments,users&include=tags
Here you are allowed to leave out the intermediate table that binds posts to tags. In this example you see all three table relation types (hasMany, belongsTo and hasAndBelongsToMany) in effect:
This may lead to the following JSON data:
{
"records":[
{
"id": 1,
"title": "Hello world!",
"content": "Welcome to the first post.",
"created": "2018-03-05T20:12:56Z",
"comments": [
{
id: 1,
post_id: 1,
user_id: {
id: 1,
username: "mevdschee",
phone: null,
},
message: "Hi!"
},
{
id: 2,
post_id: 1,
user_id: {
id: 1,
username: "mevdschee",
phone: null,
},
message: "Hi again!"
}
],
"tags": []
},
{
"id": 2,
"title": "Black is the new red",
"content": "This is the second post.",
"created": "2018-03-06T21:34:01Z",
"comments": [],
"tags": [
{
id: 1,
message: "Funny"
},
{
id: 2,
message: "Informational"
}
]
}
]
}
You see that the “belongsTo” relationships are detected and the foreign key value is replaced by the referenced object. In case of “hasMany” and “hasAndBelongsToMany” the table name is used a new property on the object.
When you want to create, read, update or delete you may specify multiple primary key values in the URL. You also need to send an array instead of an object in the request body for create and update.
To read a record from this table the request can be written in URL format as:
GET /records/posts/1,2
The result may be:
[
{
"id": 1,
"title": "Hello world!",
"content": "Welcome to the first post.",
"created": "2018-03-05T20:12:56Z"
},
{
"id": 2,
"title": "Black is the new red",
"content": "This is the second post.",
"created": "2018-03-06T21:34:01Z"
}
]
Similarly when you want to do a batch update the request in URL format is written as:
PUT /records/posts/1,2
Where “1” and “2” are the values of the primary keys of the records that you want to update. The body should contain the same number of objects as there are primary keys in the URL:
[
{
"title": "Adjusted title for ID 1"
},
{
"title": "Adjusted title for ID 2"
}
]
This adjusts the titles of the posts. And the return values are the number of rows that are set:
1,1
Which means that there were two update operations and each of them had set one row. Batch operations use database transactions, so they either all succeed or all fail (successful ones get roled back).
For spatial support there is an extra set of filters that can be applied on geometry columns and that starting with an “s”:
These filters are based on OGC standards and so is the WKT specification in which the geometry columns are represented.
By default all input is accepted and sent to the database. If you want to strip (certain) HTML tags before storing you may add the ‘sanitation’ middleware and define a ‘sanitation.handler’ function that returns the adjusted value.
'sanitation.handler' => function ($method, $tableName, $column, $value) {
return is_string($value) ? strip_tags($value) : $value;
},
The above example will strip all HTML tags from strings in the input.
By default all input is accepted. If you want to validate the input, you may add the ‘validation’ middleware and define a ‘validation.handler’ function that returns a boolean indicating whether or not the value is valid.
'validation.handler' => function ($method, $tableName, $column, $value, $context) {
return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
},
When you edit a comment with id 4 using:
PUT /records/comments/4
And you send as a body:
{"post_id":"two"}
Then the server will return a ‘422’ HTTP status code and nice error message:
{
"code": 1013,
"message": "Input validation failed for 'comments'",
"details": {
"post_id":"must be numeric"
}
}
You can parse this output to make form fields show up with a red border and their appropriate error message.
There are 4 cache engines that can be configured by the “cacheType” config parameter:
You can install the dependencies for the last three engines by running:
sudo apt install php-redis redis
sudo apt install php-memcache memcached
sudo apt install php-memcached memcached
The default engine has no dependencies and will use temporary files in the system “temp” path.
You may use the “cachePath” config parameter to specify the file system path for the temporary files or in case that you use a non-default “cacheType” the hostname (optionally with port) of the cache server.
These are the supported types with their default length/precision/scale:
character types
boolean types:
integer types:
floating point types:
decimal types:
date/time types:
binary types:
other types:
JavaScript does not support 64 bit integers. All numbers are stored as 64 bit floating point values. The mantissa of a 64 bit floating point number is only 53 bit and that is why all integer numbers bigger than 53 bit may cause problems in JavaScript.
The following errors may be reported:
The following JSON structure is used:
{
"code":1002,
"message":"Argument count mismatch in '1'"
}
NB: Any non-error response will have status: 200 OK
I am testing mainly on Ubuntu and I have the following test setups:
This covers not all environments (yet), so please notify me of failing tests and report your environment. I will try to cover most relevant setups in the “docker” folder of the project.
To run the functional tests locally you may run the following command:
php test.php
This runs the functional tests from the “tests” directory. It uses the database dumps (fixtures) and database configuration (config) from the corresponding subdirectories.
Install docker using the following commands and then logout and login for the changes to take effect:
sudo apt install docker.io
sudo usermod -aG docker ${USER}
To run the docker tests run “build_all.sh” and “run_all.sh” from the docker directory. The output should be:
================================================
Debian 9
================================================
[1/4] Starting MariaDB 10.1 ..... done
[2/4] Starting PostgreSQL 9.6 ... done
[3/4] Starting SQLServer 2017 ... skipped
[4/4] Cloning PHP-CRUD-API v2 ... done
------------------------------------------------
mysql: 80 tests ran in 2470 ms, 0 failed
pgsql: 80 tests ran in 680 ms, 0 failed
sqlsrv: skipped, driver not loaded
================================================
Ubuntu 16.04
================================================
[1/4] Starting MariaDB 10.0 ..... done
[2/4] Starting PostgreSQL 9.5 ... done
[3/4] Starting SQLServer 2017 ... done
[4/4] Cloning PHP-CRUD-API v2 ... done
------------------------------------------------
mysql: 80 tests ran in 2490 ms, 0 failed
pgsql: 80 tests ran in 710 ms, 0 failed
sqlsrv: 80 tests ran in 4204 ms, 0 failed
================================================
Ubuntu 18.04
================================================
[1/4] Starting MySQL 5.7 ........ done
[2/4] Starting PostgreSQL 10.4 .. done
[3/4] Starting SQLServer 2017 ... skipped
[4/4] Cloning PHP-CRUD-API v2 ... done
------------------------------------------------
mysql: 80 tests ran in 2882 ms, 0 failed
pgsql: 80 tests ran in 668 ms, 0 failed
sqlsrv: skipped, driver not loaded
The above test run (including starting up the databases) takes less than one minute on my machine.