A PHP backend framework inspired by Symfony and Doctrine, currently in active development
To be used, the framework needs to be integrated into the index.php file, the environment variables need to be loaded and the ROOT_DIR constant needs to be defined, here is a simple example of its implementation :
<?php
use Sirius\Kernel;
use Sirius\http\Request;
require dirname(__DIR__) . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__) , '.env.local');
$dotenv->load();
$kernel = new Kernel();
$request = Request::create();
$response = $kernel->handleRequest($request);
$response->send();
exit();
Routes need to be set up in the config folder as a JSON file named routes.json, in this json file the routes need to be set up as objects inside an array, with the following elements :
- route : the route name
- path : the route path
- controller : the controller's namespace and method, example : controller": "App\Controller\IndexController::indexAction
Here is an example with several routes :
[
{
"route": "index",
"path": "/",
"controller": "App\\Controller\\IndexController::indexAction"
},
{
"route": "blog_index",
"path": "/blog",
"controller": "App\\Controller\\BlogController::indexAction"
}
]
Routes can have wildcard parameters in their path, which need to be set in between brackets. Those wildcars can then be used as arguments for the associated controller method :
{
"route": "blog_show",
"path": "/blog/{category}/{slug}",
"controller": "App\\Controller\\BlogController::showAction"
}
example of a path with several wildcard parameters (category and slug)
Controllers are the endpoints of the routes, they can have the Request as well as the Wildcard parameters as arguments.
They are where the request is processed and where a Response needs to be returned. Generally this is either a rendered html page (from a Twig file) with a parameters array, a JON response, or a redirect.
Rendering in HTML : To render in HTML, the Controller method render() needs to be called, there are two parameters to pass, a link from the templates directory to the correct Twig template file, as well as an optional parameters array, which will pass data which can be reused in the associated template.
return $this->render('/path/to/template.html.twig', ['parameterToPass' => $variable])
Sending a JSON response : To send a JSON response, the method sendJson (which contains two parameters, a data array and an optional status array with a 200 default value).
return $this->sendJson(['response' => $content], 403);
Sending a JSON response : Redirectionc can also be made from the controller, the redirect method needs to be called, an url needs to be passed, an optional flash message can also be passed in an array, with the 'type' parameter, which can be anything (generally success or danger/error) and the 'message' parameter, which is the flash message.
$this->redirect("/", ['type' => 'success', 'message' => "Bienvenue"]);
The abstract controller has several usable methods which are important to know about :
- getManager() : gets the instantiated EntityManager, which is used for everything about Entities and database connections (seen later)
- getUser() : gets the current User if connected and in session.
- createForm(object $entity, array $options(optional)) : instantiates a new Form object
- flashMessage(string $type, string $message) : sets a flash message up for the next request
- render(string $template, array $parameters) : sets the render
- sendJson(array $data, int $status) : sets a JSON response
- redirect(string $path, array $flash = ['type' => null, 'message' => null]) : sets a redirect with an optional flash message
For the database, first of all a database URI needs to be provided, this URI is then parsed and called inside the framework with PDO, an abstraction layer is then added on top of it, with a Doctrine inspired Entity system. The three major parts of an entity is the Entity class, the associated repository, and the associated JSON config file which contains all the needed information about the entity. The EntityManager class (which can be called in controllers with $this->getManager()) is the "brain" of the entity system and coordinates everything together. Let's see all of this in details.
The database needs to be set up in a database.json file in the config directory. The recommanded way is setting the URI (which is a sensitive information) in the .env.local file and calling it in the database.json file as en environment variable, like this:
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
{
"database" :
{
"url" : "$_ENV['DATABASE_URL']"
}
}
An Entity is a PHP Class with properties and setter and getter methods (as well as custom methods if needed) that abstracts MySql data as PHP objects and makes it easy to manipulate. If you are familiar with Doctrine Entities you will easily recognize the patterns here, but there are some subtle differences. For example, the associated information used for the database is not set as annotations but as a JSON file with all the needed information (more below in the Entities Config section). Here is an example of a part of an entity :
namespace App\Entity;
class Post
{
/* Properties */
/**
* @var int
*/
private int $id;
/**
* @var string|null
*/
private ?string $title = null;
/* Methods */
public function __construct()
{
/* Properties can be set here */
$this->title = 'Default title';
}
public function getId(): ?int
{
return $this->id;
}
public function setId(int $id)
{
$this->id = $id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title)
{
$this->title = $title;
}
}
The repository is where the entity or entities are retrieved and hydrated, it consists of a class extending an abstract Repository class. The most important part here is having to pass the entity name as a string.
Example :
<?php
namespace App\Repository;
use Core\database\EntityManager;
use Core\database\Repository;
class PostRepository extends Repository
{
public function __construct(?EntityManager $entityManager = null)
{
parent::__construct($entityManager, "post");
}
}
The entities need JSON configuration files that need to be placed in the /config/entities/ folder with the same name as the entity. Those files need to contain :
- The repository namespace
- the entity namespace
- the entity name
- the entity database table name
- the fields of the entity, which are the properties, those fields need to have as object title the name of the entity property, they also need to contain :
- the fieldName which is the table field name
- the type (json, string, text, datetime, bool, association). If the type is association, it is also needed to precise some information about the associated entity :
- associatedEntity: the name of the associated entity
- repository: the namespace of the associated entity's repository
- if there are any, the childrenEntities, which are associated entities that need to also be removed if the parent entity is deleted.
- the title of the json should be the associated entity
- like for association type entities, it is needed to provide the entity name with the associatedEntity field.
Example :
{
"repository" : "App\\Repository\\PostRepository",
"entity" : "App\\Entity\\Post",
"name" : "post",
"table" : "post",
"id" :
{
"type" : "integer"
},
"fields" :
{
"title" :
{
"fieldName" : "title",
"type": "string"
},
"category" :
{
"fieldName" : "category_id",
"type": "association",
"associatedEntity": "category",
"repository": "App\\Repository\\CategoryRepository"
}
},
"childrenEntities" :
{
"Comment":
{
"associatedEntity" : "comment"
}
}
}
The Entity Manager serves as the coordinator, it gets, parses and passes the entities data, connects to the database through PDO, passes the queries, is used to add, modify or remove entities.
EntityManager has several important methods to know :
- getEntityData(string $entityName) : gets and parses the data about an entity
- ::getAllEntityData() returns the data about all the entities, it's a static method that can be used anywhere.
- save(object $entity) prepares a statement to save a new entity in the database
- update(object $entity) prepares a statement to update an existing entity
- remove(object $entity) prepares a statement to delete an existing entity
- flush() execute the prepared statements one after the other
- getStatements() get the actually prepared statements (they don't stay between requests)
Security is an important part of any web application and framework, and this framework comes with several features to enhance this.
The firewall is set up in /config/security.json, the important part inside this file is the firewalls part, which has three parameters :
- a pattern for the affected paths
- which roles have access to this path
- an optional redirect parameter, to which the user needs to be redirected if the access is denied
Example :
{
"firewalls":
{
"admin":
{
"pattern": "/admin",
"roles":
[
"ROLE_ADMIN"
],
"redirect": "/login"
}
}
}
- There is a built in feature which verifies a logged user's session details and compares them to the database information. If there is a difference the user is disconnected. If you don't want your users to be disconnected after a profile or account change for example, it is recommanded to update the session so that the user's session information stays up to date with the new entity data.
- PDO's prepare and execute methodology is used so that no query is directly used as is for database interactions. Also entity datas are abstracted as much as possible, more than ease of use and conformity, it also makes database interactions with user inputted data more secure.
- The request links are sanitized as early as possible during the request processing.
- The integrated Forms functionality (seen below) incorporates several sanitizing and verification processes by default. To name a few : CSRF tokens, input sanitization, entity compatibility testing if needed
The Form class is an in-built system that makes it easy to rapidly do secure forms that can be associated or not with an entity (and automatically adds the processed input data into it). In this framework, forms are very customizable with lots of option that can be entered in an array. Here are some examples of forms :
Contact form :
$this->addTextInput('fullName', ['required' => true, 'class' => 'form-control', 'placeholder' => 'Prénom / Nom'])
->addEmailInput('email', ['required' => true, 'class' => 'form-control', 'placeholder' => 'Email'])
->addTextareaInput('content',['class' => 'form-control', 'placeholder' => "Ecrire un message", 'value' => '', 'label' => 'Message', 'rows' => 5, 'cols' => 35])
->setSubmitValue('Envoyer', ['class' => 'button-bb-wc-2 as-c br-5 mt-1'])
->addCheckbox('consent', ['class' => 'text-muted', 'label' => 'I consent to share my personal data', 'entity' => false,])
->addCss('d-f fd-c');
Register form :
$this->addTextInput('username', ['class' => 'form-control', 'placeholder' => "Nom d'utilisateur"]);
$this->addEmailInput('email', ['required' => true, 'class' => 'form-control', 'placeholder' => 'Email']);
$this->addPasswordInput('password', ['required' => true, 'class' => 'form-control', 'placeholder' => 'Mot de passe', 'hash' => true]);
$this->setSubmitValue('accepter', ['class' => 'button-bb-wc']);
Blog post creator/editor form :
$this->addCss('w-75')
->addTextInput('title', ['class' => 'form-control js-binder', 'placeholder' => "Titre", 'dataAttributes' => ['type' => 'text', 'target' => 'slug', 'target-attribute' => 'value', 'options' => ['slugify' => true]], 'value' => 'edit' === $options['type']? $entity->getTitle() : null])
->addTextInput('metaTitle', ['class' => 'form-control', 'placeholder' => "Méta titre", 'value' => 'edit' === $options['type']? $entity->getMetaTitle() : null])
->addTextInput('metaDescription', ['class' => 'form-control', 'placeholder' => "Méta description", 'value' => 'edit' === $options['type']? $entity->getMetaDescription() : null])
->addTextInput('slug', ['class' => 'form-control', 'placeholder' => "Slug", 'value' => 'edit' === $options['type']? $entity->getSlug() : null])
->addSelectInput('category', $options['selection'], ['class' => 'form-control w-75', 'placeholder' => 'choisissez une catégorie', 'label' => 'catégorie :', 'targetField' => 'id', 'selected' => $category])
->addDateTimeInput('createdAt', ['class' => 'form-control', 'placeholder' => "Date de publication", 'value' => 'edit' === $options['type']? $entity->getCreatedAt()->format("Y-m-d\TH:i:s") : null])
->addDateTimeInput('updatedAt', ['class' => 'form-control', 'placeholder' => "Date de modification", 'value' => 'edit' === $options['type']? $entityUpdated : null])
->addSelectInput('status', $Selection, ['class' => 'form-control w-75', 'placeholder' => 'Publié', 'label' => 'Statut :', 'targetField' => 'status', 'selected' => $selected])
->addSelectInput('featured', $Selection, ['class' => 'form-control w-75', 'placeholder' => 'En vedette', 'label' => 'En vedette :', 'targetField' => 'featured', 'selected' => $selectedFeatured])
->addHiddenInput('mediaHiddenInput', ['entity' => false, 'class' => 'js-binder', 'required' => false,'dataAttributes' => ['type' => 'image', 'from' => 'modal', 'target' => 'previewImage']])
->addButton('mediaLibrary', ['class' => 'js-modal button-bb-wc m-1', 'value' => 'Galerie média', 'type' => 'button', 'dataAttributes' => ['target-modal' => 'mediaModal']])
->addDiv('mediaShow', ['class' => 'hrem-15 js-filler pt-1', 'dataAttributes' => ['type' => 'image', 'id' => 'previewImage', 'class' => 'mh-80 d-b mw-100', 'src' => 'edit' === $options['type']? $media : ''], 'wrapperClass' => 'mt-1', 'label' => 'Prévisualisation du média principal'])
->addHiddenInput('header', ['sanitize' => false])
->addHiddenInput('content', ['sanitize' => false]);
As we can see this system is very customizable and robust. We can add input types and pass some data, a name and options in an array, for each input field.
To instantiate a form there are several ways :
- the controller methode createForm(object $entity, array $options = [])
- directly insantiating the Form class (Request $request, object $entity, Session $session, array $options = [])
- extending the Form class, adding your form inputs here and instantiating this new form type. This is recommended to declutter the controller.
Here is an example using the third method :
$editorForm = new EditorForm($request,$post, $this->session, ['name' => 'newPost','submit' => false, 'selection' => $selection, 'type' => 'new', 'wrapperClass' => 'mb-1']);
The form class can have several options passed to it in the $options array :
- method : the form method (POST by default)
- name : the form name, optional
- submit : if set and false, disables the submit button, true otherwise
- wrapperClass : sets classes for the wrapper divs of every input if defined, has to be a string
- errorClass : sets the CSS for error events for every input if defined.
- action : sets the action path of the form if defined, else it will set the current request's path as the action path.
- sanitize: if defined and set to false, the inputs wont be sanitized
- addTextInput(string $name, array $options)
- addDateTimeInput((string $name, array $options)
- addHiddenInput((string $name, array $options)
- addButton((string $name, array $options)
- addDiv((string $name, array $options)
- addTextareaInput((string $name, array $options)
- addPasswordInput((string $name, array $options)
- addCheckbox((string $name, array $options)
- addSelectInput((string $name,array $selection, array $options)
- addFileInput((string $name, array $options)
- addCss(string $classes)
- setSubmitValue((string $name, array $options)
inputs need to be named. If they are to be associated with an entity property, this name needs to be the same as the associated property.
Each input has their specific available options, like placeholders, default values etc, here are some of them.
- sanitize : if false the input is not sanitized, else it is by default
- hash : if true and it is a password type then it is hashed, else it's false by default
- entity: if set and false then the input is not mapped with an entity property (it can be retrieved with Form's getData() method)
- value : an input's default value if compatible
- modifyIfEmpty : if set and false, doesn't modify a field if the input is left empty, else it does by default (if the input is mapped)
- class : add classes to the input
- targetField : for association types entity properties, gives the ability to select a custom index to search an entity in the database. Defaults to ID.
- placeholder : Input placeholder
- label : Input label
- wrapperClass : Input's wrapper div class
- dataAttributes : can set data attributes
To render a form, it needs to be passed with the method renderForm(), this can be passed to twig or even as an AJAX call
return $this->render('/form_page.html.twig',[
'form' => $form->renderForm()
]);
this can then be rendered easily :
{{ form.render|raw }}
The form can be used as AJAX data, by using the form.data (which gives raw data about the form and the inputs) instead of form.render (which renders the form, with the html already generated)
If used, the email settings need to be configurated inside /config/email.json
Defaults
- email : sender email address
- name : sender name
- timezone : actual timezone
phpmailer
- username : authentication for the smtp service
- password : authentication for the smtp service
- host : SMTP hostname
- port : SMTP host port
- smtpAuthentication : if authentication is needed or not for the SMTP
- SMTPSecure : Security type
Example :
{
"default" :
{
"email": "$_ENV['EMAIL_USERNAME']",
"name": "domain.com",
"timezone": "Europe/Paris"
},
"phpmailer" :
{
"username": "$_ENV['EMAIL_USERNAME']",
"password": "$_ENV['EMAIL_PASSWORD']",
"host": "smtp.gmail.com",
"port": "587",
"smtpAuthentication": true,
"SMTPSecure": "TLS"
}
}
- addReceiver(string $reveicer) : set receiver address of the email
- subject(string $subject) : set the subject of the email
- sender(string $email, string $name) : change the sender email and name
- addRepleyTo(string $email) : adds a reply-to address
- setContent(string $content) : set the html content of the email
- setRender(string $template, array $parameters = []) sets a twig template render with optional parameters like for controller renders but for an email
- send() : send the email
The framework includes an extensible command system, with some base commands that can be used to easily generate new entities or controllers for example. To use a command you need to call the main managing script and precise the command as well as the necessary options or arguments like this :
php core/CommandManager.php CommandName --optionName argumentName=argumentValue
Here are some commands that are provided with the framework :
- CreateEntity : Creates an entity
- alias : create:e
- options :
- newtable : creates a migration file of the entity, ready to be inserted into the database
- CreateController : Creates a controller with a CRUD boilerplate
- alias : create:c
- arguments :
- subfolder : Specifies a subfolder inside Controller in which to insert the new controller.
- options :
- setroute : Specifies that a new route has to be created in the routes config file in conjunction with the controller
- ExecuteMigration : migrates and adds a new table from a new entity to the database
- alias : e:m
- options :
- latest : Indicates that the latest migration file should be exexuted
the command configuration is done inside /config/commands.json, active commands need to be added in this file with a name and the namespace of the command, like this :
{
"HelloWorld":
{
"name": "HelloWorld",
"class": "Core\\commands\\HelloWorldCommand"
}
}
Commands need to be extended from the Command class. each command needs to be configured inside the configure() method, this is where the name of the command, the alias, the options, the arguments and the description are added. The actual command execution code has to be put inside an execute() method, here is an example :
<?php
namespace Core\commands;
class HelloWorldCommand extends Command
{
public function configure()
{
$this->setName('HelloWorld')
->setAlias('hlw')
->setDescription('writes Hello World')
->addArgument('argumentName', 'argument description')
->addOption('optionName', 'option description');
}
public function execute()
{
echo "Hello World" . PHP_EOL;
}
}
Several classes and methods can be used to help with different tasks, they can mainly be found in the Core\utils namespace :
There are two methods :
- paginateArray(array $content, int $currentPage, int $limit): ?array
- It is used to paginate an already queried array of items. This method doesn't touch the database. It returns the items that should be kept for the current page.
- paginate(Repository $repository, int $currentPage, int $limit, string $column = null, string $order = null, string $row = null, string $criteria = null): ?array
- It queries the database based on the current page and the number of items in the tabl. Then it returns the number of pages, the current page and the kept items for the current page
They are static funtions that can help with strings :
- normalizeForComparison(...$strings): array
- it returns all the passed strings as lowercase strings inside an array
- changeTypeFromValue(string $string)
- it is used to change the type of a string that could be another type, for example a string with "true" or "false" or an int.
- slugify(string $str)
- slugifies a string
- parseFile(string $path)
- It is used to parse json files into PHP arrays. It is used extensively to parse the config files.
- getClassNameFromObject($object)
- gets the class name from an instantiated object
MIT