Skip to content

Introduction

AzCraft edited this page Mar 13, 2023 · 16 revisions

Base concepts

The project has 3 main parts:

  • The Routing system handles requests
  • The ORM system provides interface to MySQL database
  • The HTML formatting system (aka Template system)

It should remind you of MVC.

Naming files and using namespaces

The framework resides inside the \Core namespace. The directory structure for classes that would be autoloaded (strongly recommended for all classes) should follow the \(<Namespace>\)*<Class Name>.php format. (case sensitive)

Therefore creating a project begins with:

git init
git submodule add https://github.com/AstralShadow/PHPFramework.git Core

I strongly recommend using git (or similar) to manage your projects.

The Routing system

The routing system parses the accessed URL and HTTP method and picks how to handle the request.

"Handling a request" consists of passing a Request object to the proper handler. Handlers are static methods contained in Modules (a fancy name for classes) and have attributes that tell the Routing system when to pick them.

An example of a module would be:

// A bunch of use statements omitted

class Home
{
    #[GET]
    public static function index()
    {
        // Handles requests to root path

        // Do something and return a RequestResponse
    }

    #[GET("/app")]
    #[GET("/app/{id}")]
    public static function myAppFunction(Request $req)
    {
        // Handles requests to /app and /app/<id>

        // Use req to access more data about the request
        //  such as the value of id in the path
        if($req->var("id") != null) {
            // You can use $req->id, but it will
            //  throw an exception when processing "/app"
            // Accessing trough var() returns null instead
            $req->id;
        }

        // Return a RequestResponse
    }

    #[Fallback]
    public static function notFound()
    {
        return new ApiResponse(404);
    }
}

Further descriptions about the Routing system can be found in

The ORM system

Object–relational mapping (ORM, O/RM, and O/R mapping tool) in computer science is a programming technique for converting data between a relational database and the heap of an object-oriented programming language. This creates, in effect, a virtual object database that can be used from within the programming language. Wikipedia

The ORM system handles objects that map to database entries. These objects are defined using classes that inherit from Core\Entity and are currently referred to as "models". A model represets a single table in the database. (or rather a view over it)

Table names and primary keys

Models have to be described using the Core\Attributes\Table attribute, which takes the table name in the database as first and only argument, and Core\Attributes\PrimaryKey, which takes one or more string arguments - the names of the columns used as primary keys. Currently every table has to have at least one primary key column because object storage and all non-search queries operate with primary keys.

Data fields

Models contain public (or protected) fields for every column in the table they represent. Private fields are not accessed by Core\Entity and can be used to store state that would get destroyed at the end of the current request. Be careful when storing state in private fields, as the constructor is not called when fetching an entry.

Supported data types for mapped fields are int, string, float, \DateTime and any model that has a single id field. In case a model is used the database stores the value of the id and has the same data type as the id of the referenced model.

Currently referencing a model with composite primary key is not implemented. You can store the primary keys instead and access the objects by calling Model::get(...$key_values).

Model logic

Any specific operations with model data should be implemented as a method. This way one can store both data and logic together.

Creating objects / Inserting data

Inserting a new database entry happens when constructing an object. If data initialization is required the model constructor has to explictly call parent::__construct() after populating all needed fields.

Modifying objects

The public method save() saves any changes in the data fields. One has to call it after modifying an object to store the modifications.

_Calling parent::_construct() after initializing the data fields saves a call to save(), as the data is being sent with the create query. You can also use a call to save instead, for example, if you need to know an auto_increment primary key to finish object creation.

Reloading objects

Once fetched from database (including after being created), objects are cached. In general this is good, because it removes unneeded queries, but in some cases (such as SSE) reloading is necessary.

The cache is not reloaded automatically as in most cases the lifetime of the PHP script handling this request is so short that it doesn't really matter.

To update an object call $object->load();.

Deleting objects

The Core\Entity class provides a static delete method which takes either an instance of the model or all id-s of the entry being deleted.

In practices some developers prefer marking an entry as deleted instead of deleting it. This allows for deleted data to be restored. On the other side, it leads to increased storage costs, so you should prepare a system that cleans up old deleted entries if you decide to not delete entries.

Searching objects

The Core\Entity class provides a static find(array $conditions) method to every Model. The $conditions argument is a named array containing "field_name" => value pairs. Querying with [] returns all entries in the database.

Currently the find method fetches all columns of the matched entries and uses them to populate all uncached objects. This will be fixed, but for now refrain from using unneeded calls to find()

Traceable fields (One-to-Many)

For easier work with One-to-Many relations the attribute Core\Attributes\Traceable was introduced. This attribute can be set only for fields of type that inherits from Core\Entity. The type of the field will be referred to as T1 and the type of the model which contains the Traceable attribute will be referred to as T2

The Traceable attribute takes a name of a method that will be dynamically added to T1 by the Core\Entity class. (Make sure it does not collide with any existing or dymanically added method.)

