PHP Espresso is a small PHP Framework I created to develop runtime web servers for PHP running CLI programs and scripts. Very similar to frameworks like Express for NodeJS, Gorilla Mux for Golang, etc.
IMPORTANT NOTE: This is just a proof of concept to test the reliability of a runtime web server for PHP, its use and implementation is discouraged for production-level projects as it's an experimental framework for learning purposes.
PHP was designed to be a Single-Threaded Non-Asynchronous programming language, hence, the implementation of these type of web servers is very difficult as there will be always blocking processes for each request, hence, this server/framework is non-scalable.
- 3.1 Creating a basic web server
- 3.2 Serving a basic static HTML Page
- 3.3 Create a POST request
- 3.4 Complete Rest API CRUD example
- 3.5 Defining middlewares
- 3.6 Asynchronous programming
- PHP 8.0.0 or major
- Have PHP sockets module installed and enabled
- Composer
- Have a initted Composer project
Install PHP Espresso via Composer:
composer require edgaralexanderfr/php-espressoCreate a server.php file inside your project with the following program:
<?php
require_once 'vendor/autoload.php';
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
const PORT = 80;
$server = new Server();
$router = new Router();
$router->get('/', function (Request $request, Response $response) {
return $response->send([
'message' => 'Hello world!',
'code' => 200,
]);
});
$server->use($router);
$server->listen(PORT, function () use ($server) {
$server->log('Listening at port ' . PORT . '...');
});Run the server:
php server.php # Use sudo if necessary for port 80Visit http://localhost or execute:
curl http://localhostAnd voila! π
<?php
require_once 'vendor/autoload.php';
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
$server = new Server();
$router = new Router();
$router->get('/php-espresso-page', function (Request $request, Response $response) {
return $response->setPayload(
<<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Web Page with PHP Espresso!</title>
</head>
<body>
<h1>My Web Page with PHP Espresso!</h1>
<p>This page was served using PHP Espresso.</p>
</body>
</html>
HTML
);
});
$server->use($router);
$server->listen(80, function () use ($server) {
$server->log('Listening at port 80...');
});Visit http://localhost/php-espresso-page in your browser.
<?php
require_once 'vendor/autoload.php';
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
$server = new Server();
$router = new Router();
$router->post('/users', function (Request $request, Response $response) {
$body_json = $request->getJSON();
return $response->send([
'message' => 'User created successfully',
'code' => 201,
'user' => $body_json,
], 201);
});
$server->use($router);
$server->listen(80, function () use ($server) {
$server->log('Listening at port 80...');
});Execute a POST request:
curl -X POST http://localhost/users -d '{"name":"Alexander The Great"}'<?php
require_once 'vendor/autoload.php';
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
/** @var stdClass[] */
$users = [];
/** @var int */
$users_id = 1;
$server = new Server();
$router = new Router();
$router->get('/users', function (Request $request, Response $response) use (&$users) {
return $response->send($users);
});
$router->get('/users/:id', function (Request $request, Response $response) use (&$users) {
$id = $request->getId();
foreach ($users as $user) {
if (isset($user->{'id'}) && $user->id == $id) {
return $response->send($user);
}
}
return $response->send([
'message' => 'User not found',
'code' => 404,
], 404);
});
$router->post('/users', function (Request $request, Response $response) use (&$users, &$users_id) {
$body = $request->getJSON();
$email = $body->email ?? null;
$name = $body->name ?? null;
if (!$email || !$name) {
return $response->send([
'message' => 'Email and Name are required',
'code' => 400,
], 400);
}
$user = (object) [
'id' => $users_id++,
'email' => $email,
'name' => $name,
];
$users[] = $user;
return $response->send([
'message' => 'User created successfully',
'code' => 201,
'user' => $user,
], 201);
});
$router->patch('/users/:id', function (Request $request, Response $response) use (&$users) {
$id = $request->getId();
$body = $request->getJSON();
foreach ($users as &$user) {
if (isset($user->{'id'}) && $user->id == $id) {
$user->email = $body->email ?? $user->email;
$user->name = $body->name ?? $user->name;
return $response->send([
'message' => 'User updated successfully',
'code' => 200,
'user' => $user,
]);
}
}
return $response->send([
'message' => 'User not found',
'code' => 404,
], 404);
});
$router->delete('/users/:id', function (Request $request, Response $response) use (&$users) {
$id = $request->getId();
foreach ($users as $i => &$user) {
if (isset($user->{'id'}) && $user->id == $id) {
array_splice($users, $i, 1);
return $response->send([
'message' => 'User deleted successfully',
'code' => 200,
]);
}
}
return $response->send([
'message' => 'User not found',
'code' => 404,
], 404);
});
$server->use($router);
$server->listen(80, function () use ($server) {
$server->log('Listening at port 80...');
});Create a couple of users:
curl -X POST http://localhost/users -d '{"email":"[email protected]","name":"John Doe"}'
curl -X POST http://localhost/users -d '{"email":"[email protected]","name":"Jane Doe"}'Retrieve all created users:
curl http://localhost/usersRetrieve user with id 2:
curl http://localhost/users/2Update user with id 1:
curl -X PATCH http://localhost/users/1 -d '{"name":"John James Doe"}'Delete user with id 2:
curl -X DELETE http://localhost/users/2PHP Espresso supports global and route middlewares. You can assign as much middlewares to a single route as you want.
To do so, you can create a new middlewares.php file and add the following code:
<?php
require_once 'vendor/autoload.php';
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
define('AUTH_CREDENTIALS', (object) [
'user' => '[email protected]',
'pass' => '1234567890', // Please... don't...
]);
/**
* Middleware for admin authentication.
*/
function auth(Request $request, Response $response, callable $next)
{
$authorization = $request->getHeader('Authorization') ?? '';
$auth = explode(' ', $authorization);
$type = $auth[0] ?? '';
$token = $auth[1] ?? '';
$credentials = explode(':', base64_decode($token));
$user = $credentials[0] ?? null;
$pass = $credentials[1] ?? null;
if ($type != 'Bearer' || $user != AUTH_CREDENTIALS->user || $pass != AUTH_CREDENTIALS->pass) {
return $response->send([
'message' => Espresso\Http\CODES[401],
'code' => 401,
], 401);
}
$next();
}
/** @var stdClass[] */
$users = [];
/** @var int */
$users_id = 1;
$server = new Server();
$router = new Router();
// Global middleware to check service status:
$server->use(function (Request $request, Response $response, callable $next) use ($argv) {
$status = $argv[1] ?? '';
if ($status == 'service-closed') {
return $response->send([
'message' => 'Service unavailable temporary due to maintenance',
'code' => 503,
], 503);
}
$next();
});
$router->get('/users', function (Request $request, Response $response) use (&$users) {
return $response->send($users);
});
$router->post('/users', 'auth', function (Request $request, Response $response) use (&$users, &$users_id) {
$body = $request->getJSON();
$email = $body->email ?? null;
$name = $body->name ?? null;
if (!$email || !$name) {
return $response->send([
'message' => 'Email and Name are required',
'code' => 400,
], 400);
}
$user = (object) [
'id' => $users_id++,
'email' => $email,
'name' => $name,
];
$users[] = $user;
return $response->send([
'message' => 'User created successfully',
'code' => 201,
'user' => $user,
], 201);
});
$server->use($router);
$server->listen(80, function () use ($server) {
$server->log('Listening at port 80...');
});If you run:
php middlewares.php service-closedAnd do:
curl http://localhost/usersOr:
curl -X POST http://localhost/users -d '{"email":"[email protected]","name":"John Doe"}'You will get the following message:
{"message":"Service unavailable temporary due to maintenance","code":503}If you kill the previous server with CTRL+C and then run:
php middlewares.phpYou will be able to retrieve the users list now, e.g:
curl http://localhost/usersTo create a new user you need to be authenticated, to do so, assign an encoded Bearer Token using base64 to a variable and then pass the Authorization Header to curl command:
AUTH_TOKEN=$(echo '[email protected]:1234567890' | base64)
curl -X POST http://localhost/users -d '{"email":"[email protected]","name":"John Doe"}' -H "Authorization: Bearer ${AUTH_TOKEN}"It's still possible to do asynchronous programming with PHP Espresso by creating an asynchronous server and using the async and $next functions and callables:
<?php
require_once 'vendor/autoload.php';
use function Espresso\Event\async;
use Espresso\Http\Request;
use Espresso\Http\Response;
use Espresso\Http\Router;
use Espresso\Http\Server;
const SMALLER_FILE_PATH = __DIR__ . '/files/smaller-file.txt';
const BIGGER_FILE_PATH = __DIR__ . '/files/bigger-file.txt';
function read_file(string $path, int $bytes, callable $callable = null): void
{
$file = fopen($path, 'r');
$file_size = filesize($path);
$content = '';
$read_bytes = 0;
async(function () use ($bytes, $callable, &$file, $file_size, &$content, &$read_bytes) {
if ($read_bytes < $file_size) {
$chunk_size = min($file_size - $read_bytes, $bytes);
$chunk = fread($file, $chunk_size);
$content .= $chunk;
$read_bytes += $chunk_size;
return false;
}
if ($callable) {
$callable($content);
}
});
}
$server = new Server();
$router = new Router();
$router->get('/read-file', function (Request $request, Response $response, callable $next) {
$size = $request->getParam('size');
$file_path = $size == 'big' ? BIGGER_FILE_PATH : SMALLER_FILE_PATH;
read_file($file_path, 8, function (string $content) use ($request, $response, $next, $size) {
$response->send([
'file_content' => $content,
'size' => $size,
]);
$next();
});
});
$server->use($router);
$server->async(true);
$server->listen(80, function () use ($server) {
$server->log('Listening at port 80...');
});The async function initiates an Event Looper inside of the listen method when running in async mode by setting $server->async(true);.
async may return a boolean value (false) when the async call is not done yet and returns true or nothing when it's finished.
In this example, the async call inside of the read_file function will return false as long as the requested file is not completed yet, this by reading $bytes as a step for each chunk read through every call inside the Event Loop as an asynchronous process.
Once the whole file is read, the $callable callback will be called, passing in the content of the file on the async call by returning nothing at the very end of the function.
If you execute:
curl 'http://localhost/read-file?size=big'\
& curl 'http://localhost/read-file?size=small'\
& waitThe smaller file request will respond earlier than the larger file request despite of being executed right at the same time.
This could be a way to implement asynchronous programs and libraries for streaming, networking, databases, files, I/O operations, etc, although it's not perfect, it would require a vast work to implement lots of PHP libraries that were designed initially to be Single-Threaded and Synchronous.
Maybe the future of PHP is promising for this purpose with the introduction of tools like Fibers and stuff, but yet, we will see how it goes. ππ
