REpresentational State Transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web Services. REST APIs are one of the pillars of modern web development. Most web applications these days are developed as single-page applications on the frontend, connected to backend APIs written in different languages. There are various PHP frameworks which could help you build a REST API in minutes. But, let us learn how to build a simple REST API in core PHP.
This is the first part of the Learn PHP
series, where you'll learn how to build a REST API in PHP using core PHP.
Next two:
Prerequisites
PHP REST API Skeleton
Create a /src
directory, and a composer.json
file in the top directory with one dependency: the DotEnv library, which allows storing secured pieces of information in .env
file.
composer.json
{
"require": {
"vlucas/phpdotenv": "^5.3"
},
"autoload": {
"psr-4": {
"Src\\": "src/"
}
}
}
A PSR-4 autoloader will automatically look for PHP classes in the /src
directory.
Install dependencies now:
composer install
It will create a /vendor
directory, and the DotEnv dependency will be installed (autoloader will load classes from /src
with no include()
calls).
Create a .gitignore
file for your project with two lines in it, so the /vendor
directory and local .env
file will be ignored:
.gitignore
vendor/
.env
Next, create a .env.example
file for Secret variables:
.env.example
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
and a .env
file where you'll fill in your actual details later (it will be ignored by Git so it won’t end up in your repository).
Create a start.php
file which loads the environment variables.
start.php
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
echo $_ENV['DB_HOST'];
// test code:
// it will output: localhost
// when you run $ php start.php
Configure the Database for your PHP REST API
We will use MySQL to power our simple API.
Create a new database and user for your app:
mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
GRANT ALL on blog.* to 'rest_api_user'@'localhost';
quit
The REST API will contain posts for our Blog
Application, with the following fields: id
, title
, body
, author
, author_picture
, created_at
. It allows users to post their blog on our Blog
application.
Create the database table in MySQL.
mysql -u rest_api_user -p;
// Enter your password
use blog;
CREATE TABLE `post` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`body` text NOT NULL,
`author` varchar(255),
`author_picture` varchar(255),
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
Add the database connection variables to your .env
file:
.env
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password
Create a class to hold the database connections and add the initialization of the connection to the start.php
file.
src/Database.php
<?php
namespace Src;
class Database {
private $dbConnection = null;
public function __construct()
{
$host = $_ENV['DB_HOST'];
$port = $_ENV['DB_PORT'];
$db = $_ENV['DB_DATABASE'];
$user = $_ENV['DB_USERNAME'];
$pass = $_ENV['DB_PASSWORD'];
try {
$this->dbConnection = new \PDO(
"mysql:host=$host;port=$port;dbname=$db",
$user,
$pass
);
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function connet()
{
return $this->dbConnection;
}
}
start.php
<?php
require 'vendor/autoload.php';
use Src\Database;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
$dbConnection = (new Database())->connet();
Add a Class for the Post Table and Implement PHP REST API
There are many ways to interact with the database in an object-oriented context, but, let us go with a simple method where you will implement methods to return all posts, return a specific post and add/update/delete a post.
Also, the API endpoints which will be handled by our frontend api/index.php
.
REST API with the following endpoints:
Your APIs
API | CRUD | Description |
---|---|---|
GET /posts | READ | Get all the Posts from post table |
GET /post/{id} | READ | Get a single Post from post table |
POST /post | CREATE | Create a Post and insert into post table |
PUT /post/{id} | UPDATE | Update the Post in post table |
DELETE /post/{id} | DELETE | Delete a Post from post table |
api/index.php
<?php
require "../start.php";
use Src\Post;
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-Requested-With");
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri );
// endpoints starting with `/post` or `/posts` for GET shows all posts
// everything else results in a 404 Not Found
if ($uri[1] !== 'post') {
if($uri[1] !== 'posts'){
header("HTTP/1.1 404 Not Found");
exit();
}
}
// endpoints starting with `/posts` for POST/PUT/DELETE results in a 404 Not Found
if ($uri[1] == 'posts' and isset($uri[2])) {
header("HTTP/1.1 404 Not Found");
exit();
}
// the post id is, of course, optional and must be a number
$postId = null;
if (isset($uri[2])) {
$postId = (int) $uri[2];
}
$requestMethod = $_SERVER["REQUEST_METHOD"];
// pass the request method and post ID to the Post and process the HTTP request:
$controller = new Post($dbConnection, $requestMethod, $postId);
$controller->processRequest();
src/Post.php
<?php
namespace Src;
class Post {
private $db;
private $requestMethod;
private $postId;
public function __construct($db, $requestMethod, $postId)
{
$this->db = $db;
$this->requestMethod = $requestMethod;
$this->postId = $postId;
}
public function processRequest()
{
switch ($this->requestMethod) {
case 'GET':
if ($this->postId) {
$response = $this->getPost($this->postId);
} else {
$response = $this->getAllPosts();
};
break;
case 'POST':
$response = $this->createPost();
break;
case 'PUT':
$response = $this->updatePost($this->postId);
break;
case 'DELETE':
$response = $this->deletePost($this->postId);
break;
default:
$response = $this->notFoundResponse();
break;
}
header($response['status_code_header']);
if ($response['body']) {
echo $response['body'];
}
}
private function getAllPosts()
{
$query = "
SELECT
id, title, body, author, author_picture, created_at
FROM
post;
";
try {
$statement = $this->db->query($query);
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
exit($e->getMessage());
}
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode($result);
return $response;
}
private function getPost($id)
{
$result = $this->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode($result);
return $response;
}
private function createPost()
{
$input = (array) json_decode(file_get_contents('php://input'), TRUE);
if (! $this->validatePost($input)) {
return $this->unprocessableEntityResponse();
}
$query = "
INSERT INTO post
(title, body, author, author_picture)
VALUES
(:title, :body, :author, :author_picture);
";
try {
$statement = $this->db->prepare($query);
$statement->execute(array(
'title' => $input['title'],
'body' => $input['body'],
'author' => $input['author'],
'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($input['author'])).'.png?s=200',
));
$statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
$response['status_code_header'] = 'HTTP/1.1 201 Created';
$response['body'] = json_encode(array('message' => 'Post Created'));
return $response;
}
private function updatePost($id)
{
$result = $this->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$input = (array) json_decode(file_get_contents('php://input'), TRUE);
if (! $this->validatePost($input)) {
return $this->unprocessableEntityResponse();
}
$statement = "
UPDATE post
SET
title = :title,
body = :body,
author = :author,
author_picture = :author_picture
WHERE id = :id;
";
try {
$statement = $this->db->prepare($statement);
$statement->execute(array(
'id' => (int) $id,
'title' => $input['title'],
'body' => $input['body'],
'author' => $input['author'],
'author_picture' => 'https://secure.gravatar.com/avatar/'.md5($input['author']).'.png?s=200',
));
$statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode(array('message' => 'Post Updated!'));
return $response;
}
private function deletePost($id)
{
$result = $this->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$query = "
DELETE FROM post
WHERE id = :id;
";
try {
$statement = $this->db->prepare($query);
$statement->execute(array('id' => $id));
$statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode(array('message' => 'Post Deleted!'));
return $response;
}
public function find($id)
{
$query = "
SELECT
id, title, body, author, author_picture, created_at
FROM
post
WHERE id = :id;
";
try {
$statement = $this->db->prepare($query);
$statement->execute(array('id' => $id));
$result = $statement->fetch(\PDO::FETCH_ASSOC);
return $result;
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
private function validatePost($input)
{
if (! isset($input['title'])) {
return false;
}
if (! isset($input['body'])) {
return false;
}
return true;
}
private function unprocessableEntityResponse()
{
$response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
$response['body'] = json_encode([
'error' => 'Invalid input'
]);
return $response;
}
private function notFoundResponse()
{
$response['status_code_header'] = 'HTTP/1.1 404 Not Found';
$response['body'] = null;
return $response;
}
}
Let's start the PHP Server and test your APIs with a tool like Postman.
php -S localhost:8000 -t api
Done
Congratulations!! You have successfully built a REST API.
GitHub: https://github.com/shahbaz17/php-rest-api
What's Next
Now, as you have built this application, you may be aware that these endpoints are not protected, and anyone on the internet could take advantage and update/delete/insert data. So, Let's secure this API with authentication and authorization.
In my next article, I will cover how you can take advantage of Magic's Passwordless feature and secure your API with just a few lines of changes to these codes.
Top comments (4)
Salam Mohammad!
I have a problem:
PHP Fatal error: Uncaught TypeError: Dotenv\Dotenv::__construct(): Argument #1 ($store) must be of type Dotenv\Store\StoreInterface, string given, called in /Applications/XAMPP/xamppfiles/htdocs/api/start.php on line 7 and defined in /Applications/XAMPP/xamppfiles/htdocs/api/vendor/vlucas/phpdotenv/src/Dotenv.php:60
Stack trace:
0 /Applications/XAMPP/xamppfiles/htdocs/api/start.php(7): Dotenv\Dotenv->__construct('/Applications/X...')
1 {main}
thrown in /Applications/XAMPP/xamppfiles/htdocs/api/vendor/vlucas/phpdotenv/src/Dotenv.php on line 60
10.4.18-MariaDB, PHP 8
Ok, I use new phpdotenv and many change...
Don't use getenv only $_ENV example $host = $_ENV['DB_HOST'];
and
$dotenv = Dotenv\Dotenv::createImmutable(DIR);
$dotenv->load();
=> "createImmutable"
Thanks for bringing this into my attention.
New version is really good. Also, I have update the guide to reflect the same.
Are you still getting the same error @skipperski ? or have you solved it using the new approach?