The added method can be used to find all elements of T2 which point to T1 with the field described by the Traceable attribute. This method can take the same arguments as find(), effectively allowing to search within all objects of T2 referencing T1.

It is important to note that this functionality requires both models T1 and T2 to have been loaded. As this proves to be an issue and a drawback, the Core\Attributes\TraceLazyLoad was introduced.

The TraceLazyLoad attribute can be added to T1. It takes 2 attributes:

  • The full name of the model that provides the method (T2)
  • The name of the provided method When one of the methods described with TraceLazyLoad gets called, the system will automatically load the T2 class if not loaded yet.

Limitation notes

Currently the ORM system supports only part of the MySQL syntax. It is being developed on as-needed basis, so i haven't added features i haven't needed. That being said, it should be sufficient for all common tasks.

Database managment (creation and modification of database tables) has to be done manually. I strongly recommend storing all of your database modifications as number_name_date.sql somewhere in your application repository. The same applies for database creation files.

TODO Provide a few scripts to automatically apply migrations.

Example

TODO Provide a sufficient minimalistic examples.

Until i get this done, you can check some of the projects in the example projects list in README.md

The Template system

Templtaes are files that can have variables provided from modules in them. Templates are most commonly html files, but can be other text resource types as well.

Templates are contained inside the Templates/ directory.

An example of a template Templates/welcome.html would be:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>${title}</title>
    </head>
    <body>
        <h1> Welcome to ${title}! </h1>
    </body>
</html>

And a module using it:

<?php

namespace Extend;
use Core\Responses\TemplateResponse;

class Welcome
{
    #[GET]
    public static function index()
    {
        $response = new TemplateResponse(file: "welcome.html");
        $response->setValue("title", "My page");
        return response;
    }
}

Default values

The syntax for setting a default variable value is ${varname=default_value}

Variables that don't have default value set have $varname as default value.

Commands

Aside from variables, the Template preprocessor also supports a few commands. They have the common syntax ${command:value=default_value}

Currently there are two of them, with two aliases for each.

Resource paths

With html being abstracted from application logic it's often possible that the same html template gets returned for paths with different depth. (Example: layout.html can be used both in /home and /res/2)

This means that having relative resource paths in layout.html won't work. We can't expect something like img/logo.png to be found both from /home and /res/2. Usually a solution would be to use /img/logo.png instead.

Sadly, this forces our webapp to reside on the / uri of the domain. This isn't really an issue, as most apps do so, but i do like proxy forwarding my apps as https://example.com/app1/.

My solution to this was adding the path: (alias is resource:) command. The template preprocessor will automatically prepend the needed count of '../' to the path argument, creating a relative resource path that is correct for the currently visited URL.

All resources should be stored in Resources/. The path: directive prepends this to the resource path. This is done for the sake of keeping resources separate from the application.

For example using ${path:img/logo.png} works like:

  • when loaded in /home resolves to Resources/img/logo.png
  • when loaded in /res/2 resolves to ../Resources/img/logo.png

I still have a few concerns on how would the framework detect which directory does it reside in relative to the current path.

Nested templates

Templates have one more feature - they let you include other templates inside them. This has multiple uses, the most obvious being not repeating commonly used code.

The command for this is ${include:file}, with ${parse:file} being a shorter alternative.

The following example inserts header.html and footer.html around the div#container element:

<!DOCTYPE html>
<html>
    <head>
        ...
    </head>
    <body>
        ${include:header.html}
        <div id="container">
            ...
        </div>
        ${parse:footer.html}
    </body>
</html>

If any of the included files has any variables or commands, the Template preprocessor will parse them as well.

Nested template preprocessor directives

The preprocessor can parse nested ${...} directives. They are useful for a series of stuff, for example:

  • Selection between used variables: ${${name_filed}=Unnamed} which can let you select between nick and username by setting name_field

  • Complex default values such as ${name=${first_name} ${second_name}} which you should not do, see below.

For security sake (or rather, keeping all vulnerabilities at one place), the template preprocessor will only substitute nested variables with macros.

This is done because once the nested tag gets processed, it will then get parsed again as part of the one that was wrapping it.

Template Macros

Macros are a bit more wild that what you've seen so far. They are just like variables, except you can actually inject more ${...} tags in them and they get parsed.

This can be done using the setMacro(string $name, $value) or setMacros(array $key_value_pairs) methods.

You being able to inject code means that anyone who can write there can inject code!!!

!!! DO NOT PUT USER INPUT AS MACROS !!!

You have been warned.

Tips and Tricks

Creating a common _layout.html page.

For example check _layout.html used in this method in one of my projects.

Including a section based on permissions

By using something as ${parse:can_edit=edit.html} you can show or hide the editing section with the following code block:

    if(!$user->canEdit)
        $page->setValue("can_edit", "");

This block can happily stay in a template factory such as the one referenced above.