# Framework Setup After installing the project, you need to configure the framework itself. In the previous step, the project was installed in the new_project directory (or any other directory name you chose), to execute the following console commands, you need to navigate to this directory: ```$ cd new_project``` The example provided may differ for various console environments. It is assumed that all console commands in the documentation are run from this root project directory unless otherwise specified. If the application is running on a host where the framework’s console commands are not available, they can be executed via the framework’s special Web Console. ## Access Rights Configuration in Linux By default, in DEBUG mode, this permission setting is not necessary, and hosting usually provides advanced permissions, so if the project is in development mode or on a hosting, this step can be skipped. After installing the HLEB2 framework on Linux, it is necessary to configure permissions. To do this, you need to know the web server group's name. Next, here's how you can set extended edit permissions for files in the project's /storage/ directory. The web server may be named www-data, and its group may be named the same www-data. When running the framework, if the permissions are not yet set, an error will be displayed attempting to determine the active web server's name and group. To allow new files created by the web server to be editable via the console by the current user, add the user to the web server group: ```$ sudo usermod -aG www-data ${USER}``` After these group changes, to apply them, you need to log out and log back into the system as this user, or run the following command: ```$ su - ${USER}``` The next check should display 'www-data' in the group list: ```$ id -nG``` Then, extend permissions on the /storage/ directory for the group (from the root directory of the project). ```$ sudo chmod -R 750 ./ ; sudo chmod -R 770 ./storage``` ## Auto-configuration via Console Command After setting permissions, if needed, you can use the framework's own console commands. If the project was installed not via Composer, which should have executed this script automatically (and then removed it), run the command manually: ```$ php console project-setup-task``` This action will perform several minor optimizations of the project that do not directly affect its operability. ## Project Settings The /config/ directory is often used to store the project's settings. If you want to fetch additional settings using the framework, add them to the /config/main.php file in a similar manner to its settings. However, if there are many such settings, it is advisable to use the 'custom.setting.files' parameter from the /config/system.php file and list files containing separate settings. ## Dynamic Settings The 'start.unixtime' parameter under the settings name 'common' contains the UNIX time of the request processing start by the framework in milliseconds. This parameter remains constant throughout the request. ## Class Autoloading A universal class autoloader is provided alongside Composer, and its use is preferred. If a file (class) is not found, an attempt will be made to load it with the framework’s auxiliary autoloader, which follows PSR-0 naming conventions and works independently of Composer. For instance, for the framework's autoloader, the class App\Controllers\ExampleController should correspond to the file /app/Controllers/ExampleController.php in the project. # Optimization ## Class Preloading in OPcache For enhanced performance, add the following directive for the preload.php file in your current php.ini file to precompile the framework's classes and place them in the opcode cache. opcache.preload=/path/to/project/vendor/phphleb/framework/preload.php In this line, replace '/path/to/project/' with the path to your project's root directory. Learn more about preloading in the PHP documentation. Preloading is not supported on Windows. ------------------------- # WebRotor WebRotor is a PHP library that allows asynchronous execution of applications on shared hosting. As is known, shared hosting has many usage restrictions, but this specialized program provides all the benefits of asynchronous functionality even on shared hosting. The core principle of WebRotor is that when a request is made to the application, the index file does not process the requests directly but rather sends them to workers and fetches the responses back for display. Moreover, the worker is actually implemented as the code of this same index file. The workers are standard CRON-like processes, which are now available on practically every hosting provider. The difference in configuring these workers lies only in the designs of hosting providers' admin panels. To use WebRotor, you will need to modify the /public_html/index.php file (which is the presumed path to the index file on shared hosting) so that the HLEB2 framework runs in a loop. Here is a basic working example: ``` logLevel = 'warning'; $config->workerNum = 2; // Must correspond to the number of workers $config->workerLifetimeSec = 120; // Must correspond to the worker launch interval $config->runtimeDirectory = __DIR__ . '/../storage/wr-runtime'; $config->logDirectory = __DIR__ . '/../storage/wr-logs'; $server = new WebRotor($config); $server->init($psr7Creator); // Framework initialization outside the loop. $framework = new Hleb\HlebAsyncBootstrap(__DIR__); $server->run(function(ServerRequestInterface $request, ResponseInterface $response) use ($framework) { $res = $framework->load($request)->getResponse(); $response->getBody()->write($res->getBody()); foreach($res->getHeaders() as $name => $header) { $response = $response->withHeader($name, $header); } return $response->withStatus($res->getStatus()); }); ``` This code uses the HTTP client libraries nyholm/psr7 and nyholm/psr7-server, which need to be installed additionally. To complete this configuration, you will also need to launch "workers" on your hosting. These are essentially the CRON-like processes provided by the hosting service. Typically, they are configured in the hosting admin panel, and while the design of the panel can vary, the principle remains the same. You need to launch two handlers at a two-minute interval (as shown in the settings above): ```*/2 * * * * /usr/local/bin/php7.2 /my-project/public_html/index.php --id=1``` ```*/2 * * * * /usr/local/bin/php7.2 /my-project/public_html/index.php --id=2``` These two processes differ only in the ID number for the workers. After this, all requests coming to the application will be handled by two asynchronous workers. For more details, refer to the library documentation: github.com/phphleb/webrotor For local development, you can avoid running workers, as requests will be processed in the usual manner if they are not running or inactive. This way, standard debugging tools, such as xDebug, will be available locally. ------------------------- # Built-in PHP Web Server After installing the HLEB2 framework, you can verify its functionality and settings using the built-in PHP web server. Here’s a link to the official documentation. For Linux, the permissions on resources created by the framework (cache) will be set by the terminal user, so if you have not configured permissions previously, the pages may become inaccessible to another web server afterward. Only a complete cache clearance of the framework and routes using console commands can help. You can check the framework by executing the following command (from the root directory of the installed project): ```$ php -S localhost:8000 -t public``` Port 8000 may already be in use for localhost, in which case replace it with another free port, such as 8001 or 8002. Since the public folder (unless you changed its name earlier) is the public directory of the project, after executing this command, the welcome page of the framework will be accessible at http://localhost:8000. The built-in PHP web server does not support full server functionalities and should not be used on public networks. ------------------------- # Apache The project's public folder includes a .htaccess file with the necessary settings for running the HLEB2 framework. Before using the framework with Apache, make sure to enable the mod_rewrite module so that the .htaccess file is handled by the server. Basic configuration of Apache through setup. By default, these settings are already specified in /public/.htaccess, but when using the .htaccess file, ensure that AllowOverride is set to All here. ServerName mysite.com # Path to the public folder DocumentRoot/var/www/mysite.com/public/ # If .htaccess is not used AddDefaultCharset UTF-8 Options +FollowSymLinks -MultiViews -Indexes RewriteEngine on # Redirect all requests to index.php RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] After starting the server, you can verify the installation by entering the previously assigned (locally or on a remote server) resource address in the browser's address bar. ------------------------- # RoadRunner RoadRunner is a high-performance application server PHP, load balancer, and process manager. RoadRunner is written in Go, is easy to install, and acts as a replacement for PHP-FPM. It supports xDebug and its alternatives, as well as profiling and monitoring tools like Datadog and New Relic. For more details, refer to the documentation of RoadRunner. To install the server resources for RoadRunner, use the official repository: github.com/roadrunner-server/roadrunner. For RoadRunner, you will need to modify the file /public/index.php so that the HLEB2 framework operates in a loop. Here’s a basic working example: ``` waitRequest()) { try { // Getting an object with a response. $response = $framework->load($request)->getResponse(); // Convert the framework response to a handler format. $psr7->respond(new Psr7\Response(...$response->getArgs())); } catch (\Throwable $e) { $psr7->respond(new Psr7\Response(500, [], 'Something Went Wrong!')); $framework->errorLog($e); } } ``` For RoadRunner, you also need to create a configuration file .rr.yaml in the root directory of the project (assuming the compiled server file named rr is located there). An example of a minimal working configuration in .rr.yaml: ``` version: '3' server: command: 'php ./public/index.php' http: address: :8088 middleware: - gzip - static static: dir: public forbid: - .php - .htaccess pool: num_workers: 6 max_jobs: 64 debug: false supervisor: max_worker_memory: 5 metrics: address: '127.0.0.1:2113' ``` In this configuration, RoadRunner limits the operation of a single process (worker) by the maximum allowable memory setting: http.pool.supervisor.max_worker_memory in megabytes. Therefore, if the process exceeds this limit, RoadRunner properly terminates it and proceeds to the next one. The RoadRunner server is started with the console command: ```$ ./rr serve``` According to the configuration, the application will be accessible at the address: http://localhost:8088 Server metrics in Prometheus format: http://localhost:2113 ------------------------- # Workerman Workerman is a highly efficient tool for building asynchronous servers in PHP. It is designed for working with WebSockets, HTTP servers, chat applications, APIs, and other network-based applications. Workerman works without the need for additional extensions or dependencies since it is fully implemented in pure PHP. This makes it cross-platform and simple to install. Notably, Workerman supports both HTTP and HTTPS, allows working with WebSockets, and easily scales to handle a large number of connections concurrently. This makes it suitable for creating realtime applications, such as chat systems, notification services, and streaming servers. You can install Workerman via Composer as a standard PHP library. More details can be found in the installation guide. Under Workerman, you need to modify the /public/index.php file so that the HLEB2 framework runs in a loop. Basic working example: ``` count = 4; $server->onMessage = function (TcpConnection $connection, $request) use ($framework) { $res = $framework->load($request)->getResponse(); $connection->send(new Response($res->getStatus(), $res->getHeaders(), $res->getBody())); }; Worker::runAll(); ``` The Workerman server is started with the following console command: ```$ php ./public/index.php start``` According to the specified settings, the application will be available at: http://127.0.0.1:2345 ------------------------- # Swoole Open Swoole (a fork of the original Swoole extension) is a high-performance platform for asynchronous execution of coroutines in PHP. Swoole is installed as an extension for PHP. Currently, Swoole is supported only for Linux and Mac. It's important to note that Swoole does not work with xDebug, the most popular debugging tool in the PHP ecosystem, and is also poorly compatible with some other profiling and monitoring tools. For Swoole, you will need to modify the /public/index.php file to ensure the HLEB2 framework runs in a loop. A basic working example: ``` set([ 'log_file' => '/dev/stdout' ]); // Framework initialization outside the loop. $framework = new Hleb\HlebAsyncBootstrap(__DIR__); $http->on('request', function ($request, Response $response) use ($framework) { // Getting an object with a response. $res = $framework->load($request)->getResponse(); foreach ($res->getHeaders() as $name => $header) { $response->header($name, $header); } $response->status($res->getStatus(), (string)$res->getReason()); $response->end($res->getBody()); }); $http->start(); ``` The Swoole server is started with the console command: ```$ php ./public/index.php``` According to the configuration, the application will be accessible at the address: http://localhost:9504 ------------------------- # Installation and Hosting Launch The installation requirements may vary on different hosting providers, but there are basic nuances that will be noted here. ## Disabling DEBUG Mode Debug mode should be disabled on any public server, and hosting servers are no exception. To separate settings from local development, copy the file /config/common.php as /config/common-local.php and disable the debug mode in the first, and enable it in the second. Now, if the file /config/common-local.php is not uploaded to the hosting server, the settings will differ. ## Strict Project Structure Often on hosting servers, the public folder is named public_html, but it could be different. To use this folder, simply rename the public folder in a project with the framework. Learn more about changing the public folder name. It's possible that the hosting recommendations suggest placing the project in public_html, but according to the framework structure, it should be placed one directory higher to ensure alignment of public folders when migrating data. ## Using Databases The hosting provider will likely provide a database and a method to connect to it. These settings may differ from local development settings. To resolve this, create a copy of the file /config/database.php, name it /config/database-local.php and set the hosting settings in the first, and local settings in the copy. Now, if the file /config/database-local.php is not transferred to the hosting server, the settings will be distinct. ## Task Scheduler The framework includes both built-in console commands and those defined by the developer. If the host offers a task scheduling mechanism, these console commands can be scheduled as tasks. You may need to specify the full path to the PHP executable when setting a task in the scheduler. For example: /usr/local/bin/php8.2 ~/project/dir/console rotate-logs 5 An alternative to running console commands manually is using a special Web Console of the framework. ------------------------- # FrankenPHP FrankenPHP is a modern application server for PHP, designed for high performance with support for asynchronous tasks, HTTP/2, HTTP/3, and WebSockets. The server can function as a standalone application or as an extension for various web servers, such as Caddy. This web server is written in Go and leverages CGO for deep integration with PHP, delivering minimal overhead and fast request handling. It supports standard PHP extensions, debugging tools (e.g., Xdebug), as well as integration with profilers and monitoring systems. FrankenPHP has limited support for Windows. The FrankenPHP server is distributed as binary files and Docker images. The latest releases can be found in the official GitHub repository. Installation instructions are available in the server documentation at frankenphp.dev/docs. FrankenPHP operates in several modes; this example demonstrates the simplest way to get started locally with the framework and to verify that it is compatible with this web server. For the HLEB2 framework, simply specify the path to the public directory when launching from the project root: ```$ frankenphp php-server -r public/ --listen 127.0.0.1:8080``` Here, an explicit address and port have been assigned for local development. Make sure this port is not in use. Your application will now be accessible at: http://127.0.0.1:8080 ------------------------- # Nginx Running the HLEB2 framework using Nginx (or its fork Angie) can be accomplished with either nginx + PHP-FPM or nginx + apache, as well as with NGINX Unit. This guide will only cover the nginx + PHP-FPM option as it is the most common. Basic configuration for Nginx + PHP-FPM: ``` server { listen 80; server_name mysite.com; # Path to the public folder root /var/www/mysite.com/public/; index index.php; location / { # Redirect all requests to index.php try_files $uri $uri/ /index.php?$query_string; } # Process PHP files with FPM location ~ \.php$ { try_files $uri =404; include /etc/nginx/fastcgi.conf; # Path to the socket with the required PHP version fastcgi_pass unix:/run/php/php8.2-fpm.sock; } # Hide specific files location ~ /\.(ht|svn|git) { deny all; } } ``` After starting the server, you can verify the installation by entering the previously assigned (locally or on a remote server) resource address in the browser's address bar. ------------------------- # Cached Templates Besides the functions built into the framework that allow embedding standard templates, there is the possibility of placing template content in the cache. Caching can both speed up some parts of the application and slow them down if those parts already operate quickly. Given that a template should only deliver data and not perform complex calculations, caching should be done at a higher level. However, for strictly specialized cases, especially when multiple templates are embedded within another, leading to increased resource consumption, it makes sense to cache the template for a short period. Template caching is not suitable for dynamically changing and internal site pages that require authorization, since during the cache lifetime, a user might log out, but this won't be reflected on the page. It is best used for static site pages, where changes are infrequent and in areas where security-critical conditions (such as authorization) are not present. ## Function insertCacheTemplate() This function is similar to insertTemplate(), but includes an additional argument sec, where you can specify the duration in seconds to set caching. After this period expires, the next request to the template will refresh it in the cache for the same number of seconds (one minute in the example). ``` $userId], 60); ``` In the example, a separate hash will be created for each different user ID upon request, and for the value NULL, another cache variant will be returned. When in doubt about the appropriateness of template caching, it's better not to do it. ------------------------- # Twig Templating Engine The Twig templating engine is quite well-known in its field and is used by default in the Symfony framework. It can be used as a replacement for the standard templates in the HLEB2 framework. ## Integrating TWIG Using Composer: ```$ composer require "twig/twig:^3.0"``` ## Using TWIG When assigning a template in the view() function, you need to specify the .twig extension for Twig templates. The parameters from this function will be passed as variables to the Twig template in a similar manner. The framework configuration has several settings applicable to the Twig templating engine, specifically in the /config/common.php file: twig.options - contains a list of settings similar to those in the Twig documentation for configuring the templating engine. twig.cache.inverted - excludes the specified directories from caching, otherwise (depending on whether caching is enabled) it includes them. The Twig templating engine is distributed under the BSD 3-Clause license, which imposes certain restrictions on its usage. ------------------------- # Standard Templates View is a component of the architectural pattern MVC (Action-Domain-Responder for the web). Templates store the structure of the response that will be sent to the browser. Often this is HTML code containing PHP variables defined outside the template. Templates can be nested within other templates. Importing one template into another is accomplished in the framework through special functions. The function view() for embedding a template from a route or controller is intended for templates with the extension .php or .twig. When using TWIG, you won't need the standard framework functions for embedding and caching templates since TWIG provides its own tools. ## Function insertTemplate() Code parts in included files from the /resources/views/ directory can be repetitive. To extract them into a separate template, independent of the surrounding content, use the function insertTemplate(), with the first argument specifying the template name from the /resources/views/ folder, and the second specifying an array of variables that will be available in the template by array keys. To differentiate templates from other files, it's recommended to place them in a separate /templates/ folder. Example of how another template /resources/views/templates/counter.php is inserted into the template /resources/views/content.php, using part of the data from the first. ``` $title"; insertTemplate('templates/counter', ['totalVisitors' => $total, 'uniqueVisitors' => $unique]); ``` ```
Total:
Unique:
``` ## Function template() The helper function template() is similar to insertTemplate(), but it returns the template content as a string representation, instead of outputting it at the place where it is defined. ------------------------- # Search the documentation ------------------------- # Project Installation The HLEB2 framework is designed such that its installation and requirements are minimally simple. To install the framework, all you need is PHP version 8.2 or higher with a basic set of extensions and 2 megabytes of free space on your device. If you want to use a PHP version below 8.2, try the first version of the framework. The framework's code is located in the GitHub repository at https://github.com/phphleb/hleb. The first step of installation involves copying this code to a server or a local folder where it will be used. ## Copying from Repository Visit the project's repository on GitHub (link above). Click on the Code button and then Download ZIP (direct link to the file). Extract the downloaded archive to the desired folder on the server or locally. Use only verified links to the official repository of the framework. ## Cloning Using Git To clone the framework repository into the new_project folder, execute the following git command: ```$ git clone https://github.com/phphleb/hleb new_project``` This command will create a new_project folder, initialize a .git subdirectory in it, then download all the data for this repository and extract a working copy of the latest version. If you navigate to the directory created by this command new_project, you will find the project files ready for use. ## Local Development with Docker To try the framework's capabilities and deploy local development from a Docker image, use the repository phphleb/toaster. ## Installation Using Composer An alternative option is using Composer. This method is more preferable, as Composer will allow you to install various packages and extensions in the future. Install the current version of the project using the console command (assuming Composer is installed globally): ```$ composer create-project phphleb/hleb new_project``` This command will install the framework into the new_project folder. ## Extension for Database Operations If your application will work with a database, you need to install the PHP PDO extension and the corresponding driver (for example, pdo_mysql for MySQL). ## Project Public Directory For further actions, you need to configure the framework's public folder if the initial name public does not fit for some reason. For instance, on some hosting services, a folder named public_html is used. To change the project's public folder, simply rename the public folder. Additionally, in this case, you need to change the predefined name in the console file, which is located in the root folder of the project. ------------------------- # Resetting State for Asynchronous Requests The HLEB2 framework provides the capability to perform asynchronous requests, which imposes additional requirements on the code. One of the main requirements is to eliminate stored state upon the request's completion. Under the term "asynchrony" this document groups together true asynchronous mode and the conventional long-running mode, since the recommendations for both are identical. Stored state can include current user data, request data cache, various forms of memoization, etc. In programming, memoization is an optimization method that makes already computed data reusable. The approach involves caching the output of a class method and forcing the method to check whether the required computation is already in the cache before computing it. It is necessary to determine which stored states relate to the request data and which pertain to the operation of the application as a whole. For example, a computed state for general tariff information won't change from request to request, but the selected tariff for each user needs to be reset. During asynchronous requests, the next request might belong to a different user, making it important to clear information about the previous one. ## ResetInterface Using ResetInterface provides a modern way to reset the state of services in the framework container asynchronously. This applies only to services stored as singletons and allows you — by adding this interface and its reset method — to clear a service's state and perform other preparations intended for the next request. For example, in this demonstration logging service, the state of the Monolog logger will be reset according to its own internal implementation of the reset method: ``` class ExampleLogerService implements \Hleb\Base\ResetInterface { public function __construct(private Monolog $logger) { } #[\Override] public function reset(): void { $this->logger->reset(); } } ``` To enable state reset, the ResetInterface interface is added and the reset method is implemented. ## RollbackInterface Modern programming practices discourage the use of a state stored as a static class property, but it is often convenient, and concerns about it arise only when transitioning to asynchronous mode. To facilitate this transition, the HLEB2 framework provides a special interface RollbackInterface with a single static method rollback. For example, consider a stored state with current user data (simplified code): ``` class Example { private static ?User $currentUser = null; public function set(User $user): void { self::$currentUser = $user; } } ``` To reset the state, the interface RollbackInterface is added, and the method rollback is implemented: ``` class Example implements \Hleb\Base\RollbackInterface { private static ?User $currentUser = null; public function set(User $user): void { self::$currentUser = $user; } #[\Override] public static function rollback(): void { self::$currentUser = null; } } ``` Now, upon the completion of an asynchronous request, the framework will check if the class has the RollbackInterface and execute the reset method rollback. It is important to ensure that the state-resetting method is idempotent and does nothing more. That is, upon repeated execution, the application of the result will not be different. The need for idempotency is evident from the following, more complex example, where the interface is applied in inheritance (the reset method could be invoked twice): ``` class Example implements \Hleb\Base\RollbackInterface { private static ?User $currentUser = null; #[\Override] public static function rollback(): void { self::$currentUser = null; } } class ChildExample extends Example { /** @param User[] */ private static array $currentUserFriends = []; #[\Override] public static function rollback(): void { parent::rollback(); self::$currentUserFriends = []; } } ``` If you need to execute any action after completing an asynchronous request that is not related to resetting the state in a specific class, you can add it to the rollback method of the App\Bootstrap\ContainerFactory class. ------------------------- # Configurable Command Options Initially, the options for executing console commands are set in the 'run' method of the command class. They correspond to the method's argument order. In the HLEB2 framework, you can also specify one or several named parameters for a command. The order of named parameters does not matter when invoking the command. ## rules() Method The rules() method of the command class returns an array with rules for extended parameters. If such a method does not exist, add it as the first method of the command class. ``` #[\Override] protected function rules(): array { return [ Arg(name: 'Name')->short(name: 'N')->default('Undefined')->required(), Arg(name: 'force'), Arg(name: 'UserData')->list()->default([]), ]; } ``` The example shows three different named parameters of different types. The parameter name is mandatory and must not be duplicated. The first parameter supports two values -N and --Name, its presence is required. By default, --Name is equal to the string 'Undefined', and the incoming value can only be a string (not an array). The value can be in the form --Name=Fedor or -N=Mark, while --Name will be equal to 'Undefined'. The second parameter is of the form --force (without a value); if present, it equals true. The third parameter is in the form of an array, and the value can be specified multiple times, such as --UserData=1 and --UserData=2, which is equivalent to --UserData=[1,2]. Its presence is optional, and if there is no value or it is called like --UserData, it will be equal to [] (an empty array). ## Retrieving Parameter Values The parameter data can be obtained as $this->getOptions() or $this->getOption() in the run() method of the command. The first method returns a named array of system objects, from each of which you can get the value in the required format. The other returns a similar system object of one parameter by name (mandatory main, not short). ``` $name = $this->getOption('Name')->asString(); $force = $this->getOption('force')->asBool(); $options = $this->getOption('UserData')->asArray(); ``` ------------------------- # MVC Template Generation In the HLEB2 framework, when creating Models, Controllers, and entire modules, you can use special console commands. Additionally, the initial file templates are customizable according to the developer's own preferences. ## Controller Generation Console command to generate a Controller class: ```$ php console --add controller Demo/ExampleController``` The command will create the file /app/Controllers/Demo/ExampleController.php with the new Controller class. To change the template for creating a class, copy the file 'controller_class_template.php' from '/vendor/phphleb/framework/Optional/Templates/' to the folder '/app/Optional/Templates/' and make the necessary modifications. ## Middleware Generation Console command to generate new middleware: ```$ php console --add middleware Demo/ExampleMiddleware``` After execution, the file /app/Middlewares/Demo/ExampleMiddleware.php with the middleware class will be created. To modify the original middleware template, copy the file 'middleware_class_template.php' from '/vendor/phphleb/framework/Optional/Templates/' to the folder '/app/Optional/Templates/', and then make changes. ## Model Generation Example of creating a Model class from the console: ```$ php console --add model Demo/ExampleModel``` This command will create the file /app/Models/Demo/ExampleModel.php with the Model class. To change the original template for the Model, copy the file 'model_class_template.php' from '/vendor/phphleb/framework/Optional/Templates/' to the folder '/app/Optional/Templates/' and edit it as needed. ## Generating a Command Class Console command to create a new task, specifying the task name: ```$ php console --add task demo/example-task``` Upon execution, the file app/Commands/Demo/ExampleTask.php will be created. To make changes to the base class, copy the file 'task_class_template.php' from '/vendor/phphleb/framework/Optional/Templates/' to the folder '/app/Optional/Templates/' and adjust it as needed. ## Generating a Module To generate the base files for a Module in the 'modules' directory (the name can be changed in the settings), execute the following command: ```$ php console --create module main``` Where 'main' is the name of the new module. For a nested module in the 'modules/demo' folder, modify the command as follows: ```$ php console --create module demo/main``` If you need to create your own module template files, copy the contents of the directory '/vendor/phphleb/framework/Optional/Modules/example/' to the folder '/app/Optional/Modules/example/' and make the necessary changes to the files. When modifying the base files, keep in mind that special tags are included, and they are necessary for the correct substitution of console parameters. ------------------------- # Custom Command Names In addition to generating console command names from the class name and command folder, there is a direct assignment of a name and also the addition of a short name. To specify a command name, use one of the following constants in the command class. These constants must have public visibility (public). All console command names in the project, including short ones, must be unique. ## TASK_NAME Constant The feature of the class TASK_NAME constant is replacing the automatically determined command name with the one specified in the constant. ## TASK_SHORT_NAME Constant The class TASK_SHORT_NAME constant allows you to add a short additional name to the automatically generated command name or to the one directly set in TASK_NAME. ------------------------- # Console Bowling Game The HLEB2 framework includes a small console bowling game. At the moment, it's a single-player game with score counting, levels, and strikes according to the real bowling game rules. The game is launched with a command like: ```$ php console flat-kegling-feature 8 1 50``` The numerical parameters of the command correspond to the ball throwing force (1-10), target pin number (1-10), and accuracy coefficient of hitting within the target pin (1-49 to the left and 51-100 to the right side). ------------------------- # Non-Standard Use of the Container ## Initializing a service in a service Although creating an object in the container using new with an empty constructor is a good practice, eventually, you can outsource the creation of all necessary dependencies to a separate method in a special class and register its execution in the container. However, there are ways to resolve dependencies without resorting to creating a separate wrapper class. If it becomes necessary to reuse a service from the container to initialize another service in the container, we turn to the capabilities provided by dependency injection. In the class App\Bootstrap\ContainerFactory, these methods are available, as they are in a special class for creating the container. For example, it is necessary to initialize the constructor of a service in the container. To do this, in the body of the match operator of the App\Bootstrap\ContainerFactory class, you need to add approximately the following match: ``` // File /app/Bootstrap/ContainerFactory.php use Hleb\Static\DI; // ... // ExampleService::class => new ExampleService(), // variant 1 DemoService::class => new DemoService(DI::object(ExampleService::class)), // variant 2 DemoService::class => DI::object(DemoService::class), // ... // ``` Now in the constructor of the DemoService class, the current ExampleService will be injected as defined in the container. All dependencies not explicitly specified in the used example will be resolved automatically (variant 2). It is important to ensure that dependencies do not form a cyclic dependency, which can occur if the object in the container makes another request to the container for the initialization of itself. A more complex example: ``` // File /app/Bootstrap/ContainerFactory.php use Hleb\Static\DI; // ... // SenderServiceInterface::class => new MailTransportService(), SiestaService::class => DI::method(DI::object( SiestaService::class, [ 'start' => (new DateTimeImmutable())->setTime(14, 0), 'end' => (new DateTimeImmutable())->setTime(16, 0), ] ), 'setSender', ['transport' => DI::object(SenderServiceInterface::class)]), // ... // ``` In this way, in the framework's container, despite its seeming simplicity, you can add various interdependent services. ## Adding Services in User Code By default, the framework does not allow adding services after the container has been initialized. However, by overriding the getSingleton() method to be public in the ContainerFactory class, you gain the ability to add objects to the container in your user code through this static method. Here’s an example of modifying the class: ``` // File /app/Bootstrap/ContainerFactory.php use Hleb\Constructor\Containers\BaseContainerFactory; final class ContainerFactory extends BaseContainerFactory { public static function getSingleton(string $id): mixed { // ... // if (self::$singletons[$id] instanceof \Closure) { self::$singletons[$id] = self::$singletons[$id](); } return self::$singletons[$id]; } #[\Override] public static function setSingleton(string $id, object|null $value): void { parent::setSingleton($id, $value); } } ``` From the example, it is clear that support for lazy initialization through the callable type and its handler has also been added. ------------------------- # Overriding the Default Service Fetching a default service from the container can be modified by adding your own service with a similar interface to the user container. You need to create a new service and return it from the 'getSingleton' method of the App\Bootstrap\ContainerFactory class before selecting from the default services. In the HLEB2 framework, each built-in service uses two identical interfaces (for different naming options), and you must return your own service as a singleton for the interface ending with 'Interface'. For example, for the caching service, it would be 'Hleb\Reference\CacheInterface'. ``` new OtherCacheService(), // ... // default => null }; return self::$singletons[$id]; } public static function rollback(): void { // ... // } } ``` The example shows how to replace the default caching service with your own. Here, it could be caching with database storage instead of file-based (default). Similarly, you can "remove" a default service from the container by overriding it with a NULL value. But first, you must ensure that the service is not used in either the framework's own code or the application's code. ------------------------- # Adding a Service to the Container In the section describing the Container for the HLEB2 framework, this documentation already provides a simple example of adding a demo service. Next, we'll look at an example of adding a real library for mutexes as a Service. The library github.com/phphleb/conductor contains a mutex mechanism. If you plan to use this library, you need to install it first. It is perfectly possible to assign a key in the container as a class from the library, but this may cause issues later as the application's code will be tied to a specific class or library interface, making it impossible to change it. It is better to connect external libraries to the project using the Adapter pattern, the class of which will be the key of the service in the container. ``` mutex->acquire($name, $sec); } public function release(string $name): bool { return $this->mutex->release($name); } public function unlock(string $name): bool { return $this->mutex->unlock($name); } } ``` This wrapper class for the service is created in the /app/Bootstrap/Services/ folder. Although this is a convenient directory for examples, structurally the Services folder should be located next to the project logic. Now let's add the library to the container by the created class: ``` new MutexService(new FileMutex()), // ... // default => null }; return self::$singletons[$id]; } public static function rollback(): void { // Rollback for an asynchronous request. MutexDirector::rollback(); // ... // } } ``` As seen in the example, the rollback() method has been added to reset the state for the connected mutex library that supports asynchrony. After adding, the new service is available from the container as a singleton through this class. ``` use App\Bootstrap\Services\MutexService; use Hleb\Static\Container; $mutex = Container::get(MutexService::class); ``` The method of using the added service in controllers, commands, and events (in all classes inherited from Hleb\Base\Container): ``` use App\Bootstrap\Services\MutexService; $mutex = $this->container->get(MutexService::class); ``` You can simplify the example call to the service by adding a new method with the same name mutex() to the App\Bootstrap\BaseContainer class and its interface: ``` use App\Bootstrap\Services\MutexService; #[\Override] final public function mutex(): MutexService { return $this->get(MutexService::class); } ``` Now the call will look like this: ``` $mutex = $this->container->mutex(); ``` ------------------------- # Web Console In the HLEB2 framework, a special Web Console provides access through the user's browser for executing console commands. Only framework commands are supported, meaning those starting with 'php console'. By default, the Web Console is disabled for security reasons. To specify the application page on which to display the Web Console, create a route for this with an address. ``` // File /routes/map.php Route::match(['get', 'post'], '/web-console', view('console')); ``` You also need to create a template that outputs an HTML form for the Web Console: ``` run(); ``` Now the Web Console is available at the relative address '/web-console' of the site. Additionally, you need to copy the key from the file '/storage/keys/web-console.key' and use it to access the command execution form. Commands that require user input will not work through the Web Console. ------------------------- # HLOGIN - Registration Module Creating user registration on a website often becomes necessary after the framework installation. Before beginning page development, you need to designate their visibility for different categories of users. The HLOGIN library extends the capabilities of the HLEB2 framework by adding comprehensive user registration to the site, distinguished by easy configuration and quick setup, along with convenient and diverse functionality. It supports multilingualism and several design options. Optionally, you may include a feedback form, which accompanies registration and authentication. The automatically generated admin panel contains tools for user management and display settings. After integrating registration, you can immediately focus on creating content for the site. Several basic design types are available. You can view a demonstration of the function and appearance of registration pop-up windows by clicking here. ## Installation Step 1: Install via Composer in a HLEB2-based project: ```$ composer require phphleb/hlogin``` Step 2: Install the library in the project. You will be prompted to select a design type from several options: ```$ php console phphleb/hlogin add``` ```$ composer dump-autoload``` ## Connection Step 3: You must have an active database connection before performing this action. In the project settings '/config/database.php', you need to add a connection or ensure it exists, and also verify its name is in the 'base.db.type' parameter. ```$ php console hlogin/create-login-table``` After this, use the console command to create a user with administrator rights (you will be prompted to provide E-mail and password): ```$ php console hlogin/create-admin``` If you cannot execute the console command, create the tables using the appropriate SQL query from the file /vendor/phphleb/hlogin/planB.sql. Then register an administrator and set their 'regtype' to 11. Step 4: Now you can proceed to the main placeholder page of the website if it is the default framework page without changes and check that the authorization panels are available. If the library is installed in a HLEB2-based project not from the start and the placeholder has been removed, check the login on the page '/en/login/action/enter/' of the site (using the administrator data from the previous step). Step 5: Installation of registration on specific pages through routing. To do this, set the following conditions in the routing files (project folder /routes/): ``` use App\Middlewares\Hlogin\Registrar; use Phphleb\Hlogin\App\RegType; Route::toGroup()->middleware(Registrar::class, data: [RegType::UNDEFINED_USER, '>=']); // Routes in this group will be available to all unregistered and registered users // except those that were marked deleted and banned. Route::endGroup(); Route::toGroup()->middleware(Registrar::class, data: [RegType::PRIMARY_USER, '>=']); // Routes in this group will be available to those who pre-registered (but didn't confirm E-mail), // as well as to all registered users (including administrators). Route::endGroup(); Route::toGroup()->middleware(Registrar::class, data: [RegType::REGISTERED_USER, '>=']); // Routes in this group will be available to all users who have completed full registration // (confirmed by E-mail including administrators). Route::endGroup(); Route::toGroup()->middleware(Registrar::class, data: [RegType::REGISTERED_COMMANDANT, '>=']); // Routes in this group will be available only to administrators. Route::endGroup(); Route::toGroup()->middleware(Registrar::class, data: [RegType::PRIMARY_USER, '>=', Registrar::NO_PANEL]); // Routes with check registration without displaying standard panels and buttons. Route::endGroup(); Route::toGroup()->middleware(Registrar::class, data: [RegType::PRIMARY_USER, '>=', Registrar::NO_BUTTON]); // Routes with check registration without displaying standard buttons. Route::endGroup(); ``` It is sufficient to distribute the site's routes according to these conditions (groups) so that user authorization rules are applied to them. Note that pages not included in any of these groups with conditions are outside the registration rules, and this library is not connected to them. Step 6: Configuration. After authorization, the administrator profile (/en/login/profile/) displays a button to access the admin panel. In it, you can configure registration panels and other parameters. If you need to display data depending on the user registration type: ``` use Phphleb\Hlogin\App\AuthUser; $user = AuthUser::current(); if ($user) { // Status for the confirmed user. $confirm = $user->isConfirm(); // Obtaining the user's E-mail. $email = $user->getEmail(); // Result of the administrator check. $isAdmin = $user->isSuperAdmin(); // ... // } else { // The current user is not authorized. } ``` You can also add the class Phphleb\Hlogin\Container\Auth to the container and retrieve this data from it. By default, the language used for panels is extracted from the url parameter (following the domain) or the lang attribute within the '' tag. To forcefully set the design and/or language of the panels on a page: ``` // Setting the design for the page via JS. // For example, this way you can set the `special` type for visually // impaired users without refreshing the page in the browser. hloginSetDesignToPopups('special'); // Returns the design type to its original state. hloginRevertDesignToPopups(); // Close all registration popups. hloginCloseAllPopups(); // Open a specific window, in this case user registration. hloginVariableOpenPopup('UserRegister'); // Or 'UserEnter', 'UserProfile', 'ContactMessage' // Displays an arbitrary custom message in the window (current design). hloginOpenMessage('Title', 'Message text'); // If this function exists, it will be called every time a popup is opened, passing the popup type. function hloginPopupVariableFunction(popupType) { // Custom code. (popupType = 'UserRegister' / 'UserEnter' / 'UserPassword' / 'ContactMessage') } ``` Or, using attributes: ```
``` As can be understood, registration cannot be available for users with JavaScript disabled in the browser. There are hardly any left now. ## Specific Pages If there is a need to direct a user immediately to a login or registration page, several necessary pages are automatically created: Registration Page /ru/login/action/registration/ Login Page /ru/login/action/enter/ Profile Page /ru/login/profile/ Contact Page /ru/login/action/contact/ Admin Panel Settings Page /ru/adminzone/registration/settings/ ## Additional Data Processing When validating values on the backend side submitted from registration forms, you can additionally process them with your own PHP script, if available. This way, for example, you can add a custom field to the form and check it yourself. The queries are divided into separate classes, which can be found in the folder /vendor/phphleb/hlogin/Optional/Inserted/. They can only be used after copying into the folder /app/Bootstrap/Auth/Handlers/. ## Design Custom design is available by choosing the "blank" type in the admin panel. After this, you can copy and modify the CSS file from any existing design, connecting it to the site yourself. You can also make edits based on the design type. ``` .hlogin-over-panel[data-design='base'] input { /* CSS rules for the "input" block of the "base" design */ } .hlogin-over-panel[data-design='dark'] input { /* CSS rules for the "input" block of the "dark" design */ } ``` ## Localization By default, several switchable languages are used for registration and authorization. However, all labels can be customized to your own settings. It is important to check that lengthy words fit within the panel interface. For backend localization, copy the necessary language files from /vendor/phphleb/hlogin/App/BackendTranslation/ to the folder /app/Bootstrap/Auth/Resources/php/ and make changes in the latter. For frontend localization, copy the necessary language files (starting with 'hloginlang') from /vendor/phphleb/library/hlogin/web/js/ to the folder /app/Bootstrap/Auth/Resources/js/ and make changes. You can add an additional language(s) by creating appropriately named files for backend and frontend localizations and adding it to the list of allowed languages in the 'allowed.languages' setting in the /config/main.php file (this file may be duplicated in Modules). ## Adminzone When creating your additional pages in the admin panel, surround their routes with access restrictions as shown below: ``` use App\Middlewares\Hlogin\Registrar; use Phphleb\Hlogin\App\RegType; Route::toGroup()->middleware(Registrar::class, data: [RegType::REGISTERED_COMMANDANT, '=']); // Routes in this group will only be available to administrators. Route::endGroup(); ``` The creation of pages in the admin section is described in the relevant section of this documentation. ## Sending Emails Sending emails with notifications and access recovery is done using the github.com/phphleb/muller library. In the admin panel, the sender's E-mail should be specified, for which sending from the server must be allowed. For most hostings, it is enough to create such a mailbox. The available sending E-mail is located in php.ini (sendmail_path = ... -f'email@example.com'). By default, emails are additionally logged into the folder '/storage/logs/' with the name ending in 'mail.log'. This logging can be disabled in the settings of the admin panel. ## Mail Server The default library used for sending emails has limited capabilities and should be replaced with a suitable mail server or another equivalent as the project evolves. Create the class App\Bootstrap\Auth\MailServer at the path /app/Bootstrap/Auth/MailServer.php, which implements the interface Phphleb\Hlogin\App\Mail\MailInterface. Once the file is created, emails will be sent using this class, so you should first implement your own sending logic for the chosen mail server. ## Library Update To update, execute the console commands: ```$ composer update phphleb/hlogin``` ```$ php console phphleb/hlogin add``` ```$ composer dump-autoload``` During the installation process, choose the current design that is used by default. ## Links HLOGIN library on GitHub: github.com/phphleb/hlogin Demo registration page: auth2.phphleb.ru ------------------------- # Set of traits for creating API To implement API in the HLEB2 framework, a set of traits is provided to simplify validation and data processing in controllers (where these traits are applied). The use of traits in PHP is a matter of various opinions, which is why this module is provided as a separate library, which you may choose to use; there is quite a number of validators available for development in PHP, and this is just a simple working alternative. Installation of the library github.com/phphleb/api-multitool using Composer: ```$ composer require phphleb/api-multitool``` or download and unpack the library archive into the folder /vendor/phphleb/api-multitool. ## Connecting the BaseApiTrait (set of traits) First, you need to create a parent class BaseApiActions (or another name) for controllers with API: ``` setApiBoxDebug($this->container->settings()->isDebug()); } } ``` All auxiliary traits are collected in BaseApiTrait as a set. Therefore, it is enough to connect it to the controller and get the full implementation. If a different set of these traits is required, then either use them as a group or combine them into your own set. After this, in all controllers inherited from this class, methods from each trait in the set will appear: ## ApiHandlerTrait The trait ApiHandlerTrait contains several methods that may be useful for processing returned API data. This does not mean that its methods 'present' and 'error' form the final response, they return named arrays, which can be used in your own more complex standard. An example in the controller method: ``` container->request()->get('id')->asInt(); if (!$id) { return $this->error('Invalid request data: id', 400); } $data = UserModel::getOne($id); return array_merge( $this->present($data ?: []), ['error_cells' => $this->getErrorCells()] ); } } ``` In the HLEB framework, when returning an array from a controller, it is automatically converted into JSON. When displaying the formatted array, a value 'error_cells' is added with a list of fields where validation errors occurred (if any). ## ApiMethodWrapperTrait Intercepts system errors and provides output to the 'error' method of the previous trait ApiHandlersTrait or another designated for this purpose (if the mentioned trait is not used). If a controller method is called, for proper error handling, you need to add the prefix 'action' in the controller, and in the route, leave it without the prefix. For example, for the previous controller example, the routing would be approximately like this: ``` Route::get('/api/users/{id}')->controller(UserController::class, 'getOne'); ``` Here it should be noted that originally the call goes to the controller method 'getOne', and in the controller itself, the method is 'actionGetOne'. ## ApiPageManagerTrait Implements the often necessary function of pagination for displayed data. Adds a method 'getPageInterval', which transforms pagination data into a more convenient format. This calculates the initial value of the selection, which is convenient for working with the database. ## ApiRequestDataManagerTrait Adds a method 'check' that allows checking data in one array against conditions from another. Using this trait provides the ability to verify any incoming data that has been transformed into an array, whether they are POST request parameters or JSON Body. There is a list of possible conditions by which you can verify the data, composed by the developer. For example (Request::input() for the HLEB2 framework returns a JSON Body array): ``` use Hleb\Static\Request; $data = Request::input(); // $result - a boolean value indicating whether the checks were successful or not. $result = $this->check($data, [ // Required field, type integer, minimum value 1. 'id' => 'required|type:int|min:1', // Required field, type string, check via regular expression for correspondence to email. 'email' => 'required|type:string|fullregex:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', // Optional field, but will check for type string or NULL if found. 'name' => 'type:string,null', // Required field, type string, at least 8 characters, must contain at least one digit and one uppercase letter. 'password' => 'required|type:string|minlength:8|fullregex:/^(?=.*[0-9])(?=.*[A-Z]).+$/' ]); $errorCells = $this->getErrorCells(); // An array with a list of fields that did not pass the check. $errorMessage = $this->getErrorMessage(); // An array with messages about validation errors that occurred. ``` required - a required field, always located at the beginning. List of possible types ('type' - must be in the first position or directly after required): string - checks for the presence of a string value, constraints can be minlength and maxlength. float - checks for the float(double) type, constraints can be max and min. int - checks for the int(integer) type, constraints can be max and min. regex - checks against a regular expression, for example 'regex:[0-9]+'. fullregex - checks against a full regular expression, similar to 'fullregex:/^[0-9]+$/i', must be enclosed with slashes and can contain the characters : and |, unlike the simpler regex. bool - checks for a boolean value, only true or false. null - checks for null as a valid value. void - checks for an empty string as a valid value. Type for enumerations: enum - searches among possible values, for example 'enum:1,2,3,4,south,east,north,west'. The check for equality is not strict, so both 4 and '4' are correct; for exact matching, it is better to accompany it with a type check. You can add two or more types, and they will be checked against all common conditions inclusively, for example, 'type:string,null,void|minlen:5' - this means that the string should be checked, at least 5 characters long, or empty, or null value. In all other cases, it returns false as a result of a failed validation check. You can also check an array of fields with a list of standard array fields (they will be checked according to a unified template): ``` $result = $this->check($data, [ // Optional field, array with enumeration (two fields are checked in each). 'users' => ['id' => 'required|type:int', 'name' => 'required|type:string'], // Required field, array with enumeration (three fields are checked in each). 'images' => ['required', ['id' => 'required|type:int', 'width' => 'required|type:int', 'height' => 'required|type:int']] ]); ``` To check values of nested arrays in the check array, the name is specified in square brackets. ``` $result = $this->check( [ ['name1' => ['name2' => ['name3' => 'text']]],// Data. ['[name1][name2][name3]' => 'required|type:string'] // Array with conditions. ]); ``` The above condition will return a successful check considering the nesting. ## Testing The API traits were tested using github.com/phphleb/api-tests ------------------------- # Administrative Panel The 'Administrative Panel' module in the HLEB2 framework is an extension to the HLOGIN registration library, but it can also be used independently as one or more administrative panels on a single site or as a public frontend for a website. This library was used to create the look of this framework documentation site without significant modifications. ## Installation Using Composer: ```$ composer require phphleb/adminpan``` ## Configuration By running the following command, the adminpan.php file, describing how to build a menu structure for the administrative panel, will be copied to the /config/structure/ directory. ```$ php console phphleb/adminpan add``` Initially, the /config/structure/adminpan.php file contains an empty array, with no menu sections defined. Menu sections are assigned by specifying special route names (or standard links). Example for a demo route: ``` Route::get('/{lang}/panel/page/default') ->page('adminpan', ExamplePanelController::class) ->name('adminpan.default'); ``` Here, it specifies that for the menu 'adminpan' (named the same as the adminpan.php file), the URL '/{lang}/panel/page/default' is assigned the page() controller of the ExamplePanelController class, targeting the 'index' method. Additionally, the route has a name 'adminpan.default', which is needed for mapping to a section in the menu. Now the first menu item can be created in the /config/structure/adminpan.php file. ``` 'base', // base|light default `base` 'breadcrumbs' => 'on', // on|off default 'on' 'section' => [ [ 'name' => [ 'ru' => 'Главное меню', 'en' => 'Main menu' ], 'section' => [ [ 'route' => 'adminpan.default', 'name' => [ 'en' => 'Test page', 'ru' => 'Тестовая страница', ], ], ], ], ] ]; ``` The menu can contain nested dropdown lists ('section'), currently there's only one assigned with a single item. If you navigate to the URL '/ru/panel/page/default', the design will be set to 'base' (from the settings) for the page. Also, the menu will have the 'Main Menu' with the active item 'Test Page' where content from the ExamplePanelController will be displayed. When used in conjunction with the HLOGIN library, the admin panel routes may be accessible only to a specific type of user (authenticated). For a deeper understanding of the admin panel operation, you can deploy this site locally and explore its menu structure. Library repository: github.com/phphleb/adminpan ------------------------- # Testing The framework structure is designed to avoid any obstacles to code testing built on it. This applies to all types of controllers, standard services, and custom framework functions. The testing approach depends on the usage type of the services, which may be a corresponding class with static methods such as Hleb\Static\Service::method() for built-in framework services, or DI, referring to service (and other object) injection into class methods and constructors. Dependency Injection within the framework is limited to objects created by it, including controllers, middleware, commands, events, and objects created by the service known as DI. ## Testing for Dependency Injection A simple example of a demonstration controller with DI: ``` info('Request to demo controller'); return 'OK'; } } ``` Suppose you need to ensure that the controller returns the text 'OK' without sending a message to the logs. ``` use App\Controllers\ExampleController; use Hleb\Main\Logger\NullLogger; $controller = new ExampleController(); $logger = new NullLogger(); $result = $controller->index($logger); if ($result === 'OK') { // Successful test. } ``` Here, the logging class is replaced by a class with the same interface, but its methods do not send anything to the log. It is assumed that one of the special testing libraries (such as github.com/phhleb/test-o) is used, with checks implemented through it. Now, let’s invoke the method of an arbitrary class through the DI service (specifically the framework service, not the architectural pattern itself): ``` use Hleb\Reference\Interface\Log; class Example { public function run(Log $logger): string { $logger->info('Demo class method executed'); return 'OK'; } } use Hleb\Static\DI; $result = DI::method(new Example(), 'run'); ``` In this case, the logging service will be injected from the container, and the message will be logged. Let’s modify the method invocation for testing: ``` use Hleb\Main\Logger\NullLogger; use Hleb\Static\DI; $result = DI::method(new Example(), 'run', ['logger' => new NullLogger()]); if ($result === 'OK') { // Successful test. } ``` Now the class has been tested without logging occurring. You can substitute any DI object with a custom class designed for the required behavior, making it convenient for testing. ## Testing Standard Services The built-in services of the HLEB2 framework can be accessed with static methods such as Hleb\Static\Service::method(). This approach simplifies access to services but can complicate testing of the modules containing them, although it is still feasible. Here's an example with logging: ``` use Hleb\Static\Log; class Example { public function run(): string { Log::info('Demo class method executed'); return 'OK'; } } use Hleb\Main\Logger\NullLogger; use Hleb\Init\ShootOneselfInTheFoot\LogForTest; $logger = new NullLogger(); LogForTest::set($logger); $result = (new Example())->run(); LogForTest::cancel(); if ($result === 'OK') { // Successful test. } ``` The example shows how the service state was replaced with a test object and then reverted to its initial value. To prevent this approach from being used outside of tests, in a production project, the configuration parameter 'container.mock.allowed' in the /config/common.php file is set to false. ## Functional Testing To run tests that initialize the core of the framework, you may need to replace some or all services in the container with test objects. To do this, simply implement your own service and assign it based on a condition (in the example, this is the global constant APP_TEST_ON): ``` testContainer === null) { $this->testContainer = new TestContainer(); } return $this->testContainer->get($id); } return ContainerFactory::getSingleton($id) ?? match ($id) { // ... // default => parent::get($id), }; } } ``` ## Testing Built-in Functions Several built-in framework functions that simplify service calls, such as the logger() function, are implemented through tested service calls, in this case, as a wrapper around Hleb\Static\Log. ## Testing for $this-container in Classes In controllers, middlewares, commands, events, and other classes inherited from Hleb\Base\Container, the container can be accessed as $this-container. If you choose this method of using the container (mixing various methods within a project would look odd), special initialization of the object constructor is required for testing. ``` use Hleb\Base\Container; use Hleb\Reference\LogInterface; class Example extends Container { public function run(): string { $this->container->get(LogInterface::class)->info('Demo class method executed'); return 'OK'; } } // TestContainer has an interface App\Bootstrap\ContainerInterface. $config = ['container' => new TestContainer()]; $result = (new Example($config))->run(); if ($result === 'OK') { // Successful test. } ``` ------------------------- # Configuration Setup The settings for the HLEB2 framework are stored in configuration files within the /config/ folder. At the beginning of some of these files, you might find a line similar to this: ``` if (file_exists(__DIR__ . '/common-local.php')) { return (require __DIR__ . '/common-local.php');} ``` This code indicates that if the file common-local.php exists in this folder, its settings will be used instead of the current ones (from the common.php file). Therefore, you can create copies of these files with the addition of '-local' to their names and use them for local development without adding them to version control (i.e., without pushing them to the target server). In these copied files, make sure to remove this line of code, as it is no longer necessary. Separate configurations for local development and the final server provide convenience for setup. The framework allows retrieval of any configuration value by its name, so these settings can also be used for initializing third-party libraries. ## Debug Mode In DEBUG mode, the framework operates slightly differently than usual, displaying debugging information and errors that should not be accessible on a public resource. The framework's debug mode should only be used for internal development. To disable/enable debug mode, change the debug value in the /config/common.php file as needed. Similarly, other configuration settings can be modified. ## Host Restriction To prevent header Host spoofing, specify supported host addresses in the allowed.hosts setting within the /config/common.php file, such as "example.com" and "www.example.com", used in your project. You can also set restrictions using regular expressions. In DEBUG mode, the check for hosts matching this list will not be performed. A good practice is to use relative links within the project without specifying the host (domain) of the site. ## Caching In debug mode, it is also helpful to disable caching performed by the framework. The setting app.cache.on in the /config/common.php file controls this. ## Automatic Route Cache Update The framework has built-in automatic route cache updates by default when developers make changes to them. This feature is convenient for local development, but as request volume increases, you might disable auto-updating on a production server and use a special console command whenever changes are made. The auto-update mode is adjusted by the routes.auto-update parameter in the /config/common.php file. ## Logging Errors By default, information about errors is saved in the files located in the /storage/logs/ folder. If DEBUG mode is enabled, errors may also be displayed to the user (in the browser or via API). The error level can be configured in the error.reporting setting of the /config/common.php file. Initially, all levels of PHP errors are reported (recommended setting). ## Timezone The timezone setting in the /config/common.php file specifies the timezone for date/time functions. Default: 'Europe/Moscow'. ## Database Settings The /config/database.php file contains settings for the databases in use. Initially, it provides several different examples. Within the configuration file, the list of configurations is a nested array with the key 'db.settings.list', from which the default settings block is selected, indicated by the 'base.db.type' option. ------------------------- # Routing Routing is the primary task of the framework in handling incoming requests. Here, the route responsible for the request address is defined, and subsequent actions are assigned. Sometimes routing in frameworks is also referred to as "routing," which is the same thing. Project routes are defined by the developer in the /routes/map.php file. Other route files from the "routes" folder can be included in this file, and together they form the routing map. A notable feature of these routes is that when they are loaded, the framework checks them for overall correctness and the sequence of methods used. In case of an exception, an error is generated with a reason for the exception. Since all routes in the routing map are subject to verification, this guarantees their overall correctness. After the first request or when using a special console command, routes are updated and cached. Therefore, route files should not include external code, only methods from the Route class. If after making changes to the routing map, the framework does not generate characteristic messages, these messages will not appear in the future, at least until the next changes in the connected route files. Routes are defined by methods of the Route class, one of the most commonly used being get(). The methods of this class are used exclusively in the routing map. ## Route::get() Method This method allows you to specify the handling of the HTTP GET method under specified conditions. As shown in the example: ``` Route::get('/', 'Hello, world!'); ``` The route will display the line "Hello, world!" when accessed at the root URL of the site. To render HTML code (which may contain PHP code) from a template, the method is used together with the view() function. ## Dynamic Addresses The HLEB2 framework processes arbitrary addresses according to the scheme defined by the application developer, for example: ``` Route::get('/resource/{version}/{page}/', 'Dynamic address used'); ``` In this case, all URL addresses matching the conditional scheme "site.com/resource/.../.../" will return the same text string, and the values "version" and "page" become accessible from the Hleb\Static\Request object: Request::param("version")->asString() and Request::param("page")->asPositiveInt(). These values can also be retrieved from the container and through the same-named arguments of the controller method. In the route address, you may specify that the last part can be optional: ``` Route::get('/profile/user/{id?}/', 'Variable ID value'); Route::get('/contacts/form?/', 'Optional end part'); ``` If the address is missing, it will still match this route, but the value of 'id' will be NULL. ## Default Values for Dynamic Addresses An example of a dynamic route in which default values are specified for the second and third named parts. ``` Route::get('/example/{first}/{second:two}/{third:three?}', 'defaults value in dynamic route'); ``` Similar to '/example/{first}/two/three?', only in the given Request, additional values 'second' => 'two', 'third' => 'three' will be added to the already existing dynamic parameter 'first'. If the final parameter is absent, it will be null. ## Variable Addresses Multiple route assignments (a numbered array of URL segments will appear in Request::param()): ``` Route::get('/example/...0-5/', 'From 0 to 5 arbitrary parts'); // or Route::get('/example/...1-3,7,9,11-20/', 'Number of parts within the specified range'); ``` ## Tag in Address The framework does not allow interpreting parts of the URL as compound segments, as this contradicts standards, but there is an exception to this rule. A common scenario is when a user's login is prefixed with a special @ tag in the URL. It can be set as follows: ``` Route::get('/profile/@{username}', 'Username with tag'); ``` ## Function view() The function specifies which template from the /resources/views/ folder to associate with the route. Example for the file /resources/views/index.php: ``` Route::get('/', view('index')); ``` Variables can be passed to the function as a second argument in an associative array. ``` Route::get('/', view('index', ['title' => 'Index page'])); ``` The variables will be available in the template. ``` redirect('/new/address/{%name%}', 301); ``` ## Route Grouping Route grouping is used to assign common properties to routes by adding methods to groups, which then apply the method's action to the entire group. The scope of a group is defined using the toGroup() method at the beginning of the group and endGroup() at the end. ``` Route::toGroup()->prefix('example'); // /example/first/page/ Route::get('first/page', 'First page content'); // /example/second/page/ Route::get('second/page', 'Second page content'); Route::endGroup(); ``` In this case, the prefix() method added to the group applies to all routes within it. Groups can be nested within other groups. There is also an alternative syntax for groups: ``` Route::toGroup() ->prefix('example') ->group(function () { // /example/first/page/ Route::get('first/page', 'First page content'); // /example/second/page/ Route::get('second/page', 'Second page content'); }); ``` ## Named Routes Each route can be assigned a unique name. ``` Route::get('/', view('default'))->name('homepage'); ``` This name can be used to generate its URL, making the code independent of the actual URL addresses. This is achieved by using route names instead of addresses. For example, this site operates using route names to build links to pages. ## Handling HTTP Methods Similar to the get() method for the HTTP GET method, there are methods like post(), put(), patch(), delete(), options() corresponding to POST, PUT, PATCH, DELETE, OPTIONS. These methods match their respective HTTP methods, except for options(). In all other cases, the OPTIONS method is handled according to the standard, but with options(), you can separately define how OPTIONS requests are processed (redefine them). ``` Route::options('/ajax/query/', '...')->controller(OptionsController::class); Route::post('/ajax/query/', '{"result": "ok"}')->name('post.example.query'); ``` ## Route::any() Method Assigned to a route, it matches all HTTP methods, behaving otherwise like get(). ## Route::match() Method Similar to the get() method, but with an additional first argument where you can pass an array of the supported HTTP methods. ``` Route::match(['get', 'post'], '/', 'Handler for POST and GET methods'); ``` ## Route::alias() Method The alias() method allows you to create a new route from an existing one by referencing its name. This is done by assigning a new address and executing additional actions defined by the group that the new route belongs to. ``` Route::get('/user/{id}/', view('user'))->name('profile.name'); Route::alias('/profile/{id}/', 'new.profile.name', 'profile.name'); ``` ## Route::fallback() Method Catches all unmatched paths for all HTTP methods (or specified ones). There can only be one fallback() method in routes for a specific HTTP method. This allows you to assign handling for an unmatched route (instead of a 404 error) for all types of HTTP methods or individually. ## Route Protection To protect routes from CSRF attacks, the protect() method is used. Assigning it to a route or group of routes adds a check for the presence of a special token set previously. ``` Route::get( '/ajax/query', 'Protected route')->protect(); ``` It works as follows: An access token is output on the page, you can use the csrf_token() or csrf_field() function. This token is sent via JavaScript or in a form with the request. The request's route has the protect() method and checks the token. ## Controller Assignment Controller is a part of the MVC architecture (Action-Domain-Responder for the web), responsible for the subsequent handling of a request identified by the router but should not contain business logic. A controller cannot be used for a group of routes; it is assigned to a specific one or individually. The controller() method is used for this. ``` use App\Controllers\DefaultController; Route::get('/')->controller(DefaultController::class, 'index'); ``` In the example, the first argument is the class of the assigned controller, the second is the controller method used. The 'index' method can be omitted as it is used by default. Note that the get() method no longer needs a second argument when a controller is used. ## Middleware Controllers If a controller can only be assigned once to a route, multiple middlewares can be applied. You can also assign a middleware to a group of routes. ``` Route::toGroup() ->middleware(FirstGeneralMiddleware::class) ->middleware(SecondGeneralMiddleware::class); Route::get('/example', '...')->middleware(GetMiddleware::class); Route::post('/example', '...')->middleware(PostMiddleware::class); Route::endGroup(); ``` The middleware() method means that the middleware will be executed before the main route handler. There is a similar method, before(), and a method after() (which runs after the main handler). The main handler here refers to the text returned by the route, the assigned template, or the execution of the controller. Assigned middlewares are executed in the order they are declared. The arguments for the middleware method are similar to the controller. You can specify the method to be executed as the second argument, with the default being 'index'. The difference is the presence of a third argument, which can pass an array of parameters to the middleware. These parameters are available in the Hleb\Static\Router::data() method or via the container. ## Modules A module is a type of controller. It points to the project's /modules/ directory and contains the name of the module being used. ``` Route::get('/section/')->module('default', DefaultModuleController::class); ``` ## Where() Method Validation A route can have dynamic parts in its URL, and with the where() method, you can define rules for those parts. ``` Route::toGroup() ->prefix('/{lang}/') ->where(['lang' => '[a-z]{2}']); Route::post('/profile/{user}/{id}', '...') ->where(['user' => '/[a-z]+/i', 'id' => '[0-9]+']); Route::endGroup(); ``` In this example, the parts named 'lang', 'user', and 'id' will be validated using regular expressions. ## Domain Limitation The special method domain() can be assigned to a route or group of routes. The first argument can specify the domain or subdomain name, and the second argument defines the level of rule matching. ## Substitution Principle There is a method where the target controller and method are determined based on the values of the dynamic URL. In this case, the route might look like this: ``` Route::get('/page/{controller}/{method}') ->controller('MainController', 'init'); ``` In this example, for the URL /page/part/first/, the framework will attempt to determine the controller as 'MainPartController' and the method as 'initFirst' (converted following the camelCase principle). The substitution principle in handlers should be managed carefully, as URL data may lead to unexpected controllers or methods being invoked. Additionally, you can specify dependencies on the request HTTP method by using the key '[verb]'. ``` Route::match(['get', 'post'], '/page/{target}') ->controller('Main[verb]Controller', '[verb]Method>'); ``` In this example, for the URL /page/example/, the framework will attempt to determine the controller as 'MainExampleGetController' and the method as 'getMethod' (converted following the camelCase principle). For the POST method, these will be 'MainExamplePostController' and 'postMethod'. The ability to perform substitutions can be particularly useful when distributing request HTTP methods across controller methods. ## Disabling Debug Mode in a Route In some routes, the output of the DEBUG panel may not be provided in debug mode. For instance, this applies to GET requests to an API where a response is expected in JSON format. There is a temporary way to disable debug mode by using a GET parameter _debug=off in the request, but there is also a permanent way by specifying the noDebug() method for a route. This method can also be applied to a group of routes. In this example, it is applied to all API requests. If the DEBUG panel output is disabled using the noDebug() method, but you still temporarily need to enable debug mode, it is enough to specify _debug=on in the GET parameters of the request. It is important to keep in mind that the enabling/disabling of debug mode discussed here is only relevant if the DEBUG mode is active in the configuration settings; otherwise, it remains completely disabled. ## Updating Route Cache By default, the route cache in the framework is automatically updated after changes are made to the /routes/map.php file. There is also a console command to update the route cache: ```$ php console --routes-upd``` For high-traffic projects, you might need to disable automatic updates in production and only recalculate the route cache using the console command. This is configured via the 'routes.auto-update' setting in the /config/common.php file. ------------------------- # Model Model is a component of the architectural pattern MVC (Action-Domain-Responder for the web). In the HLEB2 framework, the Model is represented by a template that has static access methods. The Model can provide access to a certain dataset, usually a connected DBMS (Database Management System). The provided template can be used by the developer in their own way. It can use the built-in wrapper over PDO (class Hleb\Static\DB) or be replaced by your own template, for example, by connecting one of the existing ORM. ## Creating a Template Apart from copying and modifying the demonstration file DefaultModel.php, there is another simple way to create the required class using a console command. ```$ php console --add model ExampleModel``` This command will create a new template /app/Models/ExampleModel.php. You can use another suitable name for the class. The HLEB2 framework also allows you to create a custom template by default for this command. ------------------------- # Console Commands The framework HLEB2 includes both built-in console commands and the capability for developers using the framework to create their own. Console commands are executed from the terminal or task scheduler, and their entry point is the 'console' file located in the project root, which is a regular PHP file. ## Standard Commands You can get the list of framework commands by running the console command: ```$ php console --help``` --version or -v(displays the current version of the framework) --info or -i [name](shows current settings from common) --help or -h(displays the default list of commands) --ping(service check, returns predefined value) --logs or -lg(outputs the last lines from log files) --list or -l(displays the list of added commands) --routes or -r(formatted list of routes) --find-route (or -fr) [method] [domain](route search by URL) --route-info (or -ri) [method] [domain](route info by URL) --clear-routes-cache or -cr(removes route cache) --update-routes-cache or --routes-upd or -u(updates route cache) --clear-cache or -cc(clears framework cache) --add [desc](creates a class) --create module (creates module files) --clear-cache--twig or -cc-twig(clears cache for Twig template engine) --help(displays command help) ## Creating Your Own Console Command Example of adding your own console command by creating the corresponding class in the /app/Commands/Demo/ folder: ``` setResult() method within the class to set the data, and then access this data externally via the getResult() method. ``` use App\Commands\Demo\ExampleTask; use Hleb\Static\Command; $task = new ExampleTask(); Command::execute($task, ['speed', 'quality']); echo $task->getResult(); ``` ## Specifying Text Color in the Terminal To output text or a portion of it in one of the basic terminal colors, use the specially designated color() method in the command. For example: ``` color()->green('this text is green'); $yellowText = $this->color()->yellow('this text is yellow'); echo $greenText . " and " . $yellowText . PHP_EOL; return self::SUCCESS_CODE; } } ``` ## Setting Command Restrictions with Attributes The type and intended use of created commands can be controlled using PHP attributes. The attribute #[Purpose] is used to define the command’s visibility scope. ``` container->get(CsrfInterface::class)->token(); // variant 2 $token = $this->container->csrf()->token(); ``` Example of accessing the Csrf service in template code: ```
``` For TWIG template engine: ```
``` The Csrf object can also be obtained through dependency injection using the Hleb\Reference\Interface\Csrf interface. ## token() The token() method returns a unique user session token. ## field() The field() method returns HTML content to insert in the form to pass the token with other data. ## validate() This method allows manual token validation (if protection is not enabled on the route). ------------------------- # Settings The Settings service allows you to obtain standard or custom framework settings from the files within the /config/ directory. Methods of using Settings in controllers (and all classes inherited from Hleb\Base\Container) exemplified by retrieving the designated timezone from the /config/common.php file: ``` // variant 1 use Hleb\Reference\SettingInterface; $timezone = $this->container->get(SettingInterface::class)->getParam('common', 'timezone'); // variant 2 $timezone = $this->container->settings()->getParam('common', 'timezone'); // variant 3 $timezone = $this->settings()->getParam('common', 'timezone'); ``` Example of accessing Settings within application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\SettingInterface; $timezone = Container::get(SettingInterface::class)->getParam('common', 'timezone'); // variant 2 use Hleb\Static\Settings; $timezone = Settings::getParam('common', 'timezone'); // variant 3 $timezone = config('common', 'timezone'); ``` The Settings object can also be obtained through dependency injection via the interface Hleb\Reference\Interface\Setting. Settings are divided into four groups: 'common', 'main', 'database', and 'system'. They correspond to the configuration files within the /config/ directory. If a different file is being used, such as 'main-local.php' instead of 'main.php', the setting must still be retrieved using the name 'main'. The service methods - common(), main(), database(), and system() allow for retrieving parameters from the respective settings. For example: ``` use Hleb\Static\Settings; $timezone = Settings::common('timezone'); ``` ------------------------- # Redirection The Redirect service provides a method to redirect to an internal page or a full URL. Since this service is based on the 'Location' header, it must be applied before any content is rendered. The redirection can be executed in a controller or middleware, for example: ``` // variant 1 use Hleb\Reference\RedirectInterface; $this->container->get(RedirectInterface::class)->to('/internal/url/', status: 307); // variant 2 $this->container->redirect()->to('/internal/url/', status: 307); ``` Additionally, the Redirect object can be obtained through dependency injection using the Hleb\Reference\Interface\Redirect interface. To redirect to a route address by its name, use Redirect together with the Router service, which allows you to retrieve this address. ``` $this->container->redirect()->to(url('route.name')); ``` ------------------------- # Logging Service The Log service is a logging mechanism in the HLEB2 framework that allows storing errors and messages in a dedicated log storage. The principle of log retention in the framework is based on PSR-3. By default, the framework uses a built-in logging mechanism that saves logs to a file. All PHP errors and the operation of the application itself are logged, as well as informational and debug logs specified by the developer in the code. The framework's standard file logs are stored in the project's /storage/logs/ folder. Ways to use Log in controllers (and all classes inherited from Hleb\Base\Container) are exemplified by adding an informational message: ``` // variant 1 use Hleb\Reference\LogInterface; $this->container->get(LogInterface::class)->info('Sample message'); // variant 2 $this->container->log()->info('Sample message'); ``` Example of logging in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\LogInterface; $data = Container::get(LogInterface::class)->info('Sample message'); // variant 2 use Hleb\Static\Log; Log::info('Sample message'); // variant 3 logger()->info('Sample message'); ``` The Log object can also be obtained through dependency injection via the Hleb\Reference\Interface\Log interface. For simplicity, examples hereafter will only use the function logger(). Executing one of the previous examples will create a log file in the /storage/logs/ directory (if it did not exist previously) with a line added similar to this: [13:01:12.211556 10.01.2024 UTC+03] Web:INFO Sample message {/path/to/project/app/Controllers/TestController.php on line 31} {App\Controllers\TestController->get()} GET http://example-domain.ru/test-log 127.0.0.1 #{"request-id":"71cc0539-af41-556d-9c48-2a6cd2d8090f","debug":true} The log text shows that a message 'Sample message' was output with a specified level 'INFO', along with additional information about the log call, precise time, and basic request data. Confidential information and data within logs, whose disclosure could lead to security breaches of the project, are not recommended to be sent to third-party services for log storage as they can be susceptible to hacking. ## Logging Levels When choosing a logging level, you should be guided by the content and importance of the data being output. The list from ordinary messages to critical errors in ascending order: debug() - debug messages, usually used only during project development. By default, the framework settings have a maximum level set below (info), and these messages will not be saved to the log. info() - informational messages that are necessary to understand how a particular part of the code functions and if all conditions are met. Here you can output a specific SQL query so you can later verify its correct execution. notice() - notifications about events in the system. For example, it can signal an approach to a critical threshold of some important value that has not yet been reached. warning() - for logging exceptional cases, not as critical errors, but as warnings. For example, the use of deprecated APIs, misuse of APIs, and other undesirable cases. error() - runtime errors occurring under certain conditions. These errors do not require immediate action but should be registered and monitored. critical() - critical errors in the program, such as the unavailability of one of the components. alert() - general system unavailability, which could be a database failure, entire website downtime, etc. Actions to resolve this should be taken immediately. emergency() - the system is completely unusable. ## Logging Context According to PSR-3, you can pass a named array of data as the second argument for substitution in the message text, for example: ``` logger()->error('Failed to create user {name}', ['name' => 'Ivar']); ``` In the built-in framework log, you can also add other data to the array, and they will be output by key in the log in the section with 'request-id'. Third-party logging mechanisms may not support this feature. ## Alternative Logger The HLEB2 framework supports only one active instance of the logging mechanism; if you need to replace it with a third-party logger, this must be done during the framework initialization. This necessity is justified by the fact that error logging should start from the very beginning of loading and operation of the framework itself. ## Logging Settings In the /config/common.php file: log.enabled - enables/disables saving to logs, which can be useful when temporarily disabling logging to reduce application load. max.log.level - sets the maximum logging level (from messages to critical errors). For example, if you set the level to 'warning', logs with levels 'debug', 'info', and 'notice' will not be saved. max.cli.log.level - the maximum logging level when using the framework via console commands from the terminal or task scheduler. error.reporting - this parameter relates to the error level but is also related to logging as it determines which errors will enter the log. log.sort - for standard file logging, it splits logs by source (site domain). log.stream - outputs logs to the specified output stream if specified, for example, '/dev/stdout'. log.format - two formats are available for standard logging, 'row' (default) and 'json', the latter converts log outputs into JSON format. In the /config/main.php file: db.log.enabled - logs all queries to the databases. ## Usage Examples General examples that show the difference between logging errors and regular informational logs: ``` // Will output to the log. logger()->info('Info message'); try { throw new ErrorException('Warning message'); } catch(\ErrorException $e) { // Will output an error to the log and continue execution. logger()->warning($e); } // Will output an error to the log and interrupt execution. throw new ErrorException('Error message'); ``` ## Viewing Logs With standard file storage of logs, the most recently added logs can be displayed in the terminal using the console command: ```$ php console--logs 3 5``` The specified command will display the last three logs for the five most recent log files by date. In the log record (by default, in files), each log entry has a "request-id" label, which can be used to filter all logs for a specific request. For UNIX systems and macOS, you can use the 'grep' command to search by error type: ```$ grep -m10 :ERROR ./storage/logs/*``` This command's flexibility allows searches under various conditions, including by "request-id" of a request. For Windows, an alternative would be the 'findstr' command: ```D:\project>findstr /S /C:":ERROR" "storage/logs/*"``` ## Log Rotation The framework includes the App\Commands\RotateLogs class, a console command implementation for deleting outdated log files. ```$ php console rotate-logs 5``` This command will delete all log files created more than five days ago. By default, it is set to three days. The command is intended for manual rotation or to be added to a task scheduler (for daily execution). To enable the framework to automatically monitor the maximum size of log files, configure the 'max.log.size' option in the /config/common.php file. The value is specified as an integer in megabytes. However, with this setting active, if there is an unexpectedly high log volume within the current day, all logs from the previous day may be deleted. ------------------------- # Router Service The Router service is designed for interacting with route data in the HLEB2 framework. Ways to use Router in controllers (and all classes inherited from Hleb\Base\Container) demonstrated with relative URL formation by route name: ``` // variant 1 use Hleb\Reference\RouterInterface; $uri = $this->container->get(RouterInterface::class)->url('route.name'); // variant 2 $uri = $this->container->router()->url('route.name'); // variant 3 $uri = $this->router()->url('route.name'); ``` Example of accessing Router in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\RouterInterface; $uri = Container::get(RouterInterface::class)->url('route.name'); // variant 2 use Hleb\Static\Router; $uri = Router::url('route.name'); ``` The Router object can also be obtained via dependency injection using the Hleb\Reference\Interface\Router interface. For simplicity, further examples will only include references through Hleb\Static\Router. ## url() The url() method is intended for converting a route name into a relative URL address. A simple example: ``` Route::get('/example/simple/page', '...')->name('simple.route.name'); ``` ``` use Hleb\Static\Router; echo Router::url('simple.route.name'); // /example/simple/page ``` Since route addresses may have dynamic parameters and an optional trailing part, specify these in additional arguments when present. ``` Route::get('/example/{type}/page?', '...')->name('dynamic.route.name'); ``` ``` use Hleb\Static\Router; $uri = Router::url('dynamic.route.name', ['type' => 'special'], endPart: false); ``` ## address() The address() method is similar to the url() method but returns the full URL including the HTTP scheme and domain name from the current request. Since the domain is assigned only the current one, use concatenation with Route::url() for another domain. The returned address for the specified methods will include or exclude a trailing slash based on the corresponding framework settings. Built-in framework functions url() and address() are shorthand for calling the same-named Router methods. ## name() The name() method can be used to find out the name of the current route, if it is assigned. ## data() The data() method returns data for the current middleware if it has been set in the route. It can be used only in middleware. ------------------------- # Data Caching The framework's Cache service is a simple file cache for data. Its methods support PSR-16. The caching works as follows: Data is stored in the cache with a unique key, specifying a ttl in seconds. Within this time, starting from cache creation, cache requests by this key return cached data, which remains unchanged. The cache can be forcibly cleared by key or entirely at any time. If the cache was not created, cleared, or expired, a new cache will be created for the specified duration. The built-in service implementation supports main PHP data types—strings, numeric values, arrays, objects (via serialization). If you need more advanced caching features, add another implementation to the container, replacing or supplementing the current one. This could be the github.com/symfony/cache component. Methods for using Cache in controllers (and all classes inheriting from Hleb\Base\Container) using the example of retrieving cache by key: ``` // variant 1 use Hleb\Reference\CacheInterface; $data = $this->container->get(CacheInterface::class)->get('cache_key'); // variant 2 $data = $this->container->cache()->get('cache_key'); ``` Example of retrieving cache from Cache in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\CacheInterface; $data = Container::get(CacheInterface::class)->get('cache_key'); // variant 2 use Hleb\Static\Cache; $data = Cache::get('cache_key'); ``` The Cache object can also be accessed through dependency injection via the Hleb\Reference\Interface\Cache interface. To simplify examples, further ones will only use access through Hleb\Static\Cache. ## Unique Key The most challenging aspect of this caching method (besides invalidation) is choosing a unique key that uniquely identifies the cached data. For instance, if you're caching data obtained from a database with a specific query, the key should include information about this query, as well as the database name if a similar query could be made from different databases. ## Cache Initialization In this example, a test verification result will be added to the cache with an expiration period of one minute. Naturally, in real conditions, you should choose data for caching where forming it is more resource-intensive than using the cache. ``` use Hleb\Static\Cache; $key = 'example_cache_key'; if (!Cache::has($key)) { $data = mt_rand(); // Receiving data. Cache::set($key, $data, ttl: 60); } else { $data = Cache::get($key); } ``` The methods get(), set(), and has() have been used here respectively for retrieving, adding to the cache, and checking its existence by key. These three methods are replaced by a single method getConform(), which operates with a Closure function to get data if they are not found in the cache. ``` use Hleb\Static\Cache; $data = Cache::getConform('example_cache_key', function () { return mt_rand(); // Receiving data. }, ttl: 60); ``` Example with a closure function that uses an external context: ``` use Hleb\Static\Cache; $param = 10; $data = Cache::getConform('example_cache_key', function () use ($param) { return mt_rand() * $param; // Data calculation. }, ttl: 60); ``` ## Clearing Cache The entire cache within the framework is cleared by using the clear() method, but caution must be taken with a large amount of cache. This call should be used rather infrequently, and it can also be done via a console command: ```$ php console --clear-cache``` Clearing the entire cache will only affect the cached template data and the framework data added by the Cache service. The TWIG templating engine has its own cache implementation, and a separate console command is provided for clearing it. If there is a need to delete the cache by one of the keys, this can be done using the delete() method. To have the framework automatically track the maximum cache size, you need to configure the 'max.cache.size' option in the /config/common.php file. The value is represented as an integer in megabytes. Due to the uneven distribution of cache in the files, this will be an approximate tracking of the maximum directory size for cache files. If caching is not occurring, make sure the 'app.cache.on' setting is enabled in the /config/common.php file; this is recommended to be disabled in debug mode. ------------------------- # Converting to PSR To use external libraries that employ contracts based on PSR recommendations, you may need to convert your own framework entities into the appropriate PSR objects. Due to the framework's principle of self-sufficiency and initial rejection of external dependencies, the framework's system classes are similar to standard ones, but have their own interface. To adhere to the standards, this is addressed by using the Converter adapter, implemented as a Service. The Converter service provides methods to obtain objects according to PSR interfaces, derived from the system objects of the HLEB2 framework. Methods of using Converter in controllers (and all classes inherited from Hleb\Base\Container) exemplified by retrieving an object for logging using PSR-3: ``` // variant 1 use Hleb\Reference\ConverterInterface; $logger = $this->container->get(ConverterInterface::class)->toPsr3Logger(); // variant 2 $logger = $this->container->converter()->toPsr3Logger(); ``` Example of retrieving a logger object from the Converter service within application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\ConverterInterface; $logger = Container::get(ConverterInterface::class)->toPsr3Logger(); // variant 2 use Hleb\Static\Converter; $logger = Converter::toPsr3Logger(); ``` The Converter service can also be obtained through dependency injection via the interface Hleb\Reference\Interface\Converter. ## toPsr3Logger The toPsr3Logger() method returns a logging object with the PSR-3 interface (Psr\Log\LoggerInterface). ## toPsr11Container The toPsr11Container() method returns a container object with the PSR-11 interface (Psr\Container\ContainerInterface). ## toPsr16SimpleCache The toPsr16SimpleCache() method returns a caching object with the PSR-16 interface (Psr\SimpleCache\CacheInterface). ## PSR-7 Objects There are a sufficient number of third-party libraries for handling PSR-7 objects, so including another implementation in the framework is unnecessary. For example, they can be created using the popular Nyholm\Psr7 library: ``` use Hleb\Reference\RequestInterface; use Hleb\Reference\ResponseInterface; use Hleb\Static\Container; // Request $rq = Container::get(RequestInterface::class); $psr7Response = new \Nyholm\Psr7\Request( $rq->getMethod(), (string)$rq->getUri(), $rq->getHeaders(), $rq->getRawBody(), $rq->getProtocolVersion(), ); // Response $rs = Container::get(ResponseInterface::class); $psr7Response = new \Nyholm\Psr7\Response( $rs->getStatus(), $rs->getHeaders(), $rs->getBody(), $rs->getVersion(), $rs->getReason(), ); ``` The set of parameters in the constructor depends on the chosen library. To avoid initializing this way each time, the implementation can be delegated to a separate class or service. ------------------------- # DB Service — Using Databases The DB service provides the initial capability to send queries to databases. Using a wrapper over PDO and the database configuration of the HLEB2 framework, the service offers simple methods to interact with various databases (supported by PDO). The PHP PDO extension and necessary database drivers must be enabled for this service to work. To use a different connection method, such as ORM(Object-Relational Mapping), add the instantiation of the chosen ORM as a service container using the framework's configuration settings. According to the project's structure provided with the HLEB2 framework, the DB service can only be used in Model classes. A Model class (whose template can be created using a console command) acts as a basic framework for use within MVC (Action-Domain-Responder for web). It can be adapted or replaced according to preference for the selected AR(Active Record) or ORM library (and then adjust the template for the console command). Examples of usage in a Model for database queries: ``` get(DbInterface::class)->dbQuery($query); // variant 2 $data = self::container()->db()->dbQuery($query); // variant 3 $data = DB::dbQuery($query); return $data; } } ``` The following methods of the DB service are used for executing database queries. ## dbQuery() The dbQuery() method was used in the examples above for creating direct SQL queries to the database. The query and query parameters are not separated in it, so every suspicious parameter, especially those coming from a Request, must be handled (with proper escaping) using the special quote() method. ``` use Hleb\Static\DB; $result = DB::dbQuery(sprintf("SELECT * FROM users WHERE name='%s' AND address='%s'", DB::quote($name), DB::quote($address))); ``` Escaping query parameters ensures protection against SQL injection. Such attacks are based on injecting arbitrary SQL expressions as part of external data. Another method of the DB service is more versatile and simplifies parameter handling. ## run() When successfully executed, the run() method returns an initialized PDOStatement object. All methods of this object, such as fetch() and fetchColumn(), are standard for PDO. ``` use Hleb\Static\DB; $result = DB::run("SELECT * FROM users WHERE name=? AND address=?", [$name, $address])->fetchAll(); ``` The capabilities of PDOStatement are described in the PDO documentation. ## Asynchronous Queries For asynchronous queries, using this service is similar and depends on the configuration of the web server in use. Additionally, some ORMs are adapted to support this mode of operation. One such library, as indicated in its documentation, is Cycle ORM. ------------------------- # Sessions The user session mechanism in the HLEB2 framework is provided by the Session service — a simple wrapper around PHP's session management functions. Examples of using Session in controllers (and all classes inheriting from Hleb\Base\Container), such as retrieving a value from a session: ``` // variant 1 use Hleb\Reference\SessionInterface; $value = $this->container->get(SessionInterface::class)->get('session_name'); // variant 2 $value = $this->container->session()->get('session_name'); ``` Example of accessing a session in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\SessionInterface; $value = Container::get(SessionInterface::class)->get('session_name'); // variant 2 use Hleb\Static\Session; $value = Session::get('session_name'); ``` The Session object can also be obtained through dependency injection via the Hleb\Reference\Interface\Session interface. To simplify examples, the following will only include access through Hleb\Static\Session. In the standard Session service implementation, methods appropriately use the global $_SESSION variable. ## get() The get() method retrieves session data by parameter name. ``` use Hleb\Static\Session; $value = Session::get('session_name'); ``` ## set() The set() method allows assigning session data by name. ``` use Hleb\Static\Session; Session::set('session_name', 'value'); ``` ## delete() The delete() method removes session data by name. ## clear() The clear() method removes all session data. ## all() The all() method returns an array with all session data. ## getSessionId() The getSessionId() method returns the current session identifier. The session identifier can be modified in the 'session.name' configuration setting in the /config/system.php file, and is initially set to 'PHPSESSID'. ## Asynchronous Mode In asynchronous use of the framework, the methods of the Session service function similarly, but a different mechanism for setting and reading them is used. ------------------------- # Response Object The framework's Response service holds global data for forming a response to the client. When using the framework asynchronously, this data reverts to default values after each request ends. Methods for assigning data to Response in controllers: ``` // variant 1 use Hleb\Reference\ResponseInterface; $this->container->get(ResponseInterface::class)->set('Hello, world!'); // variant 2 $this->container->response()->set('Hello, world!'); // variant 3 $this->response()->set('Hello, world!'); ``` The method is similar for all classes inheriting from Hleb\Base\Container, but forming a response directly in Response outside the controller is considered bad practice. Example of using Response in application code (the code will also be difficult to maintain in this case): ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\ResponseInterface; Container::get(ResponseInterface::class)->set('Hello, world!'); // variant 2 use Hleb\Static\Response; Response::set('Hello, world!'); ``` Access to the Response service can also be obtained through dependency injection via the Hleb\Reference\Interface\Response interface. To simplify examples, they will only include access via DI from now on. Combined with print and echo outputs, data from Response will be shown later; the correct strategy is to use only one method for outputting results. At the end of a request, the framework will still refer to the specified Response object for output, even if this object wasn't returned from the controller. This can be handy for one-time or sequential data addition in Response within a single controller method. If it's necessary to manipulate response objects containing different data, any other Response can be used according to PSR-7. The alternative Response must be returned in the invoked controller method. ## Response Body The response body consists of data added to the Response object, which can be converted into a string. Typically, this is message text displayed to the user or data in the format of JSON or XML, possibly dynamically generated HTML, etc. The following methods of the Response service are available for adding data: set() or setBody() — assigns data, completely overwriting any previous response body if it exists. add() or addToBody() — appends to the end of the previously added data. To retrieve data from the service: get() or getBody() — retrieves the current state of the response body in the Response object. Before sending data to the client, ensure it is checked for XSS vulnerabilities. If the data has not been processed in this way before, it can be passed through the PHP function htmlspecialchars(). ## HTTP Response Status By default, the status is set to 200. If the response should have a different status, use the setStatus() method, with the first argument being the status and the second a short status message if it differs from the standard. In the status '404 Not Found', such a message is 'Not Found'. Standard status messages are usually used, so you can set the status by number directly in the set() method as the second argument. The method getStatus() allows you to obtain the current HTTP status from the Response service. ## Response Headers Besides the global server-side response headers, you can specify your own headers to be returned with a specific response from the framework. The following methods of the Response service are intended for this second type of headers. ``` setHeader('Content-Type', 'application/json'); $headerData = $response->getHeader('Content-Type'); var_dump($headerData); // array(1) { [0]=> string(16) "application/json" } $response->setHeader('Content-Type', 'text/html; charset=utf-8'); $headerData = $response->getHeader('Content-Type'); var_dump($headerData); // array(1) { [0]=> string(24) "text/html; charset=utf-8" } return $response; } } ``` The setHeader() method sets a header by name, overriding the previous value if it was set. In the rare case where multiple identical headers are needed, the replace argument allows adding a header to the current value. The hasHeader() method checks if a header exists by name. The getHeader() method is designed to obtain an array of header data by name. The getHeaders() method returns the data of all headers set in Response as an array. While operations on headers using standard PHP functions will work in conjunction, conflicts may arise when used together with the Response object. It is better to use just one approach throughout the application. ## HTTP Protocol Version The default HTTP protocol version is '1.1' unless determined from the current request. Since the return value should usually match the request itself, changes are rarely used. Nevertheless, the getVersion() and setVersion() methods are available for getting and setting the version respectively. ------------------------- # Cookies The HTTP cookies in the HLEB2 framework are handled by the Cookies service. Examples of using Cookies in controllers (and all classes inheriting from Hleb\Base\Container), such as retrieving a value from cookies: ``` // variant 1 use Hleb\Reference\CookieInterface; $value = $this->container->get(CookieInterface::class)->get('cookie_name'); // variant 2 $value = $this->container->cookies()->get('cookie_name'); // variant 3 $value = $this->cookies()->get('cookie_name'); ``` Example of accessing cookies in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\CookieInterface; $value = Container::get(CookieInterface::class)->get('cookie_name'); // variant 2 use Hleb\Static\Cookies; $value = Cookies::get('cookie_name'); ``` The Cookies object can also be obtained through dependency injection via the Hleb\Reference\Interface\Cookie interface. To simplify examples, the following will only include access through Hleb\Static\Cookies. ## get() The get() method returns the cookie by name as an object. Through this object, you can obtain both raw data and data transformed into the required format. The framework handles HTML tag transformation, which is necessary if the data is to be displayed on a page to avoid potential cookie-based XSS vulnerabilities. The example shows various ways to retrieve the cookie value: ``` use Hleb\Static\Cookies; // (!) Original raw data. $rawValue = Cookies::get('cookie_name')->value(); // Validated data converted to string. $clearedStringValue = Cookies::get('cookie_name')->asString(); // Data converted to an integer. $integerValue = Cookies::get('cookie_name')->asInt(); // Data checked for a positive integer. $positiveIntegerValue = Cookies::get('cookie_name')->asPositiveInt(); ``` ## all() The all() method returns a named array of objects similar to those obtained with the get() method, from which you can retrieve values of all or specific cookies. The most common error when using the object returned by these methods is treating the object as a value instead of retrieving the value from the object. ## set() The set() method is used to set or update a cookie by its name. The first argument is the cookie name, the second one is the value to be assigned. The third argument 'options' expects an array of additional parameters, similar to the PHP function setcookie(), where you can set options like 'expires', 'path', 'domain', 'secure', 'httponly', and 'samesite'. ``` use Hleb\Static\Cookies; $options = [ 'expires' => time() + 60 * 60 * 24 * 30, 'path' => '/', 'domain' => '.example.com', // leading dot for compatibility or use subdomain 'secure' => true, // or false 'httponly' => true, // or false 'samesite' => 'None' // None / Lax / Strict ]; Cookies::set('cookie_name', 'value', $options); ``` ## delete() The delete() method is used for deleting a cookie by its name. ## clear() The clear() method allows you to clear all cookies. ## Asynchronous Mode In the asynchronous usage of the framework, the methods of the Cookies service function similarly, but a different mechanism is used for setting and reading them. ------------------------- # File Path Manager For application versatility and portability, all operations involving file and directory path references within the project must be relative to its root directory. In the HLEB2 framework, the file path manager is handled by the Path service. It enables manipulation of relative file paths in the project by providing a wrapper over the corresponding PHP functions. Usage of Path in controllers (and any classes inheriting from Hleb\Base\Container) as an example of obtaining a full path from the root directory: ``` // variant 1 use Hleb\Reference\PathInterface; $path = $this->container->get(PathInterface::class)->getReal('@storage/public/files'); // variant 2 $path = $this->container->path()->getReal('@storage/public/files'); ``` Example of defining a file path in the application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\PathInterface; $path = Container::get(PathInterface::class)->getReal('@storage/public/files'); // variant 2 use Hleb\Static\Path; $path = Path::getReal('@storage/public/files'); ``` The Path object can also be obtained through dependency injection via the Hleb\Reference\Interface\Path interface. To simplify examples, only usage through Hleb\Static\Path will be shown in further examples. ## The @ Symbol In the examples above, there is a '@' symbol at the beginning of the relative path. It indicates that the path starts from the root directory of the project. If the project's root directory is /var/www/hleb/, the example would return the string '/var/www/hleb/storage/public/files'. On Windows, the result would look slightly different, but it would still be a valid full path to the specified folder. The prefix '@storage' is predefined for the framework. Here is a list of other assigned mappings: '@' - the root directory of the project with the HLEB2 framework. The path can be specified arbitrarily, for example '@/other/folder'. '@app' - the path to the project's /app/ folder. '@public' - the path to the project's /public/ folder with public project files, which is targeted by the web server. Even if the name is changed, it will still correspond to '@public'. '@storage' - the path to the project's /storage/ folder, where caches, logs, and other auxiliary files are stored. '@resources' - the path to the project's /resources/ folder. This folder contains various project resources: page templates, email templates, build templates, etc. '@views' - the path to the project's /resources/views/ folder. '@modules' - the path to the project's /modules/ folder, even if the module directory name has been changed in the settings. '@vendor' - the path to the project's library folder, which remains the same even if the folder name is different. Thus, any path within the project is allowed, so transferring to a server with a different directory structure or to another folder won't be an issue, as paths will always point to the correct location. The Path service has several methods that correctly recognize relative paths starting with '@'. A trailing slash for a relative path string, such as '@storage/logs/', is significant. The full path returned by the method will include the trailing slash in this case. ## getReal() The getReal() method can be seen in the examples above. It returns a string with the full path derived from a relative one. If the specified path does not exist, the method returns false. The hl_realpath() framework function works in the same way. ## get() The get() method differs from getReal() in that it will return a string for the full path even if the path does not exist, without checking for existence. The function hl_path() can be used as an alternative to this method. ``` use Hleb\Static\Path; $dir = Path::get('@/non-existent/dir'); $file = Path::get('@/non-existent/file.txt'); $file = hl_path('@/non-existent/file.txt'); ``` ## relative() This method differs from other methods of the Path service in that it takes a full path and returns a relative one with '@' at the beginning. Sometimes it is necessary to output the relative path in project logs or in other places, hiding the full path. The relative() method helps in such cases. ``` use Hleb\Static\Path; $path = Path::relative(__FILE__); ``` The example shows obtaining the relative path to the current file. ## createDirectory() The createDirectory() method creates a directory (if it does not exist) along with any nested subfolders by the specified relative path with '@' at the beginning or a full path. ## exists() The exists() method is used to check for the existence of a file or directory. It accepts both full paths and relative paths with '@' at the beginning. The framework function hl_file_exists() has a similar action. ## contents() The contents() method is a wrapper around file_get_contents(), but it can also accept a relative path starting with '@' in addition to a full path. This method is duplicated by the framework function hl_file_get_contents(). ## put() This method is similar to the file_put_contents() function. Besides a full path, the put() method can also accept a relative path starting with '@'. The framework function hl_file_put_contents() can be used as an alternative to this method. ## isDir() The isDir() method is a wrapper around the is_dir() function, and it can accept both a full path and a relative path starting with '@'. The function hl_is_dir() can be used instead of this method. ## Asynchronous Requests Some file operations, such as writing to a file, are blocking for asynchronous calls, so it is recommended to use their asynchronous-supported alternatives. ------------------------- # Request Object The system Request object is created at the very beginning of the framework's HTTP request processing. It is not only created but also populated with information (headers, parameters, etc.) This object facilitates the initial functioning of the HLEB2 framework while processing a request. The system Request is solely intended for this purpose. The Request service, which can be obtained from the container by default and through which the current request data can be utilized, is a wrapper over the system object. Methods of obtaining data from the Request in controllers (and all classes inherited from Hleb\Base\Container) using the current HTTP method: ``` // variant 1 use Hleb\Reference\RequestInterface; $method = $this->container->get(RequestInterface::class)->getMethod(); // variant 2 $method = $this->container->request()->getMethod(); // variant 3 $method = $this->request()->getMethod(); ``` Example of obtaining the HTTP method from the Request in application code: ``` // variant 1 use Hleb\Static\Container; use Hleb\Reference\RequestInterface; $method = Container::get(RequestInterface::class)->getMethod(); // variant 2 use Hleb\Static\Request; $method = Request::getMethod(); ``` Additionally, the Request object can be obtained through dependency injection via the Hleb\Reference\Interface\Request interface. To simplify examples, henceforth they will only contain references through Hleb\Static\Request. ## HTTP Request Method A request to the application is made with a specific HTTP method; the framework supports the following: 'GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS' (and 'HEAD'). The methods getMethod() and isMethod() help determine the current method. The former returns a value like 'GET', while the isMethod(...) method requires specifying the sought value for comparison. ## Parameters from $_GET, $_POST, and Request Body Data sent along with the request can be used in various ways. They can be stored in their original form without requiring preliminary processing. However, if they need to be displayed immediately in the response, the data should be secured against the injection of executable scripts. The get() and post() methods return an object with the corresponding parameter. This object can be used to obtain both raw data and data transformed into the required format. ``` use Hleb\Static\Request; // (!) Original raw data. $rawValue = Request::get('param')->value(); // Validated data converted to string. $clearedStringValue = Request::get('param')->asString(); // Data converted to an integer. $integerValue = Request::get('param')->asInt(); // Data checked for a positive integer. $positiveIntegerValue = Request::get('param')->asPositiveInt(); ``` The most common mistake when using the object returned by these methods is using this object as a value instead of obtaining the value from the object. If you need to get the result as an array, for example, for a query with '?param[key]=value', the object with the value has an asArray() method, where array values will be protected from XSS vulnerabilities. The value() method returns an array but contains raw, unprocessed data. The input() method is used to determine and retrieve an array of data from the body of the request. This can be JSON data or url-encoded parameters transformed into an array. Thus, you can retrieve as an array POST-, PUT-, PATCH-, or DELETE parameters or parameters passed in the body of the request in JSON format. Data obtained with the input() method represents processed values with HTML tags converted into special characters. If you need to get the body of the request as an array in its original form, the getParsedBody() method is designed for this purpose. It is similar to input(), but it returns data in unprocessed form. If the previous formats do not fit, the request body in its original form, as a string value, is returned by the getRawBody() method. This way, you can transform the data into the required format yourself. ## Dynamic Route Parameters These request parameters refer to the dynamic parts of the route, with specific values assigned to them in the request. The param() method allows retrieving these values by the name of the dynamic parameter. The result will be an object through which the value can be accessed in the desired format. For instance, if a request matches a route of this type: ``` Route::get('/{version}/{page}/')->controller(ExampleController::class); ``` For the URL /10/main/, the parameters 'version' and 'page' are defined as follows: ``` use Hleb\Static\Request; $page = Request::param('page')->asString(); // main $version = Request::param('version')->asPositiveInt(); // 10 ``` A common mistake when using the object returned by this method can be using this object as the value itself, rather than extracting the value from the object. The data() method returns an array of objects for all dynamic parameters. Values from these objects can similarly be accessed in both raw and processed formats. To retrieve the original dynamic route parameters as an array of values without processing, use the rawData() method. Note that when the framework processes incoming data (when selected), it only protects against XSS attacks. In other cases, such as when quote escaping is required for database storage, additional security measures must be applied. ## Request URI Data The object returned by the getUri() method is based on the UriInterface from PSR-7, enabling you to retrieve the following request data: getUri()->getHost() - the domain name of the current request, such as 'mob.example.com'. It may include the port if it’s specified in the request. getUri()->getPath() - the path in the address following the host, e.g., '/ru/example/page' or '/ru/example/page/'. getUri()->getQuery() - the query parameters, such as '?param1=value1¶m2=value2'. getUri()->getPort() - the request port. getUri()->getScheme() - the HTTP scheme of the request, 'http' or 'https'. getUri()->getIp() - the request IP address. ## Request HTTP Scheme To specify the type of HTTP scheme, use the isHttpSecure() method. It returns whether the scheme is 'https'. The getHttpScheme() method returns the current HTTP scheme as either 'http://' or 'https://'. ## Getting the Host from the Address The getHost() method is used to retrieve the domain name of the current request. It is equivalent to getUri()->getHost(). Together with the HTTP scheme, you can get the host address by using the getSchemeAndHost() method. ## Request URL The getAddress() method returns the full URL of the request, excluding GET parameters. ## File Uploads When a user uploads a file or files, you can retrieve their data using the getFiles() method. It returns an array of arrays with data or an array of objects, depending on whether the framework was initiated with an external Request. ## Request Headers The array of all incoming request headers is returned by the getHeaders() method. These are request headers sorted by key (name). You can check for the existence of a header by name using the hasHeader() method. The getHeader() method returns an array of matching headers (values) by name. The getHeaderLine() method also returns header values by name, but as a string in enumeration form. ## $_SERVER Data To retrieve data set by the web server in the $_SERVER variable, you can use the server() method. It returns the value by parameter name. ## Request Protocol Version The getProtocolVersion() method returns the request protocol version, for example '1.1'. ------------------------- # Container The Container in the HLEB2 framework is a collection of so-called services, which can be retrieved from or added to the container. Services are logically self-contained structures with a specific purpose. In the HLEB2 framework, the initialization of services in the container is streamlined without unnecessary abstraction. Services are not initialized by the framework from configuration, as is typically implemented, but rather within a special class App\Bootstrap\BaseContainer, which is accessible for editing by the developer using the framework. (Most often, you'll use the App\Bootstrap\ContainerFactory class, where services are defined as singletons.) All the files for these classes are located in the /app/Bootstrap/ directory of the project. This structure allows a significant number of services to be added to the container without a major impact on performance. ## BaseContainer Class This class represents the container that will be used to retrieve services. If a service needs to be a new instance of the class each time it's requested from the container, it should be specified here within a match() expression. ``` parent::get($id), }; } } ``` Adding a service is similar to adding it in the ContainerFactory class. ## ContainerFactory Class A factory for creating services as singletons, with the ability to override the framework's default services. It's used to add custom services, which are initialized only once per request. For example, we might need to add a RequestIdService that returns a unique ID for the current request. This is a demonstration example of a service; in general, services represent more complex structures. Let's add its creation to the ContainerFactory class: ``` new RequestIdService(), default => null }; return self::$singletons[$id]; } #[\Override] public static function rollback(): void { self::$singletons = []; } } ``` Now, when the RequestIdInterface is requested from the container, it will return an instance of RequestIdService, stored as a singleton. The key for retrieval can be defined not only as an interface but also as the base class RequestIdService, as it will be utilized in DI (Dependency Injection). Despite the fact that the match() expression can contain multiple keys to a value, to avoid duplicating services (and consequently violating the singleton principle), only one should be assigned. Starting from PHP v8.4, you can leverage "lazy objects" support in the container. An object of this kind, when retrieved from the container, won’t be initialized until it’s actually accessed. In the App\Bootstrap\ContainerFactory class, you need to define the service as follows: ``` ... ExampleServiceInterface::class => self::getLazyObject(ExampleService::class), ... ``` ## Creating a Method in the Container To simplify working with the new service keyed by RequestIdInterface, let's add a new method in the container. This will make it easier to find in the container through the IDE. The new method requestId is added to the container class (BaseContainer). Now the class looks like this: ``` parent::get($id), }; } // New method. #[\Override] final public function requestId(): RequestIdInterface { return $this->get(RequestIdInterface::class); } } ``` Important! For this to work, the requestId method must also be added to the App\Bootstrap\ContainerInterface interface. In the example, the service is assigned by interface, allowing the service class in the container to change while maintaining the interface linkage. For your own internal application classes, you can also omit the interface here and specify the class mapping directly. For the framework's standard services, all these actions have already been done; you can retrieve them through the corresponding controller method. The process of creating a new service is detailed in the example of adding a real library. Creating interdependent services is described in the section non-standard container usage. ## rollback() Function of the Container You have probably noticed the rollback() function in the ContainerFactory class. This function is necessary for resetting the states of services during asynchronous use of the framework, for example, when used with RoadRunner. Here is how it works: When the framework completes an asynchronous request, it resets the state of standard services. Then, it calls the rollback() function to execute the code it contains to reset the state of manually added services. Therefore, if the framework is used in asynchronous mode, you can initialize the service state reset (as well as that of any other module) here. ------------------------- # Dependency Injection Dependency Injection (also DI) is a framework mechanism for supplying dependencies to the constructor or other methods of created objects. When the framework creates objects such as controllers, middlewares, commands, and others, dependency injection is already set up when the target method (including the constructor) is called. According to the DI mechanism, if you specify the necessary classes or interfaces in the method's dependencies (arguments), the framework will attempt to find such matches in the container, retrieve them from the container, or create the object itself and substitute it in the required argument. If such a service is not found in the container, an attempt will be made to create an object from a suitable class in the project, and if the latter has dependencies in its constructor, the framework will try to fill them in a similar way. If there are no substitution values for arguments with default values, the default will be used. Otherwise, the framework will return an error indicating that the DI for the specified dependencies could not be successfully used. ## DI Implementation in the Framework When a controller or middleware object is created on the framework side, the constructor's dependencies are resolved first, then those of the called method. Also, when a request is processed by the framework, only one method in the matched controller will be called. In such a case, it doesn't matter where the dependency comes from, whether from the constructor or method, although in some cases, the constructor is more convenient. The following example shows two controller methods with different assignments of $logger from the container via DI. ``` logger; } } ``` Dependencies for middleware are set in a similar manner. In the framework commands and events (Events), this is implemented in a similar way, but only through the constructor: ``` logger; return self::SUCCESS_CODE; } } ``` ## Creating Objects with DI Dependency injection is convenient because during testing, we can create the necessary values for class dependencies. However, when creating an object manually, initializing all its dependencies ourselves would be inconvenient. To automate this process, the framework provides the Hleb\Static\DI class. ``` use Hleb\Reference\LogInterface; use Hleb\Static\DI; // Demo class for insertion. class Insert { } // Class with dependencies. class Example { public function __construct(private readonly LogInterface $logger) { } public function run(Insert $insert): void { echo $this->logger::class; echo ' & '; echo $insert::class; } } $exampleObject = DI::object(Example::class); echo DI::method($exampleObject, 'run'); // Hleb\Reference\LogReference & Insert ``` This section demonstrates how to create an object of a class whose constructor has a dependency, and how to call the desired method of the object where a value also needs to be automatically inserted. The example also shows a dependency that is not from the container (the Insert class), whose object is created and injected into the method. A frequently used variant of DI with Request and Response (in this case obtained from the container): ``` container property to access services. The standard framework classes — controllers, middlewares, commands, events — are already inherited from this class. If a service in the container interface has its own method assigned, the service can be accessed through this method. Example of accessing a demo service in a controller: ``` container->get(RequestIdInterface::class); // variant 2 $requestIdService = $this->container->requestId(); } } ``` The reference to the container is stored in the $this->config property (key 'container' in the array) of the object class inherited from Hleb\Base\Container. When creating the specified object, a different value can be assigned (for example, with a test container) in the 'config' argument. Otherwise, if a specific container is not specified in the 'config' argument or the 'config' argument of the constructor is missing, the container will be created by default. ``` service = $this->container->get(RequestIdInterface::class); } } // Create an object with a framework container. $requestIdService = (new ExampleService())->service; // Create an object with a test container. $config = ['container' => new TestContainer()]; $requestIdService = (new ExampleService($config))->service; ``` Exceptions are the Model classes, where accessing the service similarly will be as follows: ``` get(RequestIdInterface::class); // variant 2 $requestIdService = self::container()->requestId(); return []; } } ``` ## Container Class Access to the service container is also provided by the Hleb\Static\Container class, for example: ``` use App\Bootstrap\Services\RequestIdInterface; use Hleb\Static\Container; // variant 1 $container = Container::getContainer(); $requestIdService = $container->get(RequestIdInterface::class); // variant 2 $requestIdService = Container::get(RequestIdInterface::class); ``` ## Standard Services In the /vendor/phphleb/framework/Static/ folder, there are wrapper classes over the framework's standard services, which can be used in code similarly to the Hleb\Static\Container class, but for individual services. These services can also be accessed using the previously mentioned methods. Due to the existence of different approaches in naming interfaces, accessing standard services from the container can be either with or without the Interface suffix. For example, Hleb\Reference\RequestInterface is equivalent to Hleb\Reference\Interface\Request. ------------------------- # HLEB2 The HLEB2 framework is the next step in the evolution of the HLEB framework. The framework was originally designed to become the fastest PHP framework while still providing a robust toolkit for web development. If you have any questions about using the framework or believe that a particular topic is not sufficiently covered in the documentation, please reach out to our support chat on Telegram: @phphleb ------------------------- # Middleware Middleware is a type of controller, but its primary purpose is not to provide the expected response to the user (although middleware can return error texts), but to perform specific tasks before or after that response is generated. Unlike a controller, this middleware can be assigned not only to a route but also to a group of routes. Both can have multiple different middleware (or even the same ones, if needed). For example, user authorization can be implemented in middleware and applied to a group of routes where it is needed. Before the execution of the controller or any other primary action attached to the route, the current user and their authorization status will be determined. Otherwise, the middleware class will hand over execution to another controller, return an error, or redirect to another route, depending on the implementation. When the middleware() method (options after() or before()) is applied in a route, it takes a data argument. This is another difference from the controller; a data array can be passed to this argument, which will then be available in middleware. The array data is accessible in the method Hleb\Static\Router::data() or via the container. The middleware class must inherit from Hleb\Base\Middleware. ## Return Values Typically, the purpose of the called method of this class is not to return anything, but to validate conditions. However, in some cases, returning a value is allowed. string|int|float - these types will be converted to a string and output as text in their original form. array - the returned array will be converted into a JSON string. After this, further execution is terminated. bool - if false is returned, it is equivalent to stopping further execution. ## Creating Middleware Besides copying the demonstration file DefaultMiddleware.php and modifying it, there is another simple way to create the required class using a console command. ```$ php console --add middleware ExampleMiddleware``` This command will create a new template /app/Middlewares/ExampleMiddleware.php. You can use another suitable name for the class. The HLEB2 framework also allows you to create a custom template by default for this command. ------------------------- # Module The modular approach in software architecture allows you to logically divide a project into large composite fragments (modules). A defining feature of a module is its self-sufficiency; in some sense, it’s a form of dividing a monolithic application into "microservices". The key difference from microservices is that modules must exchange data through predefined contracts, which replace HTTP API (or message brokers), and they also share a common folder for routes, services, and external libraries from the /vendor/ directory. It is recommended to design contracts in a way that would allow extracting a module into a full-fledged microservice if needed. In the HLEB2 framework, a Module is essentially an MVC (Action-Domain-Responder for web) in miniature. The module has its own controller, its own folder for templates, and even its own configuration is permissible, all of which are located within the module’s folder. Its own logic is also assumed (as well as Models), but for this, it is recommended to create a separate structure in the project’s /app/ folder or within the module itself. When using the approach of full autonomy of parts in the project, which is the essence of modular development, you may not use controllers, middleware, or models from /app/ at all, implementing everything within the modules. The role of a module’s controller in the route differs from a regular controller in that the method is named 'module' instead of 'controller', and it contains an additional initial argument with the module’s name. ``` use Modules\Example\Controllers\ExampleModuleController; Route::any('/demo-module')->module('example', ExampleModuleController::class, 'index'); ``` The module’s controller must inherit from Hleb\Base\Module. For the Composer class loader to generate the class map for modules, add the module folder name ("modules/") to the "autoload" > "classmap" section of the /composer.json file. ## Creating a Module A simple way to create the basic structure of a module using a console command: ```$ php console --create module example``` This command will create a new module template in the /modules/example/ directory of the project. You can use another suitable name for the module, consisting of lowercase Latin letters, numbers, dashes, and the '/' symbol (indicating nesting). There is an option to override the original module files used during generation. Structure of the module after creation (if there was no modules folder previously, the console command will create it in the project root): modules - directory for modules example - example module folder config | main.php - module settings controllers | DefaultModuleController.php - module controller views example.php - module template The main.php file can contain settings similar to the /config/main.php file but with values used only in the module, meaning it will "override" them. Initially, the main.php file contains no settings; all settings from /config/main.php are used. Similarly, settings in the /config/database.php can be replaced by creating a file with the same name. Settings of other configuration files always act globally. The module controller is similar to the standard controller of the framework. When using the view() function, the path to the template will point to the module's 'views' folder, as it does for all built-in framework functions for template work. ## Nested Modules There is an option to group modules into collections nested in different subfolders within /modules/. For this, modules are placed one level down, and the module name includes the group name. This creates a second level of module nesting. Let's assume we need to place a module group named 'main-product', which will contain the modules 'first-feature' and 'second-feature'. modules main-product - module group | first-feature - first-feature module folder | config | | main.php | | database.php | controllers | | ModuleGetController.php | | ModulePostController.php | views | template.php | second-feature - second-feature module folder controllers | ModuleController.php middlewares | ModuleMiddleware.php views template.php This is how it will look in the route map: ``` use Modules\MainProduct\{ FirstFeature\Controllers\ModuleGetController, FirstFeature\Controllers\ModulePostController, SecondFeature\Controllers\ModuleController, SecondFeature\Middlewares\ModuleMiddleware, }; Route::get('/demo-group-module/first') ->module('main-product/first-feature', ModuleGetController::class); Route::post('/demo-group-module/first') ->module('main-product/first-feature', ModulePostController::class); Route::any('/demo-group-module/second') ->module('main-product/second-feature', ModuleController::class) ->middleware(ModuleMiddleware::class); ``` In the group named 'first-feature', there is a reassignment of settings, including for databases. The example for 'second-feature' uses global settings, additionally, it has middleware for the controller. It is possible that more controllers may appear there. Similarly, a structure is created for the third level of nesting if it is necessary. ## Folder Name with Modules Initially, the folder with modules is called 'modules'; before creating modules, you can change this name in the settings, for example, to 'products'. This is done in the file /config/system.php - setting 'module.dir.name'. If the change is made with already existing module classes, you need to correct the namespace for modules that are PSR-0 compliant. ## Overriding Settings In a module, two configuration files can be overridden - /config/main.php and /config/database.php. The values of the parameters are overridden recursively by key; otherwise, the parameter has a global value. New parameters that have no global counterpart will be available locally within the module. ## Paths to Templates in Modules When using modules as separate packages, it is not always necessary for the package to include View templates, as styling and result output may be a separate layer in the application structure. Therefore, there can be two options for using templates. "Using" refers to pointers to templates in the function view() as well as in special functions like insertTemplate(). If the module has a folder /views/, template paths will point to it. However, if there is no such folder, the template search will occur in the project's /resources/views/ directory. ------------------------- # Controller The Controller is part of the MVC architecture (Action-Domain-Responder for web), responsible for further managing the handling of a request that has already been identified by the router, but should not contain business logic. In the HLEB2 framework, controllers are regular handler classes bound to a route using the controller() method. This method points to the controller class and its executable method. Upon a match, the framework creates an instance of this class and calls the method. The controller class must inherit from Hleb\Base\Controller. The framework searches for the controller in the /app/Controllers/ folder according to its namespace. Here is the default controller code: ``` 'Main template', 'description' => 'Template description'], 205); ``` If you use this example in a controller, the template /resources/views/template/file.php will be called. In the file, the variables $title and $description will be available with their corresponding values: ``` where(['version' => '[0-9]+', 'page' => '[a-z]+']) ->controller(DefaultController::class, 'resource'); ``` The variables $version and $page can be inserted into the 'resource' controller method. ``` config))->index(); } } ``` No Events assigned to controllers will be applied to this nested controller. ## HTTP Error Classes If a certain condition in the controller code should end with an HTTP error, there are several predefined exception classes for this, such as 'Http404NotFoundException' and 'Http403ForbiddenException'. For example, by specifying the error as 'throw new Hleb\Http404NotFoundException();', the framework will generate the HTTP code and standard 404 error text in the response. ## Incoming Data Validation In the HLEB2 framework, basic validation of dynamic parts of the route address can be declared directly in the route using the where() method. If you need to validate payload data, such as POST request data in JSON format, one option is to use the api-multitool library. By using the trait from this library Phphleb\ApiMultitool\ApiRequestDataManagerTrait, the check() method becomes available and can be used to validate various request data. ## Creating a Controller Besides copying and modifying the demo file DefaultController.php, there is also a simple way to create a controller using a console command. ```$ php console --add controller ExampleController``` This command will create a new controller template at /app/Controllers/ExampleController.php. A different suitable name for the class can be used. The framework also allows creating a custom default template for this command. ------------------------- # Built-in Framework Functions The HLEB2 framework introduces a number of its own functions of various purposes, which reduce code size and accelerate application development, as they are shorthand for common actions. Some built-in framework functions have hl_ at the beginning of their names, and there are also duplicates of functions without this prefix. Therefore, if you forget the name of the desired function, just type hl_ and your IDE should suggest available options. ## Working with Route Data The HLEB2 framework has its own routing system. The following functions are intended to interact with this system. If you practice assigning custom names to routes, they might be useful here. ### route_name() This function returns the name of the current route or null if it is not assigned. Despite this very useful information, it may only be needed in conjunction with another function that works with addresses. ### url() The url() function returns a relative URL by route name with substitutions for necessary parameters. Function arguments: routeName - the name of the route for which the address is needed. replacements - an array of substitution parts if the route is dynamic. endPart - a boolean value determining if the last part of the address is required, if it is optional in the route. method - for which HTTP method the address is needed. Some methods may not fit the route, and in such cases, it will return an error. By default, 'get'. ``` // For the Route::get('/{lang}/adminpanel/{user_id}?/', '...')->name('user.profile'); // will return `/en/adminpanel/1` $relativeUrl = url('user.profile', ['user_id' => 1, 'lang' => 'en'], true); ``` Consistent use of internal URLs by their route names allows the entire application to change static parts of addresses in routes without making changes to the rest of the code. ### address() The address() function returns the full URL based on the route name with the substitution of the current domain. Since the domain is only the current one, use concatenation with url() for a different domain. The set of parameters is similar to the url() function. This function allows you to generate correct links to the project pages. However, it is better to use relative URLs for in-app navigation. ## Retrieving Current HTTP Request Data ### request_uri() Returns an object with information from the relative URL of the current request. The basis for the object returned by the request_uri() function is the UriInterface (method getUri()) from PSR-7, which allows you to obtain the following request data: request_uri()->getHost() - The domain name of the current request, such as 'mob.example.com'. May include the port depending on its presence in the request. request_uri()->getPath() - The path from the address after the host, such as '/ru/example/page' or '/ru/example/page/'. request_uri()->getQuery() - Request parameters, such as '?param1=value1¶m2=value2'. request_uri()->getPort() - The request port. request_uri()->getScheme() - The HTTP scheme of the request, either 'http' or 'https'. request_uri()->getIp() - The IP address of the request. In these examples with request_uri(), two styles of naming conventions are used within a single expression (snake_case and camelCase), which is because most functions of the HLEB2 framework are in snake_case similar to PHP functions, while the methods of the returned object are in camelCase, according to PSR-12. If you are accustomed to a different function format, wrap the current ones in the necessary style. ### request_host() The request_host() function allows you to obtain the current host, possibly along with the port. For example, example.com or example.com:8080 if it is specified in the request URL. This is useful for generating correct links to project pages. However, for internal navigation within the application, it is better to use relative URLs. ### request_path() The request_path() function returns the current relative request path from the URL without GET parameters. For example, /ru/example/page or /ru/example/page/. ### request_address() The request_address() function returns the complete current request address from the URL without GET parameters. For example, `https://example.com/ru/example/page` or `https://example.com/ru/example/page/`. ## Redirect Redirecting to other pages of the application or other URLs. ### hl_redirect() The hl_redirect() function performs a redirect using a specified header and exits the script. Thus, if content has already been output before this function is applied, headers will not be sent, and a warning will be displayed instead of redirecting. It operates based on the 'Location' header. When used in framework-based classes, such as in controllers, it's more appropriate to use a similar method Redirect::to(). ``` hl_redirect('/target-page', status: 302); // or use Hleb\Static\Redirect; Redirect::to('/target-page', status: 302); ``` ## Fetching Framework Configuration Data Configuration data from the framework or custom settings can be used in the application code. The following functions allow these data to be retrieved anywhere in the project code. Project parameters and settings should be collected in its configuration files, and they can be used not only for the application's needs but also for configuring connected third-party libraries. ### config() Each configuration parameter is distributed by groups according to the main filename. These might be standard groups ('common', 'database', 'main', 'system') or additional ones created for the project. The group's name is passed as the first argument to the config() function. The parameter's name itself is the second argument. The function returns this parameter's value. For example: ``` $timezone = config('common', 'timezone'); $lang = config('main', 'default.lang'); ``` ### get_config_or_fail() As the name get_config_or_fail() suggests, this function returns the configuration parameter's value or throws an error if the parameter is not found or is null. The arguments are similar to the config() function. ### setting() Since it’s recommended to add custom values to the 'main' group, a separate function setting() is provided for frequent use of this configuration. Its application is similar to the config() function with the first argument 'main'. ### hl_db_config() The special function hl_db_config() serves as an equivalent of the config() function with the first argument 'database'. ### hl_db_connection() The hl_db_connection() function is used to retrieve data from any existing connection in the 'db.settings.list' of the 'database' settings group. It returns an array of settings or throws an error if they are not found. ### hl_db_active_connection() The hl_db_active_connection() function, like the hl_db_connection() function, returns a settings array but specifically for the connection marked as "active" in the 'base.db.type' parameter. These functions for accessing database parameters are essential when adding third-party libraries that require a connection configuration to a specific database. ## Debugging Functions The framework includes several functions for quick code debugging. They complement and extend the PHP var_dump() function in various ways. Depending on the situation, a suitable one can be chosen. ### print_r2() This function has been retained from the first version of the framework. It is used to display data in a readable format for the debug panel. Thus, when DEBUG mode is off, debug data passed to the function won’t be displayed, as the debug panel is disabled. This is convenient during development, as you don’t need to worry about its visibility outside of debug mode. An optional second argument to the print_r2() function allows you to add a description to the displayed data for easy identification in the panel. Example: ``` use Hleb\Static\Request; $debugData = Request::param('test')->toString(); print_r2($debugData, name: "Printing the value of `test` from a dynamic route."); ``` ### var_dump2() The var_dump2() function is a complete analog of var_dump(), but it outputs more structured information. If the output is intended for a browser, the original line breaks and indents are preserved. ### dump() The dump() function is another wrapper around var_dump(), but it converts the result to HTML code, which appears cleaner and more informative than the standard output. ### dd() Similar to dump(), it outputs HTML code but also terminates the script after that. The dd() function is easy to locate on the application page, as its output will be at the very bottom. ## File System Operations The HLEB2 framework organizes file and directory operations based on relative paths from the project root. Such paths begin with '@/' to denote the root directory. This approach is used across many standard services in the framework and is recommended for consistent usage. The following functions serve as wrappers around equivalent PHP functions, adding the capability to use the '@' prefix. ### hl_file_exists() The hl_file_exists() function is analogous to the PHP function file_exists(), but it also accepts special paths starting with '@'. ### hl_file_get_contents() The hl_file_get_contents() function is similar to the PHP function file_get_contents(), but it allows for special paths starting with '@'. ### hl_file_put_contents() The hl_file_put_contents() function is equivalent to the PHP function file_put_contents() and also accepts paths starting with '@'. ### hl_is_dir() The hl_is_dir() function is similar to the PHP function is_dir(), but it can also accept paths with a starting '@'. ## CSRF Protection Detailed documentation on the implementation of protection against CSRF attacks in the framework. ### csrf_token() The csrf_token() function returns a secure token for protection against CSRF attacks. ### csrf_field() The csrf_field() function returns HTML content to insert into a form for CSRF attack protection. ## Templates Although the framework allows integration with the Twig templating engine, it also provides a straightforward implementation of built-in templates that do not use custom syntax different from standard PHP or HTML. Learn more about the framework's standard templates. ### insertTemplate() With the insertTemplate() function, the generated template is inserted at the location in the file where this function is called. Key parameters: viewPath - a specific path to the template file. This format is similar to the path types used in the view() function. extractParams - an associative array of values that will be converted into template variables. ### template() The template() function returns the framework template's text representation. This is useful if you need to pass the content further, for example, if it is an email template. Parameters are similar to those in the insertTemplate() function. ### insertCacheTemplate() The insertCacheTemplate() function is similar to insertTemplate() except that the template is cached for the specified number of seconds in the sec parameter. Other arguments are identical to those in the insertTemplate() function. ## Environment Variable Retrieval The HLEB2 framework includes several functions for retrieving environment variables in a convenient format (type). ### Functions get_env(), env(), env_bool(), env_array(), and env_int() The get_env() function retrieves the value of an environment variable and determines its type. Although it is a universal function, it's recommended to use specific functions from the list below for type casting. The env() function returns a string, env_bool() converts the value into a boolean, env_array() parses it into an array, and env_int() converts it into an integer. Additionally, each function allows for a second parameter to specify a default value: ``` $logEnabled = env_bool('LOG_ENABLED', default: true); $logLevel = env('APP_LOG_LEVEL', default: 'info'); $uploadLimitMb = env_int('UPLOAD_LIMIT_MB', default: 10); $redisConnection = env_array('REDIS_CLUSTER_CONFIG', default: []); ``` ## Additional Various specialized functions. ### is_empty() Checks for emptiness in a more selective way than the PHP function empty(). The is_empty function will return false only in four scenarios: an empty string, null, false or an empty array. Passing an undeclared variable will result in an error; therefore, to mimic the original function, you can suppress this error by adding '@' before the function like this: ``` unset($var); if (@is_empty($var) || @is_empty($var[1])) { // Code if the variable is empty. } ``` While using error suppression is poor practice, the code within the is_empty() function does not imply the occurrence of other errors. ### logger() The function for logging logger() returns an object with methods for logging data across various levels. ``` logger()->info('This message will be sent to the log.'); ``` ### once() The once() function allows code to be executed only once for a single request, and on subsequent calls, it returns the previous result. The result of execution is stored in memory for the entire duration of the request. In this scenario, the anonymous function passed to once() will execute on the first call to once: ``` $value = once(function () { // An example of a resource-intensive operation. return ExampleStorage::getData(); }); ``` ### param() Returns an object containing dynamic request data by parameter name with the option to select the value format. For example, if the dynamic route specified the parameter /{test}/, and the request was /example/, then param('test')->value will return 'example'. param('test')->value;- directly retrieves the value. param('test')->value(); - directly retrieves the value. param('test')->asInt(); - returns the value converted to an integer, or null if absent. param('test')->asInt($default); - returns the value converted to an integer, and $default is returned if absent. If the last part of the route is an optional variable value, it will be null. Caution is advised with user data obtained as direct values. ## Framework Function Testing In most cases, the framework's standard functions are wrappers around corresponding services, so testing them is similar to testing the service. ------------------------- # Introduction HLEB2 is the second version of the HLEB framework, completely revamped and improved. Supports PHP version 8.2 and above. The initial version 2.0.0 of the framework was released in February 2024. The new version has introduced support for asynchronous execution, allowing the framework to be used with technologies such as RoadRunner and Swoole. Significant focus has been placed on performance and maintainability, implementing compatibility with PSR, adding a service container along with Dependency Injection, and much more. It adheres to the recommendations of PSR-1, PSR-2, PSR-3, PSR-4, PSR-7, PSR-11, PSR-12, and PSR-16 without mandatory implementation in development. If you want to study the framework using an AI model, a unified text version of this guide in Markdown format might be useful to you. For example, the deepwiki service allows you to ask questions about code based on this information. ## Purpose This framework can serve as a foundation for small projects, such as: a separate admin panel, microservice, chatbot, experimental pet project, console processor; as well as medium-sized websites, and can also lay the groundwork for developing your own framework with extended capabilities. In the latter case, it can also be used for large enterprise websites. HLEB2 is positioned as a simple and fast framework that efficiently performs its job. A key feature of the HLEB framework (and also HLEB2) is a complete abandonment of third-party libraries in the basic setup; at the same time, there is the possibility to integrate third-party libraries if necessary. Thus, further actions are not predetermined by dependencies, ensuring necessary flexibility. To use the framework, at minimum, one must have basic programming knowledge of the PHP language. The framework is a multi-purpose tool, and every tool can be used for unintended purposes, so it is assumed that the application developer understands what they are doing and can choose the appropriate approach for their specific project. The framework's code is thoroughly tested with unit tests. ## Projects Based on the Framework Among the applications known to the author based on HLEB2 is the discussion (and Q&A) engine LibArea. Project on GitHub: github.com/LibArea/libarea It is assumed that projects based on LibArea also operate on the HLEB2 framework. ## How to Use the Documentation The detailed guide to the framework consists of various sections. Some of the information is accompanied by code examples, such as (routing declaration): ``` Route::get('/', view('default'))->name('homepage'); ``` The list of documentation sections is located in the site's menu. For beginners, it's recommended to start exploring the framework with topics on installation, routing, and configuration editing. Information that requires special attention will be highlighted in such a block. A warning that should not be ignored will be highlighted in this kind of block. ## Local Installation of Documentation This documentation can be installed and used offline. The code is located in an open repository, and after local installation, you simply need to keep track of updates. ------------------------- # Events The HLEB2 framework has several predefined general events, each assigned to a specific action type. All event classes are located in the /app/Bootstrap/Events/ folder and are open to modifications. Technically, they replace the configuration, removing unnecessary "magic" from the project. Since these classes are tied to global events, it is recommended to segregate code depending on private implementations into separate classes. Unoptimized code within Events can lead to reduced overall project performance. ## ControllerEvent The before() method of this class is executed before each controller call from the framework. It allows you to determine which class and method are involved and, if necessary, alter the arguments given as a named array, returning them to the invoked controller method. For instance, if an incoming Request validation by a third-party library is used, this check can be implemented through the ControllerEvent event. If present, the after() method allows you to override the controller's response and is executed immediately after the controller. The method receives this result in the 'result' argument by reference, allowing you to change the returned data for a specific class and method of the controller. Globally, this might involve transforming a returned array not into JSON as set by default, but into another format like XML. The following example demonstrates attaching an additional action before calling a specific class and method of the controller: ``` beforeIndex($arguments); // ... // default: } return $arguments; } public function after(string $class, string $method, mixed &$result): void { // ... // } } ``` ## MiddlewareEvent The before() method of this middleware class is executed before each middleware call from the framework. The method's arguments allow you to determine which class and method are involved, and whether this middleware is executed after the main action. If necessary, there are options to modify the target middleware method's arguments, altering them, and returning them from the current method. In such a case, it is necessary to specify the condition to terminate the script execution after the result is output, by returning false from the after() method. The order of middlewares execution can be changed in routes, and this must be accounted for when assigning events to them, if necessary replacing elements of the Event depending on the execution order with corresponding separate middlewares. ## ModuleEvent Since modules exist in isolation, each module's controllers have their own Event. The before() method of the ModuleEvent class is executed before each controller call of any module in the framework. Unlike ControllerEvent, there is an additional argument $module to determine the module name. Similar to the controller event, this Event can also have an after() method. ## PageEvent This is another event similar to ControllerEvent, tied to calls of special 'page controllers'. Such pages are used in the framework's registration library for the admin panel and also on this documentation site. ## KernelEvent The KernelEvent event is not necessarily present in the folder with other Events, but if a class file with this name is created, it will be utilized by the framework. Its unique feature is intercepting all web requests at the highest level and creating a global action for them. For example, this could be logging user requests (not initially included in the framework): ``` $this->request->getAddress() . $this->request->getUri()->getQuery(), 'method' => $this->request->getMethod(), // Other parameters required in the log. ]; $this->log->info('Request log for the site, url: {url} method: {method}', $data); return true; } } ``` ## TaskEvent The execution occurs before each framework command launch, excluding those built into it by default. It also allows determining the called class and the source of the call (from the code or from the console). TaskEvent receives and returns the final data for the arguments of the final method, thus allowing the connection of a third-party library here. For example, this could be a standard console handler from Symfony. The after() method for this event differs in that it has access to the data set in the task as setResult(). This data is passed by reference to the 'result' argument and can be modified. If necessary, you can similarly change the returned response status using the statusCode() method. A demonstration example showing one of the ways to organize response (with a single common interface) to the execution of various tasks: ``` action = new FirstTaskEvent($method); break; case SecondTask::class: $this->action = new SecondTaskEvent($method); break; // ... // default: } return $this->action ? $this->action->getBeforeAction($args) : $args; } public function after(string $class, string $method, mixed &$result): void { $this->action and $result = $this->action->updateAfterAction($result); } public function statusCode(string $class, string $method, int $code): int { return $this->action ? $this->action->getCode($code) : $code; } } ``` This principle can be applied not only to task events but to other Events as well. The switch operator is chosen for the Event due to its ability to match one result to multiple case blocks. ## Extended Conditions Associated actions can also be assigned based on other conditions, for example, by a general group in the namespace: ``` if (str_starts_with($class, 'App\\Controllers\\Api\\')) { // ... // } ``` Additionally, event classes are inherited from Hleb\Base\Container, allowing them to use services from the container. These services can also be obtained in the event class constructors through Dependency Injection. The possibilities of using them are not limited, provided the code remains readable and optimized. Here's how you can set a condition based on the HTTP request method for a specific class and method: ``` if ([$class, $method] === [MainController::class, 'index'] && $this->request()->isMethod('GET')) { // ... // } ``` -------------------------