Advanced

This section will dive deeper into each aspect and feature of the mini-framework, to understand how everything works to get started.

API

Most APIs requires an endpoint for each operation. Let's take a simple case of CRUD methods on a `user` table. Since we have 4 methods, we will require 4 endpoints for example: `/user/create`, `/user/show`, `/user/edit`, `/user/delete`.

For testing and learning purposes you can run the following command:

            composer migrate:apply
            
        

If you had already configured your database, this will create a `user` table automatically through migration (more on that later).

The API is setup in a way to have a single endpoint, and the method requested is sent with the data.

Any data sent through the API must be in JSON format (and any received data is in the same format).

The data must contain the method requested (and any parameters if they are required).

Here is an exemple below for creating a user:

            
{
    "method": "create",
    "params": {
        "email": "user@email.com",
        "password": "user123"
    }
}
            
            
        

Any request to an API endpoint goes through a middleware app/config/Middleware.php.

The middleware handles the request type, content type, authorization header, sanitizes and validates the data before passing it to a controller.

After multiple steps, the controller assigned to the endpoint will then take the parameters from the data sent and executes the method requested.

Let's take the `create` method for a user as an example:

            
// User create
public function create()
{
    $user_id = Middleware::generateId();
    $email = $this->validateParams('email', $this->param['email'], EMAIL);
    $password = $this->validateParams('password', $this->param['password'], STRING);
    $model = new UserModel;
    $model->setUserId($user_id);
    $model->setEmail($email);
    $model->setPassword(Middleware::hash($password));
    $this->result = $model->create('user');
    if ($this->result == false) {
        $message = 'User already exists.';
    } else {
        $message = "User created successfully.";
    }
    Middleware::returnResponse(RESPONSE_MESSAGE, $message, $this->result);
}
            
            
        

After validating the parameters, we create an instance of the model and assigns those parameters to the models properties with the helps of setters before executing the create method of the model.

            
// User creation
public function create($table)
{

    $sql = "SELECT email FROM $table";
    $stmt = $this->db_conn->prepare($sql);
    $stmt->execute();
    $emails = $stmt->fetchAll(PDO::FETCH_COLUMN);

    if (!in_array($this->email, $emails)) {
        $sql = "INSERT INTO $table (user_id, email, password)
        VALUES (:user_id, :email, :password)";
        $stmt = $this->db_conn->prepare($sql);
        $stmt->bindParam(':user_id', $this->user_id);
        $stmt->bindParam(':email', $this->email);
        $stmt->bindParam(':password', $this->password);
        if ($stmt->execute()) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}           
            
            
        

Sending the above mentioned JSON data to the `/user` endpoint will get a response with a code and a message:

            
{
    "response": {
        "status": 200,
        "message": "User created successfully."
    },
    "result": true
}            
            
            
        

Or if a user already exists:

            
{
    "response": {
        "status": 200,
        "message": "User already exists."
    },
    "result": false
}            
            
            
        

You can check the other methods (`readAll`, `readUnique`, `update`, `delete`), see what parameters they require and test them to get the hang of it.

While testing the `update` or `delete` method you will get the following error message:

            
{
    "error": {
        "status": 412,
        "message": "Access Token Not found"
    }
}
            
            
        

Since their methods in app/controllers/UserApiController contains in their first line $this->validateToken(); that verifies the existence and status of the token.

Authentication is handled with the app/controllers/AuthApiController and app/models/AuthModel and is token based using JWT (JSON Web Tokens).

After creating a user and trying to login:

            
{
    "method": "login",
    "params": {
        "email": "user@email.com",
        "password": "user123"
    }
}
            
            
        

A token will be generated:

            
{
    "response": {
        "status": 200,
        "message": "Token generated successfully"
    },
    "result": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
        eyJpc3MiOiJsb2NhbGhvc3QiLCJpYXQiOjE2MzcwNzIxNjUsImV4cCI6MTYzNzA3MzA2NSwiYXVkIjoidXNlciIsImRhdGEiOnsidXNlcl9pZCI6ImM2MTM5MTIxNDk2MWIiLCJlbWFpbCI6InRlc3RAZW1haWwuY29tIn19.
        O_572ENGXh_JiMiVYdPBFX4lj725W1u5yvvDfivjTvI"
    }
}
            
            
        

