The second thing required for your component to integrate with the
Joomla API application is a webservices
plugin. This
plugin serves a dual role.
On one hand it lets the user determine which components will participate in the Joomla API application — if they do not have a use case for your component's JSON API they can and should disable the plugin.
On the other hand the plugin implements routing for your API
application. That's right; there is no such thing as a Router service
for the API application since the URL structure is meant to be
predefined and predictable[9]. As a matter of fact, the only event handler we implement
in this plugin is onBeforeApiRoute
.
The onBeforeApiRoute
event handler takes
exactly one argument, an object instance of
\Joomla\CMS\Router\ApiRouter
. This is a subclass
of Joomla's \Joomla\Router\Router
class,
optimised for use in the API application.
There are two ways to add routes for your component's API application integration, both using the router object.
The first way is calling the object's
createCRUDRoutes
method. This methods creates
five routes (named after the respective HTTP verbs):
-
A
GET
route which returns a list of records. -
A
GET
route with anid
parameter which returns a single record. -
A
POST
route to add a new record. -
A
PATCH
route with anid
parameter which updates (“edits”) an existing record. -
A
DELETE
route with anid
parameter which deletes an existing record.
As you may have intuited, these are the five CRUD (Create, Read, Update, Delete) operations supported by Joomla's controllers and more specifically the ApiController. The HTTP verbs for each CRUD operation are those specified in the REST specification. So, yes, Joomla lets us easily create RESTful JSON APIs, as promised. Neat, huh?!
This method takes four arguments:
-
$baseName
. The base name of our route. The recommended format is similar tov1/example
(for the default API route of your component) orv1/example/items
.The
v1
part is used for versioning your API. Start with v1 for the first version of your component's API. If you make a backwards incompatible change bump it to v2, then v3 and so on.The next part should be your component's name without the
com_
prefix.Some components manage more than one content type. For example a helpdesk component will have at the very least a ticket and a reply content type, the former being a representation of all the metadata of the ticket (title, date, owner, …) and the latter being a representation of the actual posts sent by the participants. In these cases the secondary, tertiary etc content items get their own route by adding one more level in the base name of the route.
-
$controller
. The name (string) of your component's controller which will be handling the request. This must be in all lowercase. -
$defaults
. This is an array of URL parameters which are added by default when parsing the route. At the very least it should be something like['component' => '
wherecom_example
']com_example
is your component's name.If you are using Joomla's category management the API route to manage the categories for your component will be using the following defaults:
['component' => 'com_categories', 'extension' => '
wherecom_example
']com_example
is your component's name. -
$publicGets
. This is a boolean. By default it's false which means that only authenticated users will be allowed to list items using GET. If you set it to true then anyone who figures out the URL can use GET to get a list of items and read all items.Warning If you set
$publicGets
to true you most likely have to override thedisplayItem
anddisplayList
methods in the respective controller to perform custom access control appropriate to your component. Most likely the items you want to show publicly are not all the items (including unpublished, deleted and possibly access-controlled) your component knows about.Caution If you have a public route you MUST think about which fields you'll be including in your JsonapiView object. Not all fields are suitable for public display; that could lead to a security vulnerability known as information disclosure.
For example, your forum component may be saving the IP address alongside the user ID and creation date and time of a forum post. DO NOT make the IP address and user ID available to the public. This combination is considered Personally Identifiable Information and can result in fines! Just the user ID may be privileged information depending on the context of the site (remember that usernames are not privileged information, but the internal user IDs are).
The second way to add routes to the ApiRouter is, of course, using
its addRoute
or
addRoutes
methods, just like any standard
Joomla router object. The former accepts a single
\Joomla\Router\Route
object whereas the latter
accepts an array of Route objects.
The constructor of the \Joomla\Router\Route
object accepts five arguments:
-
$methods
. This is an array of strings consisting of the HTTP verbs this route will be handling. The HTTP verbs must be in uppercase, e.g.['GET']
,['POST', 'PUT']
, or['PUT', 'PATCH']
to mention three of the most practical examples. -
$pattern
. This is the same as the$baseName
of thecreateCRUDRoutes
method with an added feature. If you want to include parameters, e.g. an ID, you will define it as:something
(note the colon in the front). This may be very familiar to those of you who've written routes for Symfony or Laravel applications. For example, the patternv1/example/items/:id
tells Joomla that this route will only match if thev1/example/items
route is followed by something which will be made available as the request parameter namedid
. If that something does not exist the route does not match and Joomla won't use it. What that something must look like for the route to match? See$rules
below! -
$controller
. This is different to the$controller
of thecreateCRUDRoutes
method in that it needs to have both the controller and the task, in all lowercase, separated by a dot. For example:item.add
. -
$rules
. If your route pattern has parameters you will provide a Regular Expression pattern the value must match for the route to match. For example['id' => '(\d+)']
tells Joomla that theid
parameter (which was defined as:id
in the pattern) must be an integer consisting of one or more digits — including the value0
.Tip This comes in very handy if you want to have the exact same GET route accept either a numeric ID or a text slug — assuming your component enforces unique slugs. In this case you would define two routes like this:
$router->addRoutes([ new Route(['GET'], 'v1/example/items/:id', 'item.displayItem', ['id' => '(\d+)'], ['option' => 'com_example']), new Route(['GET'], 'v1/example/items/:slug', 'item.displayItem', ['slug' => '(.*)'], ['option' => 'com_example']), ]);
If the parameter is numeric the first route matches and the parameter is made available as the
id
request parameter. Otherwise, the parameter matches the second route and the parameter is made available as theslug
request parameter. Your overriddenItemController::displayItem
method would first check if theid
request parameter exists and non-empty. If not, it would look for a non-emptyslug
request parameter and find the correct ID of the record to display by making a database query. -
$defaults
. This is the same as the$defaults
of thecreateCRUDRoutes
method with one addition. If you have a GET route and you want it to be accessible by unauthenticated users you need to add['public' => true]
to the$defaults
array. If you want it to only be accessible by authenticated users you need to add['public' => false]
.
[9] As opposed to being defined by the site's owner and limited by that person's ability to create a sensible menu structure — something which, according to my experience working on thousand of sites' backends, is most definitely not a given. At some point I will probably have to write a series of blog posts or yet another book on how to realistically build Joomla sites…