Friday, August 16, 2024
PHP Slim
PHP Slim is a lightweight, modular, and flexible PHP microframework that allows developers to build web applications and APIs quickly and easily. It is designed to be a minimalist framework that provides a simple and intuitive way to build web applications, without the overhead of a full-featured framework like Laravel or Symfony. This is PHP's equivalent of Python's FastAPI.
Install
Beside Slim we also must install PSR-7. We will use composer:
composer require slim/slim
composer require guzzlehttp/psr7
PSR-7 provides a common interface for working with HTTP messages, making it easier for developers to write code that is interoperable between different frameworks and libraries. This allows developers to write code that can be used with multiple frameworks, without having to worry about the specific implementation details of each framework.
This is what we have in our project dir:
Alright, next we will create index.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->get('/', function (Request $request, Response $response, $args) {
$response->getBody()->write("Hello, World!");
return $response;
});
$app->run();
OK, lets start development server in our project directory:
php -S localhost:8989
And if we open
http://localhost:8989 in browser:
Request and Response
PSR-7 ResponseInterface Methods
Method |
Description |
getStatusCode() |
Returns the HTTP status code of the response. |
withStatus($code, $reasonPhrase = '') |
Returns an instance with the specified status code and optional reason phrase. |
getReasonPhrase() |
Returns the reason phrase associated with the status code. |
getProtocolVersion() |
Returns the HTTP protocol version of the response. |
withProtocolVersion($version) |
Returns an instance with the specified HTTP protocol version. |
getHeaders() |
Returns all response headers as an associative array. |
hasHeader($name) |
Checks if the specified header exists. |
getHeader($name) |
Returns the value of a specific header as an array. |
getHeaderLine($name) |
Returns the value of a specific header as a string. |
withHeader($name, $value) |
Returns an instance with the specified header value. |
withAddedHeader($name, $value) |
Returns an instance with an additional value appended to the specified header. |
withoutHeader($name) |
Returns an instance without the specified header. |
getBody() |
Returns the body of the response. |
withBody(StreamInterface $body) |
Returns an instance with the specified body. |
PSR-7 ServerRequestInterface Methods
Method |
Description |
getServerParams() |
Returns server parameters as an associative array. |
getCookieParams() |
Returns cookie parameters as an associative array. |
withCookieParams(array $cookies) |
Returns an instance with the specified cookies. |
getQueryParams() |
Returns query string parameters as an associative array. |
withQueryParams(array $query) |
Returns an instance with the specified query parameters. |
getUploadedFiles() |
Returns uploaded files as an array of UploadedFileInterface instances. |
withUploadedFiles(array $uploadedFiles) |
Returns an instance with the specified uploaded files. |
getParsedBody() |
Returns the parsed body data, usually from JSON or form submissions. |
withParsedBody($data) |
Returns an instance with the specified parsed body data. |
getAttributes() |
Returns attributes derived from the request as an associative array. |
getAttribute($name, $default = null) |
Returns a single attribute from the request or a default value if not found. |
withAttribute($name, $value) |
Returns an instance with the specified attribute added. |
withoutAttribute($name) |
Returns an instance without the specified attribute. |
JSON
Can we return JSON? Yes!
$app->get('/json', function (Request $request, Response $response, $args) {
$data = ['message' => 'Hello, World!'];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
});
Notice that we need to use /json route.
Post request
For POST requests we will use RESTING browser extension. Our route controller is looking like this:
$app->post('/post', function (Request $request, Response $response, $args) {
$formData = $request->getParsedBody();
$name = $formData['name'] ?? 'Guest';
$data = ['message' => "Hello, $name!"];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
});
Middleware
What is Middleware?
Middleware is software that acts as a bridge between different applications or services. It enables communication and data management for distributed applications, allowing them to interact seamlessly.
Key Functions of Middleware
- Authentication: Verifying user identities.
- Logging: Keeping track of application events.
- Session Management: Handling user sessions.
- Data Transformation: Converting data formats for compatibility.
Why Use Middleware?
By using middleware, developers can focus on the core functionality of their applications without worrying about the underlying communication and data management complexities.
In short we use middleware before and after HTTP request!
<?php
require __DIR__ . '/vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
// Create Slim app instance
$app = AppFactory::create();
// Before middleware (executed before the route)
$beforeMiddleware = function (Request $request, RequestHandler $handler): Response {
// Example: Logging the request
error_log("Before Middleware: Incoming request - " . $request->getUri());
// Pass to the next middleware/handler
return $handler->handle($request);
};
// After middleware (executed after the route)
$afterMiddleware = function (Request $request, RequestHandler $handler): Response {
// Process the request through the next middleware/handler
$response = $handler->handle($request);
// Example: Adding a custom header to the response
$response = $response->withHeader('X-Custom-Header', 'MyValue');
// Log response status
error_log("After Middleware: Response status - " . $response->getStatusCode());
return $response;
};
// Register the middleware to the app
$app->add($afterMiddleware);
$app->add($beforeMiddleware);
// Define a simple route
$app->get('/', function (Request $request, Response $response, $args) {
$response->getBody()->write("Hello, World!");
error_log("Normal response");
return $response;
});
// Run the app
$app->run();
And as you can see, it is before and after request:
Middleware is mostly used for authentication. We check before access, request token and allow/block access to our route and controller.
Authentication
What is firebase/php-jwt?
firebase/php-jwt is a PHP library that provides a simple and efficient way to encode and decode JSON Web Tokens (JWT). JWT is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
Key Features
- JWT Creation: Easily create JWTs by encoding a payload with a secret key.
- JWT Validation: Decode and validate JWTs to ensure authenticity and integrity.
- Support for Multiple Algorithms: Choose from various signing algorithms (HMAC, RSA).
- Easy Integration: Simple to integrate into any PHP application.
- Active Maintenance: Regularly updated by the Firebase team.
Common Use Cases
- User Authentication: Stateless authentication for web applications.
- API Security: Securely transmit information between client and server.
- Single Sign-On (SSO): Facilitate SSO across multiple applications.
Install with composer:
composer require firebase/php-jwt
And this is whole code with JWT integrated:
<?php
require __DIR__ . '/vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Create Slim app instance
$app = AppFactory::create();
$key = "your_secret_key"; // Replace with your own secret key
// Middleware to protect routes with JWT
$jwtMiddleware = function (Request $request, RequestHandler $handler) use ($key): Response {
$authHeader = $request->getHeader('Authorization');
if (!$authHeader) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Authorization header not found']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
$jwt = substr($authHeader[0], 7); // Remove 'Bearer ' from the start
try {
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
} catch (Exception $e) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Invalid token: ' . $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
return $handler->handle($request);
};
// Before middleware (executed before the route)
$beforeMiddleware = function (Request $request, RequestHandler $handler): Response {
// Example: Logging the request
error_log("Before Middleware: Incoming request - " . $request->getUri());
// Pass to the next middleware/handler
return $handler->handle($request);
};
// After middleware (executed after the route)
$afterMiddleware = function (Request $request, RequestHandler $handler): Response {
// Process the request through the next middleware/handler
$response = $handler->handle($request);
// Example: Adding a custom header to the response
$response = $response->withHeader('X-Custom-Header', 'MyValue');
// Log response status
error_log("After Middleware: Response status - " . $response->getStatusCode());
return $response;
};
// Register the middleware to the app
$app->add($afterMiddleware);
$app->add($beforeMiddleware);
// Route to generate a JWT token (e.g., after a successful login)
$app->post('/login', function (Request $request, Response $response, $args) use ($key) {
$formData = $request->getParsedBody();
$username = $formData['username'] ?? null;
$password = $formData['password'] ?? null;
// Authenticate user (for demonstration, we assume successful login)
if ($username == 'user' && $password == 'pass') {
$payload = [
'iss' => 'your_domain.com',
'aud' => 'your_domain.com',
'iat' => time(),
'nbf' => time(),
'exp' => time() + 3600, // Token valid for 1 hour
'data' => [
'userId' => 1,
'username' => $username,
]
];
$jwt = JWT::encode($payload, $key, 'HS256');
$response->getBody()->write(json_encode(['token' => $jwt]));
} else {
$response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
return $response->withHeader('Content-Type', 'application/json');
});
// A protected route
$app->get('/protected', function (Request $request, Response $response, $args) {
$data = ['message' => 'This is a protected route'];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
})->add($jwtMiddleware);
// Define a simple public route
$app->get('/', function (Request $request, Response $response, $args) {
$response->getBody()->write("Hello, World!");
error_log("Normal response");
return $response;
});
// Define a public JSON route
$app->get('/json', function (Request $request, Response $response, $args) {
$data = ['message' => 'Hello, World!'];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
});
// A public POST route
$app->post('/post', function (Request $request, Response $response, $args) {
$formData = $request->getParsedBody();
$name = $formData['name'] ?? 'Guest';
$data = ['message' => "Hello, $name!"];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
});
// Run the app
$app->run();
Code Overview
Below, we will break down the key components of the code.
1. Setting Up the Slim Application
require __DIR__ . '/vendor/autoload.php';
use Slim\Factory\AppFactory;
// Create Slim app instance
$app = AppFactory::create();
Here, we initialize the Slim application and load the necessary dependencies using Composer's autoload feature.
2. JWT Middleware
The middleware is responsible for protecting routes by verifying the JWT provided in the Authorization header:
$jwtMiddleware = function (Request $request, RequestHandler $handler) use ($key): Response {
$authHeader = $request->getHeader('Authorization');
if (!$authHeader) {
// Handle missing token
}
$jwt = substr($authHeader[0], 7); // Remove 'Bearer ' from the start
try {
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
} catch (Exception $e) {
// Handle invalid token
}
return $handler->handle($request);
};
This middleware checks for the presence of a JWT in the request header, decodes it, and allows access to the protected route if the token is valid.
3. Generating a JWT Token
When a user successfully logs in, a JWT is generated:
$app->post('/login', function (Request $request, Response $response, $args) use ($key) {
// Authenticate user
if ($username == 'user' && $password == 'pass') {
$payload = [
'iss' => 'your_domain.com',
'aud' => 'your_domain.com',
'iat' => time(),
'nbf' => time(),
'exp' => time() + 3600, // Token valid for 1 hour
'data' => [
'userId' => 1,
'username' => $username,
]
];
$jwt = JWT::encode($payload, $key, 'HS256');
$response->getBody()->write(json_encode(['token' => $jwt]));
} else {
// Handle invalid credentials
}
return $response->withHeader('Content-Type', 'application/json');
});
In this route, after validating the user's credentials, a JWT is created with a payload containing user information and expiration time. This token is then sent back to the client.
4. Protecting Routes
To protect a route, we simply add the JWT middleware:
$app->get('/protected', function (Request $request, Response $response, $args) {
$data = ['message' => 'This is a protected route'];
$jsonResponse = json_encode($data);
$response->getBody()->write($jsonResponse);
return $response->withHeader('Content-Type', 'application/json');
})->add($jwtMiddleware);
This ensures that only requests with a valid JWT can access the protected route.
Now lets try to login with Resting. We will add "username":"name" and "password":"pass" in body:
The /login route simulates a login process. Upon successful authentication, it generates a JWT token with a payload that includes user data and the token's expiration time.
So now we need to add token to headers like in image bellow, add Authorization key and as value Bearer eyJ0eXAiOiJ... bearer is our token string.
We need to target /protected route and you can see result on image above.
Model
Let's create SQL database, you can drop this into phpMyAdmin:
DROP DATABASE IF EXISTS slimdb;
CREATE DATABASE slimdb;
DROP USER IF EXISTS 'slimdb'@'localhost';
CREATE USER 'slimdb'@'localhost' IDENTIFIED BY 'slimdb';
GRANT ALL ON slimdb.* TO 'slimdb'@'localhost';
USE slimdb;
-- Create the user table
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY, -- Unique identifier for each user
username VARCHAR(50) NOT NULL UNIQUE, -- Username must be unique
password VARCHAR(255) NOT NULL, -- Password (hashed)
email VARCHAR(100) NOT NULL UNIQUE, -- Email must be unique
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Timestamp of when the user was created
);
-- Create the post table
CREATE TABLE post (
id INT AUTO_INCREMENT PRIMARY KEY, -- Unique identifier for each post
title VARCHAR(255) NOT NULL, -- Title of the post
content TEXT NOT NULL, -- Content of the post
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Timestamp of when the post was created
);
If you don't know SQL this code creates database and two tables.
Now, to keep everything minimal we will add $pdo object like this:
$pdo = (function () {
// Database credentials
$host = 'localhost'; // Database host
$db = 'slimdb'; // Database name
$user = 'slimdb'; // Database username
$pass = 'slimdb'; // Database password
$charset = 'utf8mb4'; // Character set
// Data Source Name (DSN)
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
// Options for PDO
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
// Create and return a new PDO instance
return new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
// Handle connection error
throw new PDOException($e->getMessage(), (int)$e->getCode());
}
})();
PHP Data Objects (PDO) is a database access layer that provides a uniform method for accessing different databases in PHP. It allows developers to interact with databases using a consistent API, regardless of the underlying database system (e.g., MySQL, PostgreSQL, SQLite). PDO supports prepared statements, which help prevent SQL injection attacks, and offers features like transaction management and error handling, making it a secure and flexible choice for database interactions in PHP applications.
And finally, here is our route controller:
$app->post('/addPost', function (Request $request, Response $response, $args) use ($pdo){
$jsonResponse = function($data) use ($response) {
$jsonData = json_encode("Hello!");
$response->getBody()->write($jsonData);
return $response->withHeader('Content-Type', 'application/json');
};
$formData = $request->getParsedBody();
$title = $formData['title'];
$content = $formData['content'];
if (is_null($title) || is_null($content)) {
return false;
}
$sql = "INSERT INTO post (title, content) VALUES (:title, :content)";
try {
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':title', $title);
$stmt->bindParam(':content', $content);
$stmt->execute();
return $jsonResponse("Post added: " . $pdo->lastInsertId());
} catch (PDOException $e) {
return $jsonResponse("Error: " . $e->getMessage());
}
return $jsonResponse("Unkown error!");
});
We have declared $pdo just above. In PHP, the
use keyword is used in anonymous functions (closures) to import variables from the parent scope into the closure's scope. This allows you to access those variables inside the closure. Again, use Resting like in image bellow:
In Body add "Title" and "Content" and send it to "/addPost" route as POST request.
And as you can see data is in SQL database:
Have fun!
GitLab