REST API
An API is a set of site addresses that return data rather than pages, usually as JSON. Mobile apps and JavaScript use them. REST is a convention for such addresses: one address stands for a resource (posts), and the HTTP method for the action on it. Laminas has a ready-made base controller for this.
Preparation
In the response lesson the ViewJsonStrategy strategy was already enabled in config/autoload/global.php — without it a JsonModel does not turn into a JSON response.
The API controller
Create a file in the module's controller folder:
// module/Blog/src/Controller/PostApiController.php
namespace Blog\Controller;
use Blog\Model\PostTable;
use Laminas\Mvc\Controller\AbstractRestfulController;
use Laminas\View\Model\JsonModel;
class PostApiController extends AbstractRestfulController
{
public function __construct(private PostTable $posts) {}
public function getList()
{
return new JsonModel(['data' => $this->posts->fetchLatest(20)]);
}
}The class extends AbstractRestfulController. Unlike an ordinary controller it has no actions ending in Action: the method is chosen by the HTTP method.
The route
Add the controller to controllers.factories with ReflectionBasedAbstractFactory and describe a route in module/Blog/config/module.config.php:
// module/Blog/config/module.config.php, inside 'routes'
'api-posts' => [
'type' => Segment::class,
'options' => [
'route' => '/api/posts[/:id]',
'constraints' => ['id' => '[0-9]+'],
'defaults' => [
'controller' => Controller\PostApiController::class,
],
],
],The route has no action key — the controller picks the method itself. Open /api/posts: the browser shows JSON with a data key and the list of posts.
Which method is called
| Request | Controller method |
|---|---|
GET /api/posts | getList() |
GET /api/posts/5 | get($id) |
POST /api/posts | create($data) |
PUT /api/posts/5 | update($id, $data) |
DELETE /api/posts/5 | delete($id) |
If a method is not defined in your class, the controller answers with the 405 "method not allowed" code.
A single record
public function get($id)
{
$post = $this->posts->find((int) $id);
if ($post === null) {
$this->getResponse()->setStatusCode(404);
return new JsonModel(['error' => 'Post not found']);
}
return new JsonModel(['data' => $post]);
}For a post that does not exist the client gets the 404 code and an error message in JSON.
Creating and deleting
public function create($data)
{
$post = new Post();
$post->exchangeArray($data);
$this->posts->save($post);
$this->getResponse()->setStatusCode(201);
return new JsonModel(['data' => $post]);
}
public function delete($id)
{
$this->posts->delete((int) $id);
$this->getResponse()->setStatusCode(204);
return new JsonModel();
}$data holds an array of fields. If the client sent a JSON body with the Content-Type: application/json header, the controller parses it itself. Code 201 means "created", 204 means "done, no response body". Before saving, the data is checked with an InputFilter rule set from the validation lesson.
Checking with curl
curl http://localhost:8080/api/posts/1
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "A post from the API", "body": "The post text."}'The first command prints post number 1, the second creates a new one and returns it together with the assigned id.
Common mistake
The API is not protected by the listener from the access control lesson unless its route is in the protected list: DELETE /api/posts/5 deletes a post at any visitor's request. Add the api-posts route to the check, and for requests that change data answer with the 401 code instead of redirecting to the login page.