Any request requiring the validation of the token must be sent with the token in the authorisation header (of type `Bearer Token`).

Tokens have an expiration time $exp = $iat + (60 * $minutes); that can be defined in app/controllers/AuthApiController and by default it is configured to last for 30 mins $minutes = 30;.

The expiration time is calculated from the current time: $iat = time(); plus how many seconds it takes for it to expire.

To make it simple to configure, just replace the value of $minutes to how many minutes you want the token to last.

If the token expires you will get the following error message:

            
{
    "error": {
        "status": 401,
        "message": "Expired token"
    }
}
            
            
        

And you will need to re-login.

Response messages can be turned off by running the following command in your terminal:

            composer responses:off
            
        

You will then only get the result.


Controllers & Models

To create a controller, you can run the command:

            composer make:controller
            
        

This creates both a controller and a model since they rely on each other. They will be generated based on templates, the current state of the table concerned and will contain basic CRUD methods to use or base on to create your own methods.

You will be asked several questions and the generated files will depend on your answers.

Keep in mind that creating a controller/model with this command will require an existing table (either created normally or through migration) since it needs it's name and it's columns (the table must also not have an existing controller/model already created for it).

For every controller method you can add $this->validateToken(); on the first line for token validation. This ensures the execution of the method only if the token sent is valid.

By default on every controller creation, it will be already declared in the `update` and `delete` methods.


Routing

Your routes are defined in public/index.php:

            
use Dotenv\Dotenv;
use App\Application;
use App\Controller\WebController;
use App\Controller\AuthApiController;
use App\Controller\UserApiController;

require_once dirname(__DIR__) . '/composer_vendor/autoload.php';

$dotenv = Dotenv::createImmutable('../');
$dotenv->load();

$app = new Application(dirname(__DIR__));

// * WebController manages the Web side of the framework
// Web routes for views : GET
$app->router->get('/', [WebController::class, 'home']);
$app->router->get('/home', [WebController::class, 'home']);
// Web routes for views : POST

// * ApiControllers manages the API side of the framework with the Middleware
// API routes for endpoints : POST
$app->router->post('/auth', [AuthApiController::class, 'processAuth']);
$app->router->post('/user', [UserApiController::class, 'processUser']);

// Run the app and router, resolve paths and request methods and render different layout depending on callback
$app->run();
            
            
        

When the user requests an url and therefore a route. It is processed and checked for it's existence then for its type.


Views

A view is basically the page that is requested by a user in the form of a route that is then served to the user to view the contents requested.

Views are handled with a components system that makes it easier to manage each block of a view in a way when a modification has been made to a certain component, it will be reflected in all views/pages where the component is present. This helps avoid repetitiveness and keeping your code DRY and manageable.

Layouts are under the app/views/layouts folder and can be composed of static components that are present in all views using that layout and must contain a content component.

Static components are wrapped inside double brackets [[ static ]] and are under the app/views/components/static.

The content requested is any file under your app/views folder and must be wrapped inside parentheses (( content )).

A layout can be setup like this:

            
<!DOCTYPE html>
<html>

<head>
    [[ head ]]
</head>

<body class="base">
    <header class="header">
        [[ header ]]
    </header>
    (( content ))
    <footer class="footer">
        [[ footer ]]
    </footer>
</body>

</html>
            
            
        

Any content/view file can contain plain html or dynamic components that are not meant to be present in all pages since they are not part of the layout but are proper to a certain view itself.

Dynamic components are wrapped inside double curly braces {{ dynamic }} and are under the app/views/components/dynamic.

