-
Notifications
You must be signed in to change notification settings - Fork 0
Introduction
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.
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 CoreI strongly recommend using git (or similar) to manage your projects.
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
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)
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.
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).
Any specific operations with model data should be implemented as a method. This way one can store both data and logic together.
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.
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.
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();.
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.
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()
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
TraceLazyLoadgets called, the system will automatically load the T2 class if not loaded yet.
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.
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
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;
}
}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.
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.
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
/homeresolves toResources/img/logo.png - when loaded in
/res/2resolves 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.
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.
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 betweennickandusernameby settingname_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.
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.
For example check _layout.html used in this method in one of my projects.
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.