Tutorial - REST¶
Overview¶
In this tutorial, you will learn how to create a simple application that provides a RESTful API using different HTTP methods:
GET
to retrieve and search dataPOST
to add dataPUT
to update dataDELETE
to delete data
NOTE
This is just a sample application. It lacks a lot of features such as authentication, authorization, sanitization of input and error management to name a few. Please use it as a building block for your application, or as a tutorial to understand how you can build a REST API with Phalcon. You can also have a look at the rest-api project.
Methods¶
The API consists of the following methods:
Method | URL | Action |
---|---|---|
GET | /api/robots | Get all robots |
GET | /api/robots/search/Astro | Searches robots with 'Astro' in their name |
GET | /api/robots/2 | Get robots based on primary key |
POST | /api/robots | Add robot |
PUT | /api/robots/2 | Update robot based on primary key |
DELETE | /api/robots/2 | Delete robot based on primary key |
Application¶
As the application is simple, we will not implement any full MVC environment to develop it. In this case, we will use a micro application for our needs. The structure of the application is as follows:
First, we need a .htaccess
file that contains all the rules to rewrite the request URIs to the index.php
file (application entry-point):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^((?s).*)$ index.php?_url=/$1 [QSA,L]
</IfModule>
The bulk of our code will be placed in index.php
.
Now we need to create the routes, so that the application can understand what to do when end users interact with our application. The index.php
file changes to:
<?php
use Phalcon\Mvc\Micro;
$app = new Micro();
$app->get(
'/api/robots',
function () {
}
);
$app->get(
'/api/robots/search/{name}',
function ($name) {
}
);
$app->get(
'/api/robots/{id:[0-9]+}',
function ($id) {
}
);
$app->post(
'/api/robots',
function () {
}
);
$app->put(
'/api/robots/{id:[0-9]+}',
function ($id) {
}
);
$app->delete(
'/api/robots/{id:[0-9]+}',
function ($id) {
}
);
$app->handle(
$_SERVER["REQUEST_URI"]
);
As we add the routes, we use the actual HTTP methods as the names of the methods called in the application object. This allows us to easily define listening points for the application based on those HTTP methods.
The first parameter of each method call is the route and the second is the handler i.e. what do we do when the user calls that route. In our example we have anonymous functions defined for each handler. For the following route:
we explicitly set the id
parameter to be a number. When a defined route matches the requested URI, then the corresponding handler (anonymous function) will be executed.
Models¶
For this application we store and manipulate Robots
in the database. To access the table we need a model. The class below, allows us to access each record of the table in an object oriented manner. We have also implemented business rules, using built-in validators. By doing so, we have high confidence that the data saved will meet the requirements of our application. This model file needs to be created in the my-rest-api/models
directory.
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Messages\Message;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;
use Phalcon\Validation\Validator\InclusionIn;
class Robots extends Model
{
public function validation()
{
$validator = new Validation();
$validator->add(
"type",
new InclusionIn(
[
'message' => 'Type must be "droid", "mechanical", or "virtual"',
'domain' => [
'droid',
'mechanical',
'virtual',
],
]
)
);
$validator->add(
'name',
new Uniqueness(
[
'field' => 'name',
'message' => 'The robot name must be unique',
]
)
);
if ($this->year < 0) {
$this->appendMessage(
new Message('The year cannot be less than zero')
);
}
// Validate the validator
return $this->validate($validator);
}
}
droid
, mechanical
or virtual
. Any other value will make the validator return false
and the operation (insert/update) will fail. The second validator checks the uniqueness of the name for our robot. The last validator checks the year
field to be a positive number. Database¶
We need to connect our application to the database. For this example we are going to use the popular MariaDB or similar variants such as MySQL, Aurora etc. In addition to the database setup, we are going to set up the autoloader, so that our application is aware of where to search for files required.
These changes need to be made in the index.php
file.
<?php
use Phalcon\Loader;
use Phalcon\Mvc\Micro;
use Phalcon\Di\FactoryDefault;
use Phalcon\Db\Adapter\Pdo\Mysql as PdoMysql;
$loader = new Loader();
$loader->registerNamespaces(
[
'MyApp\Models' => __DIR__ . '/models/',
]
);
$loader->register();
$container = new FactoryDefault();
$container->set(
'db',
function () {
return new PdoMysql(
[
'host' => 'localhost',
'username' => 'asimov',
'password' => 'zeroth',
'dbname' => 'robotics',
]
);
}
);
$app = new Micro($container);
Operations¶
Get¶
The first handler
that we will implement is the one that retrieves data from the database, when the request is made using the GET
HTTP method. The endpoint will return all the records from the database using a PHQL query and returning the results in JSON.
The handler for get()
and /api/robots
becomes:
<?php
$app->get(
'/api/robots',
function () use ($app) {
$phql = 'SELECT id, name '
. 'FROM MyApp\Models\Robots '
. 'ORDER BY name'
;
$robots = $app
->modelsManager
->executeQuery($phql)
;
$data = [];
foreach ($robots as $robot) {
$data[] = [
'id' => $robot->id,
'name' => $robot->name,
];
}
echo json_encode($data);
}
);
PHQL, allows us to write queries using a high level, object oriented SQL dialect, that internally translates your query to the correct SQL statements depending on the database system used. The use
statement in the anonymous function offers object injection from the local scope to the anonymous function.
Get - Text¶
We can get robots using their name or part of their name. This search feature will also be a get()
as far as HTTP method is concerned and it will tie to the /api/robots/search/{name}
endpoint. The implementation is similar to the one above. We just need to change the query slightly.
<?php
// Searches for robots with $name in their name
$app->get(
'/api/robots/search/{name}',
function ($name) use ($app) {
$phql = 'SELECT * '
. 'FROM MyApp\Models\Robots '
. 'WHERE name '
. 'LIKE :name: '
. 'ORDER BY name'
;
$robots = $app
->modelsManager
->executeQuery(
$phql,
[
'name' => '%' . $name . '%'
]
)
;
$data = [];
foreach ($robots as $robot) {
$data[] = [
'id' => $robot->id,
'name' => $robot->name,
];
}
echo json_encode($data);
}
);
Get - id
¶
To get a robot by using their id
is similar to the above operations. We will just need to adjust the query that we run against the database. The HTTP method used will also be get()
and the endpoint will be /api/robots/{id:[0-9]+}
. For this handler, we are also reporting back if a robot has not been found.
The index.php
changes again:
<?php
use Phalcon\Http\Response;
$app->get(
'/api/robots/{id:[0-9]+}',
function ($id) use ($app) {
$phql = 'SELECT * '
. 'FROM MyApp\Models\Robots '
. 'WHERE id = :id:'
;
$robot = $app
->modelsManager
->executeQuery(
$phql,
[
'id' => $id,
]
)
->getFirst()
;
$response = new Response();
if ($robot === false) {
$response->setJsonContent(
[
'status' => 'NOT-FOUND'
]
);
} else {
$response->setJsonContent(
[
'status' => 'FOUND',
'data' => [
'id' => $robot->id,
'name' => $robot->name
]
]
);
}
return $response;
}
);
Insert¶
Our design allows users to submit data so that we can insert them in the database. The HTTP method used is post()
to the /api/robots
endpoint. We expect the data to be submitted as a JSON string.
<?php
use Phalcon\Http\Response;
$app->post(
'/api/robots',
function () use ($app) {
$robot = $app->request->getJsonRawBody();
$phql = 'INSERT INTO MyApp\ModelsRobots '
. '(name, type, year) '
. 'VALUES '
. '(:name:, :type:, :year:)'
;
$status = $app
->modelsManager
->executeQuery(
$phql,
[
'name' => $robot->name,
'type' => $robot->type,
'year' => $robot->year,
]
)
;
$response = new Response();
if ($status->success() === true) {
$response->setStatusCode(201, 'Created');
$robot->id = $status->getModel()->id;
$response->setJsonContent(
[
'status' => 'OK',
'data' => $robot,
]
);
} else {
$response->setStatusCode(409, 'Conflict');
$errors = [];
foreach ($status->getMessages() as $message) {
$errors[] = $message->getMessage();
}
$response->setJsonContent(
[
'status' => 'ERROR',
'messages' => $errors,
]
);
}
return $response;
}
);
Response
object. If the query was executed correctly, we manipulate the response to have a status code of 201
and text Created
. We finally update the id
of the recently created record, and send the robot back with the response. If something is wrong, we change the response status code to 409
with the text Conflict
and collect all the errors that have been produced of the database operation. We then send those error messages back with the response.
Update¶
Updating data is similar to inserting. For this operation we are using the put()
HTTP method and the endpoint /api/robots/{id:[0-9]+}
. The passed id
parameter in the URL is the id of the robot to be updated. The data submitted is in JSON format.
<?php
use Phalcon\Http\Response;
$app->put(
'/api/robots/{id:[0-9]+}',
function ($id) use ($app) {
$robot = $app->request->getJsonRawBody();
$phql = 'UPDATE MyApp\Models\Robots '
. 'SET name = :name:, type = :type:, year = :year: '
. 'WHERE id = :id:';
$status = $app
->modelsManager
->executeQuery(
$phql,
[
'id' => $id,
'name' => $robot->name,
'type' => $robot->type,
'year' => $robot->year,
]
)
;
$response = new Response();
if ($status->success() === true) {
$response->setJsonContent(
[
'status' => 'OK'
]
);
} else {
$response->setStatusCode(409, 'Conflict');
$errors = [];
foreach ($status->getMessages() as $message) {
$errors[] = $message->getMessage();
}
$response->setJsonContent(
[
'status' => 'ERROR',
'messages' => $errors,
]
);
}
return $response;
}
);
OK
. If something is wrong, we change the response status code to 409
with the text Conflict
and collect all the errors that have been produced of the database operation. We then send those error messages back with the response.
Delete¶
Delete is nearly identical to the update
process. For this operation we are using the delete()
HTTP method and the endpoint /api/robots/{id:[0-9]+}
. The passed id
parameter in the URL is the id of the robot to be deleted.
The index.php
changes again:
<?php
use Phalcon\Http\Response;
// Deletes robots based on primary key
$app->delete(
'/api/robots/{id:[0-9]+}',
function ($id) use ($app) {
$phql = 'DELETE '
. 'FROM MyApp\Models\Robots '
. 'WHERE id = :id:';
$status = $app
->modelsManager
->executeQuery(
$phql,
[
'id' => $id,
]
)
;
$response = new Response();
if ($status->success() === true) {
$response->setJsonContent(
[
'status' => 'OK'
]
);
} else {
$response->setStatusCode(409, 'Conflict');
$errors = [];
foreach ($status->getMessages() as $message) {
$errors[] = $message->getMessage();
}
$response->setJsonContent(
[
'status' => 'ERROR',
'messages' => $errors,
]
);
}
return $response;
}
);
OK
. If something is wrong, we change the response status code to 409
with the text Conflict
and collect all the errors that have been produced of the database operation. We then send those error messages back with the response.
Schema¶
In order to create the table in our database, we need to use the following SQL queries:
create database `robotics`;
create table `robotics`.`robots` (
`id` int(10) unsigned not null auto_increment,
`name` varchar(200) collate utf8_bin not null,
`type` varchar(20) collate utf8_bin not null,
`year` smallint(4) unsigned not null,
PRIMARY KEY (`id`)
)
Run¶
You can of course set up your web server to run your application. For setup instructions you can check the webserver setup document. If you want to use the built-in PHP server, you will need to create a file called .htrouter
as follows:
<?php
$uri = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
if ($uri !== '/' && file_exists(__DIR__ . $uri)) {
return false;
}
$_GET['_url'] = $_SERVER['REQUEST_URI'];
require_once __DIR__ . '/index.php';
and then run the following command:
Tests¶
There are numerous testing suites that you can use to test this application. We will use curl on every route, to verify proper operation.
Get all the robots:
curl -i -X GET https://localhost/my-rest-api/api/robots
HTTP/1.1 200 OK
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 117
Content-Type: text/html; charset=UTF-8
[{"id":"1","name":"Robotina"},{"id":"2","name":"Astro Boy"},{"id":"3","name":"Terminator"}]
Search a robot by its name:
curl -i -X GET https://localhost/my-rest-api/api/robots/search/Astro
HTTP/1.1 200 OK
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 31
Content-Type: text/html; charset=UTF-8
[{"id":"2","name":"Astro Boy"}]
Get a robot by its id:
curl -i -X GET https://localhost/my-rest-api/api/robots/3
HTTP/1.1 200 OK
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 56
Content-Type: text/html; charset=UTF-8
{"status":"FOUND","data":{"id":"3","name":"Terminator"}}
Insert a new robot:
curl -i -X POST -d '{"name":"C-3PO","type":"droid","year":1977}' \
https://localhost/my-rest-api/api/robots
HTTP/1.1 201 Created
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 75
Content-Type: text/html; charset=UTF-8
{"status":"OK","data":{"name":"C-3PO","type":"droid","year":1977,"id":"4"}}
Try to insert a new robot with the name of an existing robot:
curl -i -X POST -d '{"name":"C-3PO","type":"droid","year":1977}' \
https://localhost/my-rest-api/api/robots
HTTP/1.1 409 Conflict
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 63
Content-Type: text/html; charset=UTF-8
{"status":"ERROR","messages":["The robot name must be unique"]}
Update a robot with an unknown type:
curl -i -X PUT -d '{"name":"ASIMO","type":"humanoid","year":2000}' \
https://localhost/my-rest-api/api/robots/4
HTTP/1.1 409 Conflict
Date: Wed, 25 Dec 2019 01:02:03 GMT
Server: Apache/2.2.22 (Unix) DAV/2
Content-Length: 104
Content-Type: text/html; charset=UTF-8
{"status":"ERROR","messages":["Value of field 'type' must be part of
list: droid, mechanical, virtual"]}
Delete a robot: