Table of Contents

Let's Talk - Building a Modular PHP Framework part 3

Author(s): Louis Ouellet


In this third installment of our Building a Modular PHP Framework series, we will cover:

We'll build upon the groundwork set up in the previous parts, focusing on security (CSRF), setting up API routes, and more advanced database handling.

Cross-Site Request Forgery (CSRF)

What is CSRF? Cross-Site Request Forgery (CSRF) is a type of web security vulnerability that allows an attacker to trick users into performing unwanted actions on a website or web application where they are already authenticated. Essentially, the attacker exploits the user's browser session (including cookies) to send unauthorized requests, often without the user's awareness.

To protect against CSRF, we generate and validate a secret token that must be included in all non-GET requests.

We ensure our session is started in the Bootstrap class:

// Start the session
if (!defined('STDIN') && session_status() === PHP_SESSION_NONE) {
    ini_set('session.cookie_samesite', 'Strict');
    ini_set('session.cookie_secure', 'On');
    session_start();
}

Then we create our CSRF class:

Source: CSRF.php

The CSRF class:

Setting Up Apache Configuration (.htaccess)

To ensure our application can handle routes correctly, we create two .htaccess files: one in the root directory and one in the webroot subdirectory.

Source: .htaccess

Source: .htaccess

These rules ensure that calls are properly routed through our index.php, and any CLI or .htaccess direct calls are disallowed.

Building the API Module

The API module allows us (and potentially third parties) to request, exchange, or synchronize data with our application. Our API logic is contained in two main files: API.php and Endpoint.php.

Source: API.php

Source: Endpoint.php

How it works:

  1. We parse the request's namespace to figure out the endpoint and the action.
  2. We attempt to load a matching class from /Endpoint or from a plugin directory.
  3. If it exists, we instantiate it and check if the requested method is present.
  4. Optional authentication and permission checks are performed.
  5. The action is executed, and we return the result as JSON with the appropriate HTTP status code.

Database Handling

In part 2, we introduced models. Now, we'll refine how we handle the database itself, including queries and schema management. Let's break down the various classes.

The Database Class

Sources: Database.php

This class loads our database configuration and spins up the correct Connector based on our config file. It also provides helper methods for creating new Query or Schema objects.

Connector and MySQL Classes

Sources: Connector.php

Sources: MySQL.php

Connector is an abstract class; each database engine (e.g., MySQL, SQLite, PostgreSQL) implements its own version of these methods. In our example, we've only implemented the MySQL connector.

The Query Class

Sources: Query.php

The Query class allows you to build SQL queries in a structured, chainable way. Once you've constructed the query, calling →result() executes it. For instance:

$db = new Database();
$data = $db->query()
    ->select('*')
    ->table('users')
    ->filter()
    ->where('username', 'jdoe')
    ->result();

The Schema and Definition Classes

Sources: Schema.php

Sources: Definition.php

These classes let you manage table structures and column definitions in a more programmatic way. You can:

Conclusion

We have now implemented several key features in our PHP framework:

With these pieces in place, our framework is capable of handling basic security, robust data operations, and modular endpoint structures. In the next installments, we'll expand authentication mechanisms, discuss more advanced routing or plugin architectures, and continue refining our framework to be both extensible and secure.

GitHub Repository

v0.0.11

Tags