A content/view can look like this:

            
<main class="main">
    <h1>Hello World!</h1>
    <section class="slider">
        {{ slider }}
    </section>
</main>
            
            
        

Let's say this view is the home view which will be app/views/home.php and is using the above mentioned layout as app/views/layouts/base.php and will have a route setup in public/index.php as follow:

            
$app->router->get('/', [WebController::class, 'home']);
$app->router->get('/home', [WebController::class, 'home']);
            
            
        

And will have a view setup in app/controllers/WebController.php like this:

            
public function home()
{
    return ViewsController::render('base', 'home');
}
            
            
        

The {{ slider }} string inside the app/views/home.php will be automatically replaced by the app/views/components/dynamic/slider.php file inside this content.

Then the resulted html will be included inside the layout, replacing the (( content )) string and including any static components in the process.

All of this is done automatically by the app/core/Components.php class that scans the files and replaces the component strings by including the respective file from their directories.


Migrations

Migrations are the process of creating/modifying tables of a database in a sequenced defined order. It offers a way to have like a version control or a history of past actions made to the database tables.

It is also very useful in the development process to quickly create tables to work with and modify them, even drop them and re-create them in case a mistake occurs when setting up your schema.

All your migration files are under app/migrations. Two exemples are already included and set up to create a `user` table and to add another column after it is created.

            
namespace App\Migrations;

use App\Config\Dbh;
use App\Core\Scripts;

class m0001_create_user_table extends Dbh
{

    public function migrate()
    {
        Scripts::log("Creating user table...");
        $db_conn = $this->connect();
        $stmt = $db_conn->prepare("CREATE TABLE IF NOT EXISTS user (
                user_id VARCHAR(16) PRIMARY KEY NOT NULL,
                email VARCHAR(32) NOT NULL UNIQUE,
                password VARCHAR(128) NOT NULL,
                user_created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                user_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)");
        $stmt->execute();
    }
    
    public function drop()
    {
        Scripts::log("Dropping user table...");
        $db_conn = $this->connect();
        $stmt = $db_conn->prepare("DROP TABLE IF EXISTS user");
        $stmt->execute();
    }
}
            
            
        

You can test it while running the command:

            composer migrate:apply
            
        

The above command will run all the migrations in a sequencing order defined by their prefix and id. After checking if the table has been created and modified successfully. You can run the command:

To drop all currently applied migrations. This will only delete tables created through migration. Any tables created manually will not be affected.

            composer migrate:drop
            
        

To create a migration is very simple by running the following command:

            composer make:migration
            
        

This will create a migration file based on a defined template, while asking for informations through user input in the terminal to customize it.

All your created migrations will be automatically put into the app/migrations folder and can be applied while running the composer migrate:apply command.


Seeding

Seeders are a way of inserting predefined data to existing tables. It can either be essential static data or dummy data which is meant for testing purposes.

They are usually used in conjunction with migrations, but are not tied to them.

All seeders are under the app/seeders folder. A UserSeeder class is already included and can be tested right after your create you `user` table.

            
namespace App\Seeders;

use App\Config\Middleware;
use App\Config\Seeders;

class UserSeeder
{

    public static function seedUser()
    {

        // Data that will be seeded to the table, If you add any column into your table be sure to add its key and value or else it wil be seeded as 'NULL'
        Seeders::create('user',
        [
            'user_id'   => Middleware::generateId('admin'),
            'email'     => 'admin@email.com',
            'password'  => Middleware::hash('admin'),
            'fullname'  => '',
        ]);

        Seeders::create('user',
        [
            'user_id'   => Middleware::generateId('user'),
            'email'     => 'user@email.com',
            'password'  => Middleware::hash('user'),
            'fullname'  => '',
        ]);
    }
}
            
            
        

To run the UserSeeder, you can run the command:

            composer seed:user
            
        

To create a seeder, you must run the command:

            composer make:seeder