+++ Build A Simple REST API in PHP - Okta Developer
+++ Build A Simple REST API in PHP - Okta Developer
March 8, 2019
9 MIN READ
REST APIs are the backbone of modern web development. Most web applications these days
are developed as single-page applications on the frontend, connected to backend APIs
written in various languages. There are many great frameworks that can help you build REST
APIs quickly. Laravel/Lumen and Symfony’s API platform are the most often used examples in
the PHP ecosystem. They provide great tools to process requests and generate JSON
responses with the correct HTTP status codes. They also make it easy to handle common
issues like authentication/authorization, request validation, data transformation, pagination,
filters, rate throttling, complex endpoints with sub-resources, and API documentation.
You certainly don’t need a complex framework to build a simple but secure API though. In this
article, I’ll show you how to build a simple REST API in PHP from scratch. We’ll make the API
secure by using Okta as our authorization provider and implementing the Client Credentials
Flow. Okta is an API service that allows you to create, edit, and securely store user accounts
and user account data, and connect them with one or more applications.
There are different authentication flows in OAuth 2.0, depending on if the client application is
public or private and if there is a user involved or the communication is machine-to-machine
only. The Client Credentials Flow is best suited for machine-to-machine communication where
the client application is private (and can be trusted to hold a secret). At the end of the post, I’ll
show you how to build a test client application as well.
Table of Contents
Create the PHP Project Skeleton for Your REST API
Configure a Database for Your PHP REST API
Add a Gateway Class for the Person Table
Implement the PHP REST API
Secure Your PHP REST API with OAuth 2.0
Add Authentication to Your PHP REST API
Build a Sample Client Application (Command Line Script) to Test the PHP REST API
Learn More About PHP, Secure REST APIs, and OAuth 2.0 Client Credentials Flow
composer.json
{
"require": {
"vlucas/phpdotenv": "^2.4"
},
"autoload": {
"psr-4": {
"Src\\": "src/"
}
}
}
We’ve also configured a PSR-4 autoloader which will automatically look for PHP classes in the
/src directory.
We now have a /vendor directory, and the DotEnv dependency is installed (we can also use
our autoloader to load our classes from /src with no include() calls).
Let’s create a .gitignore file for our project with two lines in it, so the /vendor directory
and our local .env file will be ignored:
vendor/
.env
Next we’ll create a .env.example file for our Okta authentication variables:
.env.example
OKTAAUDIENCE=api://default
OKTAISSUER=
SCOPE=
OKTACLIENTID=
OKTASECRET=
and a .env file where we’ll fill in our actual details from our Okta account later (it will be
ignored by Git so it won’t end up in our repository).
We’ll need a bootstrap.php file which loads our environment variables (later it will also do
some additional bootstrapping for our project).
bootstrap.php
<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;
mysql -uroot -p
CREATE DATABASE api_example CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'api_user'@'localhost' identified by 'api_password';
GRANT ALL on api_example.* to 'api_user'@'localhost';
quit
Our rest API will deal with just a single entity: Person, with the following fields: id ,
firstname , lastname , firstparent_id , secondparent_id . It will allow us to define
people and up to two parents for each person (linking to other records in our database). Let’s
create the database table in MySQL:
.env.example
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
Then we’ll input our local credentials in the .env file (which is not stored in the repo,
remember?):
.env
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=api_example
DB_USERNAME=api_user
DB_PASSWORD=api_password
We can now create a class to hold our database connection and add the initialization of the
connection to our bootstrap.php file:
src/System/DatabaseConnector.php
<?php
namespace Src\System;
class DatabaseConnector {
try {
$this->dbConnection = new \PDO(
"mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
$user,
$pass
);
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;
use Src\System\DatabaseConnector;
Let’s create a dbseed.php file which creates our Person table and inserts some records in
it for testing:
dbseed.php
<?php
require 'bootstrap.php';
$statement = <<<EOS
CREATE TABLE IF NOT EXISTS person (
id INT NOT NULL AUTO_INCREMENT,
firstname VARCHAR(100) NOT NULL,
lastname VARCHAR(100) NOT NULL,
firstparent_id INT DEFAULT NULL,
secondparent_id INT DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (firstparent_id)
REFERENCES person(id)
ON DELETE SET NULL,
FOREIGN KEY (secondparent_id)
REFERENCES person(id)
ON DELETE SET NULL
) ENGINE=INNODB;
try {
$createTable = $dbConnection->exec($statement);
echo "Success!\n";
} catch (\PDOException $e) {
exit($e->getMessage());
}
Our database is all set! If you want to reset it, just drop the person table in MySQL and then
run php dbseed.php (I didn’t add the drop statement to the seeder as a precaution against
running it by mistake).
Add a Gateway Class for the Person
Table
There are many patterns for working with databases in an object-oriented context, ranging
from simple execution of direct SQL statements when needed (in a procedural way) to
complex ORM systems (two of the most popular ORM choices in PHP are Eloquent and
Doctrine). For our simple API, it makes sense to use a simple pattern as well so we’ll go with a
Table Gateway. We’ll even skip creating a Person class (as the classical pattern would
require) and just go with the PersonGateway class. We’ll implement methods to return all
records, return a specific person and add/update/delete a person.
src/TableGateways/PersonGateway.php
<?php
namespace Src\TableGateways;
class PersonGateway {
try {
$statement = $this->db->query($statement);
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
return $result;
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
try {
$statement = $this->db->prepare($statement);
$statement->execute(array($id));
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
return $result;
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
try {
$statement = $this->db->prepare($statement);
$statement->execute(array(
'firstname' => $input['firstname'],
'lastname' => $input['lastname'],
'firstparent_id' => $input['firstparent_id'] ?? null,
'secondparent_id' => $input['secondparent_id'] ?? null,
));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
try {
$statement = $this->db->prepare($statement);
$statement->execute(array(
'id' => (int) $id,
'firstname' => $input['firstname'],
'lastname' => $input['lastname'],
'firstparent_id' => $input['firstparent_id'] ?? null,
'secondparent_id' => $input['secondparent_id'] ?? null,
));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
try {
$statement = $this->db->prepare($statement);
$statement->execute(array('id' => $id));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
}
Obviously, in a production system, you would want to handle the exceptions more gracefully
instead of just exiting with an error message.
We’ll create a /public/index.php file to serve as our front controller and process the
requests, and a src/Controller/PersonController.php to handle the API endpoints
(called from the front controller after validating the URI).
public/index.php
<?php
require "../bootstrap.php";
use Src\Controller\PersonController;
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X
$requestMethod = $_SERVER["REQUEST_METHOD"];
// pass the request method and user ID to the PersonController and process the HTTP request:
$controller = new PersonController($dbConnection, $requestMethod, $userId);
$controller->processRequest();
src/Controller/PersonController.php
<?php
namespace Src\Controller;
use Src\TableGateways\PersonGateway;
class PersonController {
private $db;
private $requestMethod;
private $userId;
private $personGateway;
You can test the API with a tool like Postman. First, go to the project directory and start the
PHP server:
Then connect to 127.0.0.1:8000 with Postman and send http requests. Note: when making
PUT and POST requests, make sure to set the Body type to raw , then paste the payload in
JSON format and set the content type to JSON (application/json).
Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run
okta register to sign up for a new account. If you already have an account, run
okta login . Then, run okta apps create service . Select the default app name, or
change it as you see fit.
These are the credentials that your client application will need in order to authenticate. For this
example, the client and server code will be in the same repository, so we will add these
credentials to our .env file as well (make sure to replace {yourClientId} and
{yourClientSecret} with the values from this page):
Add to .env.example :
OKTAISSUER=
OKTACLIENTID=
OKTASECRET=
OKTAISSUER=https://{yourOktaDomain}/oauth2/default
OKTACLIENTID={yourClientId}
OKTASECRET={yourClientSecret}
Log in to the Okta Admin Console (tip: run okta login , open URL in a browser). Navigate to
Security > API. Select your default Authorization Server. Click the Edit icon, go to the Scopes
tab and click Add Scope to add a scope for the REST API. Name it person_api and check
Set as a default scope.
SCOPE=person_api
Now we can add the authorization code to our front controller (if using a framework, we’ll do
this in a middleware instead):
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X
$requestMethod = $_SERVER["REQUEST_METHOD"];
function authenticate() {
try {
switch(true) {
case array_key_exists('HTTP_AUTHORIZATION', $_SERVER) :
$authHeader = $_SERVER['HTTP_AUTHORIZATION'];
break;
case array_key_exists('Authorization', $_SERVER) :
$authHeader = $_SERVER['Authorization'];
break;
default :
$authHeader = null;
break;
}
preg_match('/Bearer\s(\S+)/', $authHeader, $matches);
if(!isset($matches[1])) {
throw new \Exception('No Bearer Token');
}
}
$jwtVerifier = (new \Okta\JwtVerifier\JwtVerifierBuilder())
->setIssuer(getenv('OKTAISSUER'))
->setAudience('api://default')
->setClientId(getenv('OKTACLIENTID'))
->build();
return $jwtVerifier->verify($matches[1]);
} catch (\Exception $e) {
return false;
}
}
public/client.php
<?php
require "../bootstrap.php";
$clientId = getenv('OKTACLIENTID');
$clientSecret = getenv('OKTASECRET');
$scope = getenv('SCOPE');
$issuer = getenv('OKTAISSUER');
// test requests
getAllUsers($token);
getUser($token, 1);
echo "success!\n";
// here's your token to use in API requests
return $response['token_type'] . " " . $response['access_token'];
}
function getAllUsers($token) {
echo "Getting all users...";
$ch = curl init();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:8000/person");
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: $token"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
var_dump($response);
}
var_dump($response);
}
You can run the application from the command line by going to the /public directory and
running:
php client.php
That’s it!
If you would like to dig deeper into the topics covered in this article, the following resources
are a great starting point:
Like what you learned today? Follow us on Twitter, and subscribe to our YouTube channel for
more awesome content!
Krasimir Hristozov
I don’t know what a .env.app file is, but the .env.example file is just a text file, and it
has to be copied to .env once you fill out the values.
You can use anything. You could use something like .env.prod .env.dev
Is the URL on the sample client supposed to be as shown above or you have to
change it to match your values?
Hi if im using XAMPP and placing this app in htdocs/api, the app will not run on
localhost/api/person
Where should i place the app in such a setup?
RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
Here we are serving the API using the PHP’s build in web server. What if I want to
serve using Apache? Can you please help.
You’d need to use mod_rewrite to redirect uri through the index.php. Add a
.htaccess file to the directory your index.php is in with the following content (may
need to be adjusted to suit your environment):
<ifmodule mod_rewrite.c="">
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
</ifmodule>
Thanks for this. I assume there must be some server configuration changes need to
make sure all the api calls go to the index.php script. But even then, wouldn’t the
server throw an error because it would look for a file/directory named /person?
No, the index.php file serves as a Front Controller and it parses the URI and
handles the routing
The routes are served by the index.php file which acts as a front controller - it
parses the URI and handles the routing automatically. Just make sure to pass all
requests to it (when using the built-in web server, this is not a problem).
The sample client can be run from the command line as per the tutorial, it doesn’t
have a URL (but I suppose there’s no problem to run it through the Web server, if
you prefer).
Is there any other issues? My Project root folder named ‘NoteCast’, where under it
there are the ‘public’ folder. Does this seems ok?
Ok, I’ve found the solution myself, it’s actually a SQL connection issue. I’m using a
MAMP for Mac, so there could be a mysql.sock filepath settings. Now it could run
smoothly.
But, at the end, the messages coming out from the console are as follow:
(from the terminal of my macBook pro):
"
Getting user with id#1…string(247) "
"
is there any issues when it says ‘explicit use of
FILTER_FLAG_SCHEME_REQUIRED’ as above? do i need to upgrade/replace
whichever version used in this tutorial as needed?
I do not get ow to run this through a normal web server. I tried all the suggestions
below about .htaccess etc. but could not get any to work. What URI would I use if
testing say on localhost?
I have just signed up for a okta account and found this article which I am following
but have quickly found myself not knowing stuff I feel I should know.
Where is the command “composer install” being typed? In okta or on the machine
with the code?
many thanks!
Thanks for this. Having given it more time I’ve now worked out I need a local php
dev env which then requires the composer application. Now it makes more sense, I
was naively expecting to be given this info what with the title of the document being
“Build a Simple REST API in PHP”.
Why do you need tokens / authorization if this is machine to machine? Wouldnt the
endpoint be easily programmed to just serve requests from the application id?
This seems to be a good point at first sight. I don’t know if I understand you
correctly, but if you use the API merely for your own use or for an app that makes
the requests invisibly in the background, then maybe that can work.
But if, for example, you are using the API to give a customer certain insights into
their customer account via the browser, then the API should be secured against the
customer being able to overstep their rights (with details visible in the query, e.g. via
the browser console or similar).
Wow really great article - it help me a lot in building my API in php. I have started
from tutorial in this article https://dev-bay.com/php-how… and I extended that
example with data base connection as you did in tutorial here, what have me really
impressive results.
p ess e esu ts
One question - will your solution work fully properly with php 7.0?
Hi, this is a well documented article, but i have a little problem, after i have set all
the okta credentials but i am still getting Obtaining “token…failed, exiting” maybe i
am doing something wrong, kindly help out.
Need Support?
You can reach us directly at [email protected] or you can also ask us on the forum.
OKTA.COM
HELP CENTER
Knowledgebase, roadmaps, and more
TRUST
C O N TAC T & L E G A L
MORE INFO
Pricing
Integrate with Okta
Change Log
3rd-party notes
Auth0 platform