Welcome to MageWork! A standalone, lightweight PHP framework to build websites.
Please refer to the readable documentation at the following official website: MageWork documentation
Installation
Requirements
PHP
A PHP version 8.4 or higher is required.
PHP Extensions:
- ext-fileinfo (to read an asset static file)
- ext-gd (to generate a Captcha)
- ext-openssl (to encrypt and decrypt data)
sudo apt install php8.4-gd php8.4-common
Server
MageWork is compatible with any web server.
Environment variables
You just need to set 2 environment variables:
- MW_ENVIRONMENT: The environment name (local, prod, staging...). This variable is used to read the configuration file in the
etcdirectory:etc/config.{MW_ENVIRONMENT}.php. Default value if missing isdefault. - MW_DEVELOPER_MODE: Display PHP error. Always set
0in production. Default value if missing is0.
Root directory
Configure the web server to serve the pub directory.
Examples
Built-in PHP server
Unix
From the MageWork root folder:
sudo MW_ENVIRONMENT=local MW_DEVELOPER_MODE=1 php -S localhost.magework:80 -t pub
Windows
Add an executable bat file to the MageWork root directory:
:: server.bat @echo off set MW_ENVIRONMENT=local set MW_DEVELOPER_MODE=1 php -S localhost.magework:80 -t pub pause
Apache
<VirtualHost *:80>
ServerName localhost.magework
DocumentRoot /var/www/magework/pub
SetEnv MW_DEVELOPER_MODE 1
SetEnv MW_ENVIRONMENT local
<Directory /var/www/magework/pub>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/magework.log
</VirtualHost>
Nginx
server {
listen 80;
listen [::]:80;
root /var/www/magework/pub;
server_name localhost.magework;
index index.php;
charset utf-8;
autoindex off;
location ~ /\.ht {
deny all;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
fastcgi_param MW_DEVELOPER_MODE 1;
fastcgi_param MW_ENVIRONMENT local;
}
}
Caddy
localhost.magework {
root * /var/www/magework/pub
try_files {path} {path}/ /index.php?{query}
php_fastcgi unix//var/run/php/php8.4-fpm.sock {
env MW_DEVELOPER_MODE 1
env MW_ENVIRONMENT local
}
file_server
}
CLI
You can display a page directly in the console, for debugging or CI/CD testing.
php pub/index.php {environment} {host} {path} {developer_mode}
Example:
php pub/index.php local localhost.magework /documentation/installation.html 1
Configuration
Configuration file
Open the configuration file for your current environment: etc/config.{MW_ENVIRONMENT}.php
Package
Access to a package is done in the configuration file. The package host must match the server host for MageWork to load the corresponding package.
You can specify multiple hosts to access the same package, or develop multiple packages with their own host.
<?php
// etc/config.default.php
$config = [
'packages' => [
'localhost.magework' => [ // HTTP Host
'Magework' => '', // Package name (directory) and URL prefix (empty = no prefix)
],
],
/* ... */
];
See Add a new package page for more information about packages.
Object data
In the configuration file, you can define all the environment information that needs to be passed to your own objects.
For example, to pass a variable to page-type objects in a package:
<?php
// etc/config.default.php
$config = [
/* ... */
'Acme' => [ // Package name
Core_Page::TYPE => [ // Object type
'default' => [ // All pages (default value)
'email' => 'default@example.com',
],
'/contact.html' => [ // Specific page
'email' => 'contact@example.com', // Override the default value
],
],
],
/* ... */
];
Database
To interact with a database, you need to configure your database connection:
<?php
// etc/config.default.php
$config = [
/* ... */
'default' => [ // "default" is the configuration for all packages. Use the package name for a specific configuration.
Core_Model::TYPE => [
'database' => [
'db_host' => '',
'db_username' => '',
'db_password' => '',
'db_database' => '',
'db_charset' => 'utf8mb4',
'lc_time_names' => 'en_US', // Language used to display day and month names and abbreviations
'time_zone' => '+00:00', // Affects display and storage of time values that are zone-sensitive
],
],
],
/* ... */
];
This provides the connection information to the Core_Model_Database class, for all packages.
Session cookie
If you need to use a session, the following configurations are available:
<?php
// etc/config.default.php
$config = [
/* ... */
'app' => [
'session_lifetime' => 3600, // Cookie lifetime in seconds
'cookie_same_site' => 'Lax', // None, Lax, Strict
'cookie_http_only' => true,
],
/* ... */
];
Secured protocol port
By default, the port is 443. If you're using a certificate on a different port, a configuration allows you to define the port.
This enables the application to determine if the context is secure: App::isSsl().
<?php
// etc/config.default.php
$config = [
/* ... */
'app' => [
'secured_port' => 443,
],
/* ... */
];
Forms
To manage forms and send emails, you can configure the contact settings.
<?php
// etc/config.default.php
$config = [
/* ... */
'default' => [ // "default" is the configuration for all packages. Use the package name for a specific configuration.
Core_Model::TYPE => [
'form' => [
'_mail_from_name' => 'MageWork',
'_mail_from_email' => 'hello@example.com',
'_mail_enabled' => true,
],
],
],
/* ... */
];
This provides the contact information to the Core_Model_Form class.
Add a new package
Configuration
Open the configuration file for your current environment: etc/config.{MW_ENVIRONMENT}.php
<?php
$config = [
'packages' => [
'www.example.com' => [ // HTTP Host
'Example' => '', // Package name (directory) and URL prefix (empty = no prefix)
'Admin' => 'manager', // "/manager" URL path will load the "Admin" package
],
'blog.example.com' => [ // HTTP Host
'Blog' => '', // Package name (directory) and URL prefix (empty = no prefix)
],
],
/* ... */
];
In this example, we use 3 packages in the same application:
- www.example.com
- www.example.com/manager
- blog.example.com
If you have defined local for the MW_ENVIRONMENT variable, open: etc/config.local.php.
Add the host for your local, the package name, and the URL prefix:
<?php
$config = [
'packages' => [
'localhost.acme' => [ // HTTP Host
'Acme' => '', // Package name (directory) and URL prefix (empty = no prefix)
],
],
/* ... */
];
Perform the operation again for each of your environments, and change the host. For example: etc/config.prod.php
<?php
$config = [
'packages' => [
'www.example.com' => [
'Acme' => '',
],
],
/* ... */
];
Tree structure
Next, create the package directory structure:
- packages > Acme > Page > Index.php
- packages > Acme > etc > pages.php
- packages > Acme > template > page.phtml
- packages > Acme > template > content > index.phtml
- packages > Acme > template > content > error.phtml
- pub > assets > Acme > css > style.css
- pub > assets > Acme > js > app.js
- pub > assets > Acme > media > logo.png
packages > Acme > etc > pages.php
<?php
$config = [
Core_Page::TYPE => [
'default' => [ // Data shared for all pages
'template' => 'page', // packages/Acme/template/page.phtml
'language' => 'en',
],
'404' => [ // Identifier loader when requested page is not found
'_http_code' => 404,
'content' => 'content/error', // packages/Acme/template/content/error.phtml
'meta_title' => 'Not Found',
'meta_description' => 'This page doesn\'t exist',
],
'/' => [ // Homepage
'class' => Acme_Page_Index::class,
'content' => 'content/index', // packages/Acme/template/content/index.phtml
'meta_title' => 'Home',
'meta_description' => 'Homepage',
],
],
];
packages > Acme > Page > Index.php
<?php
declare(strict_types=1);
class Acme_Page_Index extends Core_Page
{
public function getFoo(): string
{
return 'Bar';
}
}
The execute method is called before template rendering. It allows you to implement the code logic and inject data into the template.
packages > Acme > template > page.phtml
<!DOCTYPE html>
<html lang="<?= App::escapeHtmlAttr($this->getLanguage()) ?>">
<head>
<title><?= App::escapeHtml($this->getMetaTitle()) ?></title>
<?php if ($this->getMetaDescription()): ?>
<meta name="description" content="<?= App::escapeHtmlAttr($this->getMetaDescription()) ?>" />
<?php endif; ?>
<link rel="stylesheet" href="<?= $this->getAssetUrl('css/style.css') ?>" type="text/css" />
<script type="text/javascript" src="<?= $this->getAssetUrl('js/app.js') ?>"></script>
</head>
<body>
<img src="<?= $this->getAssetUrl('media/logo.png') ?>" alt="Acme" />
<?= $this->include($this->getContent()) ?>
</body>
</html>
packages > Acme > template > content > index.phtml
<?= App::escapeHtml($this->getFoo()) ?>
Access to http://localhost.acme in your browser!
CLI page debug
You can display a page directly in the console, for debugging or CI/CD testing.
php pub/index.php {environment} {host} {path} {developer_mode}
Example:
php pub/index.php local localhost.acme /
Add a new page
Create the page
Open the packages/Acme/etc/pages.php file, and add a new page:
<?php
$config = [
Core_Page::TYPE => [
/* ... */
'/contact.html' => [
'class' => Acme_Page_Contact::class, // Optional
'content' => 'content/contact',
'meta_title' => 'Contact',
'meta_description' => 'Contact our team',
'telephone' => '+33 610506070',
],
/* ... */
],
];
Warning:
- For a route with an extension, the first slash is required:
/slug.html - For a route without an extension, the first and last slashes are required:
/slug/
Create a new class: packages/Acme/Page/Contact.php
<?php
declare(strict_types=1);
class Acme_Page_Contact extends Core_Page
{
public function execute(): void
{
$this->setAddress("459 Walker Cape, Powellchester, OL16 3NA");
}
public function getEmail(): string
{
return 'john.doe@example.com';
}
}
Finally, create the template file: packages/Acme/template/content/contact.phtml
<h2>Contact us!</h2> <p>Telephone: <?= App::escapeHtml($this->getTelephone()) ?></p> <p>Address: <?= App::escapeHtml($this->getAddress()) ?></p> <p>Email: <?= App::escapeHtml($this->getEmail()) ?></p>
Access to /contact.html in your browser!
System Options
| Option | Description | Type |
|---|---|---|
| class | The page class with custom logic and methods | string |
| _http_code | HTTP response status code | integer |
| _headers | Custom headers | array |
HTTP response status code
You can force the page HTTP response code with the _http_code parameter:
<?php
$config = [
Core_Page::TYPE => [
/* ... */
'/teapot.html' => [
'_http_code' => 418,
'class' => Acme_Page_Teapot::class,
'content' => 'content/teapot',
'meta_title' => 'Teapot',
],
/* ... */
],
];
Or in the page class:
<?php
declare(strict_types=1);
class Acme_Page_Teapot extends Core_Page
{
public function execute(): void
{
$this->setData('_http_code', 418);
}
}
Custom headers
The _headers parameter allow you to send custom headers:
<?php
$config = [
Core_Page::TYPE => [
/* ... */
'default' => [
'_headers' => [
'Content-Security-Policy' => 'default-src \'self\'',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'X-UA-Compatible' => 'IE=Edge',
'X-Content-Type-Options' => 'nosniff',
],
],
/* ... */
],
];
Or in the page class:
<?php
declare(strict_types=1);
class Acme_Page_Teapot extends Core_Page
{
public function execute(): void
{
$this->setData(
'_headers',
array_merge(
$this->getData('_headers') ?: [],
['X-MageWork' => 1]
)
);
}
}
Redirection
<?php
$config = [
Core_Page::TYPE => [
/* ... */
'/old-page.html' => [
'_http_code' => 301,
'_headers' => [
'location' => '/new-page.html',
],
],
/* ... */
],
];
Serve any type of file
Page
In the page configuration file, add a new page with the class name, and the Content-Type if needed:
<?php
// packages/Acme/etc/page.php
$config = [
Core_Page::TYPE => [
/* ... */
'/api/customers/' => [
'class' => Acme_Page_Api_Customer::class,
'_headers' => [
'Content-Type' => 'application/json',
],
],
/* ... */
],
];
In the class, cancel any potential template, then render the desired content:
<?php
// packages/Acme/Page/Api/Customer.php
declare(strict_types=1);
class Acme_Page_Api_Customer extends Core_Page
{
public function execute(): void
{
$this->setTemplate(null);
}
public function render(): string
{
return json_encode(
[
'customers' => [
[
'identifier' => 1,
'name' => 'John Doe',
],
],
]
);
}
}
Examples
sitemap.xml
<?php
// packages/Acme/etc/page.php
$config = [
Core_Page::TYPE => [
/* ... */
'/sitemap.xml' => [
'class' => Acme_Page_Sitemap::class,
'_headers' => [
'Content-Type' => 'application/xml',
],
],
/* ... */
],
];
<?php
// packages/Acme/Page/Sitemap.php
declare(strict_types=1);
class Acme_Page_Sitemap extends Core_Page
{
public function execute(): void
{
$this->setTemplate(null);
}
public function render(): string
{
$pages = App::db()->getAll('pages');
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ($pages as $page) {
$xml .= '<url><loc>' . $this->getUrl($page['slug']) . '</loc></url>' . "\n";
}
$xml .= '</urlset>';
return $xml;
}
}
robots.txt
<?php
// packages/Acme/etc/page.php
$config = [
Core_Page::TYPE => [
/* ... */
'/robots.txt' => [
'class' => Acme_Page_Robots::class,
'_headers' => [
'Content-Type' => 'text/plain',
],
],
/* ... */
],
];
<?php
// packages/Acme/Page/Robots.php
declare(strict_types=1);
class Acme_Page_Robots extends Core_Page
{
public function execute(): void
{
$this->setTemplate(null);
}
public function render(): string
{
$robots = 'User-agent: *' . "\n";
if (App::getEnvironment() === 'default' {
$robots .= 'Allow: /' . "\n";
} else {
$robots .= 'Disallow: /' . "\n";
}
return $robots;
}
}
Rewrite
To rewrite a route, add a model rewrite class in your package. If the package is named Acme, the class will be Acme_Model_Rewrite.
Create the packages/Acme/Model/Rewrite.php file.
<?php
declare(strict_types=1);
class Acme_Model_Rewrite extends Core_Model
{
public function execute(): void
{
$route = App::getRoute(); // The current route without query parameters and the final /
// /my-page.html = /my-page.html
// /my-page/ = /my-page
// /my-page/?q=1 = /my-page
// /my-page?q=1 = /my-page
}
}
Then, write a regular expression to check if the route matches what you want.
If the route matches, you can update the current route and add the parameters.
<?php
declare(strict_types=1);
class Acme_Model_Rewrite extends Core_Model
{
public function execute(): void
{
$route = App::getRoute();
if (preg_match('/^\/customer\/(?P<id>[0-9]*)$/', $route, $matches)) { // /customer/{id}
$this->setData('route', '/customer/');
$_GET['id'] = $matches['id'];
}
}
}
In this example, the route /customer/?id=10 can be rewritten in /customer/10.
Add a new block
Open the packages/Acme/etc/blocks.php file, and add a new block:
<?php
$config = [
Core_Block::TYPE => [
/* ... */
'banner' => [ // The block identifier
'class' => Acme_Block_Banner::class, // Optional
'caption' => 'Welcome to MageWork!',
],
/* ... */
],
];
Create a new class: packages/Acme/Block/Banner.php
<?php
declare(strict_types=1);
class Acme_Block_Banner extends Core_Block
{
public function execute(): void
{
$this->setImage('media/banner.png');
}
public function canShow(): bool
{
return true;
}
}
Open any template file, example: packages/Acme/template/page.phtml
<?= $this->getBlock('block/banner', ['alt' => 'My Banner'], 'banner') ?>
Finally, create the template file: packages/Acme/template/block/banner.phtml
<?php /** @var Acme_Block_Banner $this */ ?>
<?php if ($this->canShow()): ?>
<figure>
<img src="<?= $this->getAssetUrl($this->getImage()) ?>" alt="<?= App::escapeHtmlAttr($this->getAlt()) ?>" />
<figcaption><?= App::escapeHtml($this->getCaption()) ?></figcaption>
</figure>
<?php endif; ?>
Data assignment
You can simply pass data to any object or template with the configuration file: packages/Acme/etc/config.php
<?php
$config = [
Core_Block::TYPE => [
'default' => [
'scope' => 'I\'m available in all blocks of the "Acme" package',
],
'banner' => [
'scope' => 'I\'m available in the block "banner" of the "Acme" package',
],
],
];
The data will be available in class or template file with the getData method or the magic getter:
<?php
$this->getData('scope');
// or
$this->getScope();
Use global configuration file to set data to packages and objects according the environment: etc/config.{MW_ENVIRONMENT}.php
<?php
$config = [
'default' => [ // All packages
'default' => [
'default' => [
'scope' => 'I\'m available everywhere in all packages for the current environment',
],
],
Core_Page::TYPE => [
'default' => [
'scope' => 'I\'m available in all pages of all packages for the current environment',
],
],
Core_Block::TYPE => [
'banner' => [
'scope' => 'I\'m available in the block "banner" of all packages for the current environment',
],
],
],
];
Overload hierarchy:
- default.default.default
- default.default.{identifier}
- default.{type}.default
- default.{type}.{identifier}
- {package}.default.default
- {package}.default.{identifier}
- {package}.{type}.default
- {package}.{type}.{identifier}
Objects and class fallback
Custom classes
You can use a custom class for any types, by specifying the wanted class name.
<?php
// packages/Acme/etc/config.php
$config = [
Core_Page::TYPE => [ // Type
'contact' => [ // Identifier
'class' => Acme_Page_Contact::class,
],
],
Core_Block::TYPE => [ // Type
'banner' => [ // Identifier
'class' => Acme_Block_Banner::class,
],
],
Core_Model::TYPE => [ // Type
'customer' => [ // Identifier
'class' => Acme_Model_Customer::class,
],
],
];
To create an instance of an object, you need to use the App::getSingleton or App::getObject methods:
App::getSingleton({identifier}, {type}, {package}) (Create a single instance)
App::getObject({identifier}, {type}, {package}) (Create a new instance)
<?php
/** @var Acme_Model_Customer $model */
$model = App::getSingleton('customer', Core_Model::TYPE);
// packages/Acme/Model/Customer.php
/** @var Admin_Model_Customer $model */
$model = App::getSingleton('customer', Core_Model::TYPE, 'admin');
// packages/Admin/Model/Customer.php
Class fallback priorities
If the class is missing, the system will automatically attempt to load a class based on the following priorities:
- {package}_{type}_{identifier}
- Core_{type}_{identifier}
- {package}_{type}
- Core_{type}
- DataObject
Overrides
To override default core objects like Core_Block or Core_Page, add the class to the root of your package:
packages > Acme > Page.php
<?php
declare(strict_types=1);
class Acme_Page extends Core_Page
{
public function myCustomMethod(): string
{
return 'Hello World!';
}
}
Your class methods will now be available for all pages.
In the same way for the blocks:
packages > Acme > Block.php
<?php
declare(strict_types=1);
class Acme_Page extends Core_Block
{
public function myCustomMethod(): string
{
return 'Hello World!';
}
}
Your class methods will now be available for all blocks.
Fallback examples
For a block type, with banner identifier, in the Acme package.
App::getSingleton('banner', Core_Block::TYPE)
The system will try to load classes in this order until it is found:
- Acme_Block_Banner
- Core_Block_Banner
- Acme_Block
- Core_Block
- DataObject
For a model type, with customer identifier, in the Acme package.
App::getSingleton('customer', Core_Model::TYPE)
The system will try to load classes in this order until it is found:
- Acme_Model_Customer
- Core_Model_Customer
- Acme_Customer
- Core_Customer
- DataObject
Database
Insert
<?php
$id = App::db()
->insert('customers', ['email' => 'john@example.com', 'firstname' => 'John', 'lastname' => 'Doe']);
// INSERT INTO customers (email,firstname,lastname) VALUES ('john@example.com','John','Doe')
<?php
$id = App::db()
->insert(
'customers',
['email' => 'john@example.com', 'attempts' => 1],
['attempts = attempts + 1']
);
// INSERT INTO customers (email) VALUES ('john@example.com') ON DUPLICATE KEY UPDATE attempts = attempts + 1
Update
<?php
$count = App::db()
->where(['email =' => 'john@example.com'])
->update('customers', ['firstname' => 'Jane', 'lastname' => null]);
// UPDATE customers SET firstname = 'Jane', lastname = NULL WHERE (email = 'john@example.com')
<?php
$count = App::db()
->where(['email =' => 'john@example.com'])
->update('customers', ['failures_num = failures_num + 1', 'lastname = upper(lastname)']);
// UPDATE customers SET failures_num = failures_num + 1, lastname = upper(lastname) WHERE (email = 'john@example.com')
Delete
<?php
App::db()->where(['email =' => 'john@example.com'])->delete('customers');
// DELETE FROM customers WHERE (email = 'john@example.com')
Get All
<?php
$customers = App::db()
->getAll('customers');
// SELECT * FROM customers
<?php
$customers = App::db()
->groupBy('email')
->orderBy('firstname', 'ASC')
->page(3)
->limit(20)
->getAll('customers', ['firstname', 'lastname', 'email']);
// SELECT firstname,lastname,email FROM customers GROUP BY email ORDER BY firstname ASC LIMIT 40,20
<?php
$customers = App::db()
->leftJoins([
'orders o' => ['o.customer_id = c.id'],
'invoices i' => ['i.order_id = o.id'],
])
->getAll('customers c');
// SELECT * FROM customers c LEFT JOIN orders o ON (o.customer_id = c.id) LEFT JOIN invoices i ON (i.order_id = o.id)
<?php
$customers = App::db()
->where([
[
'email =' => 'john@example.com',
// OR
'lastname =' => 'Doe'
],
// AND
['is_active =' => 1],
// AND
['`group` IN' => ['general', 'gold']]
])
->getAll('customers');
// SELECT * FROM customers WHERE (email = 'john@example.com' OR lastname = 'Doe') AND (is_active = '1') AND (`group` IN ('general','gold'))
Get Row
<?php
$customer = App::db()
->where(['email =' => 'john@example.com'])
->getRow('customers');
// SELECT * FROM customers WHERE (email = 'john@example.com') LIMIT 0,1
Get Value
<?php
$customerId = App::db()
->where(['email =' => 'john@example.com'])
->getVal('customers', ['id']);
// SELECT id FROM customers WHERE (email = 'john@example.com') LIMIT 0,1
Debug & Dump
<?php
App::db()->debug(true); // Do not execute all the next requests
App::db()
->where(['email =' => 'john@example.com'])
->update('customers', ['firstname' => 'Jane']);
var_dump(App::db()->dump()); // Retrieve last query and params
App::db()->debug(false); // Disabled the debug mode
Console Commands
MW_ENVIRONMENT={environment} bin/magework {package} {identifier} {args...}
To add a command to execute via CLI, add a new class in the Console folder of your package.
Create a new class: packages/Acme/Console/Date.php
<?php
declare(strict_types=1);
class Acme_Console_Date implements Core_Console_Interface
{
public function run(array $args): int
{
$format = $args[0] ?? 'Y-m-d H:i:s';
echo date($format) . "\n";
return self::SUCCESS;
}
}
Run the script via the command line:
bin/magework Acme date "d/m/Y H:i"
The identifier contains underscore for deep classes.
The class Acme_Console_Customer_Clean in packages/Acme/Console/Customer/Clean.php will be executed with:
bin/magework Acme customer_clean
Forms
<?php
/** @var Core_Model_Form $form */
$form = App::getSingleton('form', Core_Model::TYPE);
if ($form->isPost()) {
$form->setFormFields( // Fields to retrieve on error
[
'firstname' => 'Firstname',
'customer[firstname]' => 'Customer firstname',
'customer[address][0]' => 'Customer address line 1',
'email' => 'E-mail',
'country' => 'Country',
]
)
->setFormSpamField('subject') // Error if this (hidden) field is filled
->setFormRequiredFields( // All required fields
[
'firstname',
'customer[firstname]',
'customer[address][0]',
]
)
->setFormExceptedValues( // Field to check
[
'firstname' => '/[^0-9]/', // string = preg_match
'email' => FILTER_VALIDATE_EMAIL, // int = filter_var
'country' => ['fr', 'es', 'us'], // array = in_array
]
)
->validate();
if (!$form->getError()) {
/* ... */
$form->getFirstname(); // name="firstname"
$form->getCustomerFirstname(); // name="customer[firstname]"
$form->getData('customer_address_0'); // name="customer[address][0]"
/** @var Core_Mail $message */
$message = App::getSingleton('contact', Core_Mail::TYPE); // Magework_Mail_Contact
$form->setMailSubject('New contact message!');
$form->setMailMessage($message->render());
$form->setMailSendTo('contact@example.com');
$form->sendMail();
} else {
$form->getErrorFieldName();
$form->getErrorFieldLabel();
}
}
Captcha
Display the PNG captcha image
On the page class containing the form, add a method to generate a captcha and store the text in a session variable:
<?php
declare(strict_types=1);
class Acme_Page_Contact extends Core_Page
{
public function getCaptcha(): string
{
$captcha = new Captcha();
App::session()->set('captcha', $captcha->getText());
return $captcha->inline();
}
}
Display the captcha image in the form:
<form action="<?= $this->getUrl('contact/post/') ?>" method="post">
<img src="<?= $this->getCaptcha() ?>" alt="This is a textual captcha" />
<input type="text" name="secure" value="" />
<input type="submit" value="Submit" />
</form>
Validate the captcha
When the form is submitted, check the captcha's validity:
<?php
declare(strict_types=1);
class Acme_Page_Contact_Post extends Core_Page
{
public function execute(): string
{
$dataPost = $this->dataPost();
$captcha = App::session()->pull('captcha');
if ($dataPost->getData('secure') !== $captcha) {
$this->setErrorMessage('Captcha is not correct');
$this->redirect('contact.html');
}
// Form Processing
}
}
Captcha customization
| Method | Description | Example |
|---|---|---|
| setFont | Absolute monospace font file path in ttf format | /var/www/magework/fonts/courier.ttf |
| setColor | Text color in hexadecimal | #ffffff |
| setBackground | Background color in hexadecimal | #000000 |
| setChars | Allowed characters (included in the font) | abcdefghijkmnopqrstuvwxyz23456789 |
| setLength | Text length | 5 |
| setWidth | Image width in px | 105 |
| setHeight | Image height in px | 40 |
| setFontSize | Font size | 20 |
| setPaddingTop | Image padding top in px | 28 |
| setPaddingLeft | Image padding left in px | 12 |
| setAngle | Text orientation angle | 5 |
<?php
$captcha = new Captcha();
$captcha->setColor('#ffffff')->setBackground('#000000');
Framework tools
Page
<?php $page = App::page();
The page method allows retrieving the current page object from anywhere.
<?php $route = App::getRoute();
The getRoute method allows retrieving the current (cleaned) route name from anywhere (e.g. /contact.html).
/my-page.html = /my-page.html /my-page/ = /my-page /my-page/?q=1 = /my-page /my-page?q=1 = /my-page
Encryption
<?php
$var = App::encryption()?->crypt('message');
echo App::encryption()?->decrypt($var);
The encryption key is stored in var/encryption. If the key is lost, it will be impossible to decrypt the data ever again.
Cache
<?php
App::cache()?->set('foo', 'bar');
echo App::cache()?->get('foo');
The cache file is stored in var/cache. The default cache lifetime is 86400. You can update the lifetime before variable assignment:
<?php
App::cache()?->setLifetime(3600)->set('foo', 'bar');
Session
<?php
App::session()?->set('foo', 'bar');
echo App::session()?->get('foo');
The sessions are stored in var/session.
Log
<?php
App::log('message');
The logs are stored in var/log.
The second parameter allows you to change the log level (default is "info").
<?php
App::log('message', Logger::INFO);
Logger::EMERG; // Emergency: system is unusable
Logger::ALERT; // Alert: action must be taken immediately
Logger::CRIT; // Critical: critical conditions
Logger::ERR; // Error: error conditions
Logger::WARN; // Warning: warning conditions
Logger::NOTICE; // Notice: normal but significant condition
Logger::INFO; // Informational: informational messages
Logger::DEBUG; // Debug: debug messages
Database
<?php
$id = App::db()->insert('table', ['name' => 'John Doe']);
App::db()->query('UPDATE table SET name = ? WHERE id = ?', ["Jane Doe", 1]);
$result = App::db()->query('SELECT * FROM table')->fetchAll();
$result = App::db()->query('SELECT * FROM table WHERE id = ?', [1])->fetch();
Config
<?php
echo App::getConfig('app.domain');
Escaper
<?php
App::escapeHtml('<a href="#">Escaped HTML</a>');
App::escapeHtmlAttr('my-class');
App::escapeUrl('https://www.example.com');
App::escaper()->escapeQuotes("that's true");
<a href="#" onclick="alert('<?= App::escaper()->escapeQuotes("that's true") ?>')">Alert</a>
Search
<?php
$indexes = [
1 => 'Phillip J. Fry',
2 => 'Leela Turanga',
3 => 'Bender Bending Rodriguez',
4 => 'Capitaine Zapp Brannigan',
5 => 'Amy Wong',
];
$search = new Search();
$result = $search->search('leilla captain', $indexes);
array(2) {
[2]=> string(13) "Leela Turanga"
[4]=> string(24) "Capitaine Zapp Brannigan"
}
Custom shared libraries
You can add custom libraries shared by all packages.
Add the classes in the lib directory from the project root. Create the lib directory if not exists.
Example
<?php
// lib/Tools.php
declare(strict_types=1);
class Tools
{
public function format(string $value): string
{
return ucfirst(strtolower($value));
}
}
You can then instantiate the class anywhere.
<?php
$tools = new Tools();
echo $tools->format('Hello World!');
Templating best practices
Type hinting
Add the type hinting using the @var tag at the beginning of the template file.
<?php /** @var Core_Page $this */ ?>
<?php /** @var Core_Block $this */ ?>
URL
Always use the getUrl method to build internal links, with the link path as an argument. This secures the link.
<a href="<?= $this->getUrl('page.html') ?>">My Page</a>
<a href="<?= $this->getUrl('page.html') ?>#contact">My Page</a>
<a href="<?= $this->getUrl('download/invoice', ['id' => 1]) ?>">Download</a>
<a href="<?= $this->getUrl('customer.html', ['id' => 1]) ?>">My Account</a>
Escape
Always escape variables from internal methods.
App::escapeHtml: escape content that will be rendered within HTML tagsApp::escapeHtmlAttr: escape data that will be placed within HTML element attributesApp::escaper()->escapeQuotes: escape single quotation marks in a string by prefixing them with a backslash
<p><?= App::escapeHtml($this->getWelcomeText()) ?></p>
<p class="<?= App::escapeHtmlAttr($this->getClassName()) ?>">My Text</p>
<a href="#" onclick="alert('<?= App::escaper()->escapeQuotes("that's true") ?>')">Alert</a>
External libraries with composer
Feel free to use external libraries.
composer require symfony/var-dumper
<?php
declare(strict_types=1);
class Acme_Page_Index extends Core_Page
{
public function execute(): void
{
dump('foobar');
}
}
composer require nette/utils
<?php
declare(strict_types=1);
use Nette\Utils\Floats;
class Acme_Page_Index extends Core_Page
{
public function execute(): void
{
echo Floats::areEqual(0.1 + 0.2, 0.3) ? 'same' : 'not same';
}
}
Write content in Markdown
Library
In this example, we use the michelf/php-markdown package (see external libraries with composer).
composer require michelf/php-markdown
You can adapt it with your preferred Markdown library.
Page override
To write the page content in markdown, we need to override the default Core_Page class (see object override).
packages > Acme > Page.php
<?php
declare(strict_types=1);
use Michelf\MarkdownExtra;
class Acme_Page extends Core_Page
{
public function include(?string $template): string
{
$markdown = App::getPackagePath('template' . DS . $template . '.md');
if (!is_file($markdown)) {
return parent::include($template);
}
$content = file_get_contents($markdown);
return MarkdownExtra::defaultTransform($content);
}
}
Page configuration
Then, we add a new page with the content path (see create a page).
packages > Acme > etc > pages.php
<?php
$config = [
Core_Page::TYPE => [
/* ... */
'/markdown.html' => [
'content' => 'content/my-markdown-content',
'meta_title' => 'Markdown Content',
'meta_description' => 'The content of this page is written in Markdown',
],
/* ... */
],
];
Markdown file
packages > Acme > template > content > my-markdown-content.md
# Hello World! Welcome to my website.
Static Site Generator
With the native Static package, you can generate a static version of any package and serve the files directly.
The site will be generated in the static/{package} folder, then you can configure the web server to use this folder as root directory.
Command
MW_ENVIRONMENT={environment} bin/magework Static build {package} {baseUrl}
Example
For example, if your application contains a package named Acme and you want to serve it at the URL https://www.example.com:
MW_ENVIRONMENT=default bin/magework Static build Acme https://www.example.com
Configuration
You can create a configuration file in the package to be generated to ignore routes or files.
<?php
// packages/Acme/etc/static.php
$config = [
'static' => [
'ignore_files' => [ // Regex only
'/(.*)README.md$/i',
'/(.*)CHANGELOG.md$/i',
'/(.*)LICENCE$/i',
'/(.*)\.php$/i',
],
'ignore_routes' => [ // Regex only
'/\/secret.html/i',
'/\/secret\/(.*)/i',
],
],
];
lib/Session.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Session
{
private string $prefix = 'ses_';
private bool $sessionStarted = false;
/**
* @throws Exception
*/
public function __construct(?string $sessionDir = null, ?string $name = null)
{
$this->setSessionDir($sessionDir);
if ($name) {
session_name($name);
}
}
/**
* Set prefix for sessions
*
* @param mixed $prefix
* @return bool
*/
public function setPrefix(string $prefix): bool
{
return is_string($this->prefix = $prefix);
}
/**
* Get prefix for sessions
*
* @return string
*/
public function getPrefix(): string
{
return $this->prefix;
}
/**
* If the session has not started, start the session
*
* @param int $lifeTime
* @param bool $secure
* @param bool $httpOnly
* @param string $semeSite
* @param string|null $domain
* @return bool
*/
public function init(
int $lifeTime = 0,
bool $secure = false,
bool $httpOnly = true,
string $semeSite = 'Lax',
?string $domain = null
): bool {
if (!$this->sessionStarted) {
$semeSite = ucfirst(strtolower($semeSite));
if (!in_array($semeSite, ['None', 'Lax', 'Strict'])) {
$semeSite = 'Lax';
}
session_set_cookie_params($lifeTime, '/;SameSite=' . $semeSite, $domain, $secure, $httpOnly);
session_start();
return $this->sessionStarted = true;
}
return false;
}
/**
* Add value to a session
*
* @param string|array $key
* @param mixed $value
* @return bool true
*/
public function set(string|array $key, mixed $value = null): bool
{
if (is_array($key)) {
foreach ($key as $name => $value) {
$_SESSION[$this->prefix . $name] = $value;
}
} else {
$_SESSION[$this->prefix . $key] = $value;
}
return true;
}
/**
* Extract session item, delete session item and finally return the item
*
* @param string $key
* @return mixed|null
*/
public function pull(string $key = ''): mixed
{
$value = self::get($key);
if ($key === '') {
$_SESSION = [];
} else {
if (isset($_SESSION[$this->prefix . $key])) {
unset($_SESSION[$this->prefix . $key]);
}
}
return $value;
}
/**
* Get item from session
*
* @param string $key
* @return mixed
*/
public function get(string $key = ''): mixed
{
if ($key === '') {
return $_SESSION;
}
return $_SESSION[$this->prefix . $key] ?? null;
}
/**
* Check item from session
*
* @param string $key
* @return bool
*/
public function has(string $key = ''): bool
{
if ($key === '') {
return !empty($_SESSION);
}
return isset($_SESSION[$this->prefix . $key]);
}
/**
* Get session id
*
* @return string
*/
public function id(): string
{
return session_id();
}
/**
* Regenerate session_id
*
* @return string
*/
public function regenerate(): string
{
session_regenerate_id(true);
return session_id();
}
/**
* Destroy the session and remove the cookie
*
* @return bool
*/
public function destroy(): bool
{
setcookie(session_name(), '', time() - 86400);
session_destroy();
return true;
}
/**
* Check the session directory
*
* @throws Exception
*/
private function checkSessionDir(string $sessionDir): void
{
if (!is_dir($sessionDir) && !mkdir($sessionDir, 0700, true)) {
throw new Exception('Unable to create directory ' . $sessionDir);
}
if (!is_readable($sessionDir) || !is_writable($sessionDir)) {
if (!chmod($sessionDir, 0700)) {
throw new Exception($sessionDir . ' must be readable and writable');
}
}
}
/**
* Set the session directory
*
* @param string|null $sessionDir
* @return void
* @throws Exception
*/
public function setSessionDir(?string $sessionDir): void
{
if ($sessionDir !== null) {
$this->checkSessionDir($sessionDir);
$sessionDir = preg_replace('/[\/\\\\]/', DIRECTORY_SEPARATOR, $sessionDir);
$sessionDir = rtrim($sessionDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
session_save_path($sessionDir);
}
}
}
lib/Search.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Search
{
private float $minDistance;
private int $minChar;
public function __construct(int $minChar = 3, float $minDistance = 0.4)
{
$this->minChar = $minChar;
$this->minDistance = $minDistance;
}
/**
* Search if a query match indexed data. The result is the filtered indexed data.
*
* @param string $query
* @param array $indexes
* @return array
*/
public function search(string $query, array $indexes): array
{
$words = $this->getWords($query);
$result = [];
foreach ($indexes as $key => $index) {
$candidates = array_unique(array_filter(explode(' ', $this->format($index))));
foreach ($words as $word) {
foreach ($candidates as $candidate) {
if (str_contains($candidate, $word)) {
$result[$key] = $index;
break 2;
}
$distance = levenshtein($word, $candidate) / max(strlen($word), strlen($candidate));
if ($distance < $this->minDistance) {
$result[$key] = $index;
break 2;
}
}
}
}
return $result;
}
/**
* Extract cleaned words from a query
*
* @param string $query
* @return array
*/
public function getWords(string $query): array
{
$words = array_filter(explode(' ', $this->format($query)));
foreach ($words as $key => $word) {
if (strlen($word) < $this->minChar) {
unset($words[$key]);
continue;
}
if (strlen($word) > 3 && str_ends_with($word, 's')) {
$word = substr($word, 0, -1);
}
$words[$key] = $word;
}
return array_unique($words);
}
/**
* Clean a string
*
* @param string $value
* @return string
*/
public function format(string $value): string
{
$value = htmlspecialchars_decode($value);
$value = urldecode($value);
$value = strip_tags($value);
$value = $this->replaceSpecialChars($value);
$value = strtolower($value);
$value = preg_replace('/[^a-z0-9]+/i', ' ', $value);
$value = preg_replace('/\s+/', ' ', $value);
return $value;
}
/**
* Replace special chars in a string
*
* @param string $value
* @return string
*/
public function replaceSpecialChars(string $value): string
{
$convertTable = [
'&' => 'and', '@' => 'at', '©' => 'c', '®' => 'r', 'À' => 'a',
'Á' => 'a', 'Â' => 'a', 'Ä' => 'a', 'Å' => 'a', 'Æ' => 'ae','Ç' => 'c',
'È' => 'e', 'É' => 'e', 'Ë' => 'e', 'Ì' => 'i', 'Í' => 'i', 'Î' => 'i',
'Ï' => 'i', 'Ò' => 'o', 'Ó' => 'o', 'Ô' => 'o', 'Õ' => 'o', 'Ö' => 'o',
'Ø' => 'o', 'Ù' => 'u', 'Ú' => 'u', 'Û' => 'u', 'Ü' => 'u', 'Ý' => 'y',
'ß' => 'ss','à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'å' => 'a',
'æ' => 'ae','ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ò' => 'o', 'ó' => 'o',
'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'þ' => 'p', 'ÿ' => 'y', 'Ā' => 'a',
'ā' => 'a', 'Ă' => 'a', 'ă' => 'a', 'Ą' => 'a', 'ą' => 'a', 'Ć' => 'c',
'ć' => 'c', 'Ĉ' => 'c', 'ĉ' => 'c', 'Ċ' => 'c', 'ċ' => 'c', 'Č' => 'c',
'č' => 'c', 'Ď' => 'd', 'ď' => 'd', 'Đ' => 'd', 'đ' => 'd', 'Ē' => 'e',
'ē' => 'e', 'Ĕ' => 'e', 'ĕ' => 'e', 'Ė' => 'e', 'ė' => 'e', 'Ę' => 'e',
'ę' => 'e', 'Ě' => 'e', 'ě' => 'e', 'Ĝ' => 'g', 'ĝ' => 'g', 'Ğ' => 'g',
'ğ' => 'g', 'Ġ' => 'g', 'ġ' => 'g', 'Ģ' => 'g', 'ģ' => 'g', 'Ĥ' => 'h',
'ĥ' => 'h', 'Ħ' => 'h', 'ħ' => 'h', 'Ĩ' => 'i', 'ĩ' => 'i', 'Ī' => 'i',
'ī' => 'i', 'Ĭ' => 'i', 'ĭ' => 'i', 'Į' => 'i', 'į' => 'i', 'İ' => 'i',
'ı' => 'i', 'IJ' => 'ij','ij' => 'ij','Ĵ' => 'j', 'ĵ' => 'j', 'Ķ' => 'k',
'ķ' => 'k', 'ĸ' => 'k', 'Ĺ' => 'l', 'ĺ' => 'l', 'Ļ' => 'l', 'ļ' => 'l',
'Ľ' => 'l', 'ľ' => 'l', 'Ŀ' => 'l', 'ŀ' => 'l', 'Ł' => 'l', 'ł' => 'l',
'Ń' => 'n', 'ń' => 'n', 'Ņ' => 'n', 'ņ' => 'n', 'Ň' => 'n', 'ň' => 'n',
'ʼn' => 'n', 'Ŋ' => 'n', 'ŋ' => 'n', 'Ō' => 'o', 'ō' => 'o', 'Ŏ' => 'o',
'ŏ' => 'o', 'Ő' => 'o', 'ő' => 'o', 'Œ' => 'oe','œ' => 'oe','Ŕ' => 'r',
'ŕ' => 'r', 'Ŗ' => 'r', 'ŗ' => 'r', 'Ř' => 'r', 'ř' => 'r', 'Ś' => 's',
'ś' => 's', 'Ŝ' => 's', 'ŝ' => 's', 'Ş' => 's', 'ş' => 's', 'Š' => 's',
'š' => 's', 'Ţ' => 't', 'ţ' => 't', 'Ť' => 't', 'ť' => 't', 'Ŧ' => 't',
'ŧ' => 't', 'Ũ' => 'u', 'ũ' => 'u', 'Ū' => 'u', 'ū' => 'u', 'Ŭ' => 'u',
'ŭ' => 'u', 'Ů' => 'u', 'ů' => 'u', 'Ű' => 'u', 'ű' => 'u', 'Ų' => 'u',
'ų' => 'u', 'Ŵ' => 'w', 'ŵ' => 'w', 'Ŷ' => 'y', 'ŷ' => 'y', 'Ÿ' => 'y',
'Ź' => 'z', 'ź' => 'z', 'Ż' => 'z', 'ż' => 'z', 'Ž' => 'z', 'ž' => 'z',
'ſ' => 'z', 'Ə' => 'e', 'ƒ' => 'f', 'Ơ' => 'o', 'ơ' => 'o', 'Ư' => 'u',
'ư' => 'u', 'Ǎ' => 'a', 'ǎ' => 'a', 'Ǐ' => 'i', 'ǐ' => 'i', 'Ǒ' => 'o',
'ǒ' => 'o', 'Ǔ' => 'u', 'ǔ' => 'u', 'Ǖ' => 'u', 'ǖ' => 'u', 'Ǘ' => 'u',
'ǘ' => 'u', 'Ǚ' => 'u', 'ǚ' => 'u', 'Ǜ' => 'u', 'ǜ' => 'u', 'Ǻ' => 'a',
'ǻ' => 'a', 'Ǽ' => 'ae','ǽ' => 'ae','Ǿ' => 'o', 'ǿ' => 'o', 'ə' => 'e',
'Ё' => 'jo','Є' => 'e', 'І' => 'i', 'Ї' => 'i', 'А' => 'a', 'Б' => 'b',
'В' => 'v', 'Г' => 'g', 'Д' => 'd', 'Е' => 'e', 'Ж' => 'zh','З' => 'z',
'И' => 'i', 'Й' => 'j', 'К' => 'k', 'Л' => 'l', 'М' => 'm', 'Н' => 'n',
'О' => 'o', 'П' => 'p', 'Р' => 'r', 'С' => 's', 'Т' => 't', 'У' => 'u',
'Ф' => 'f', 'Х' => 'h', 'Ц' => 'c', 'Ч' => 'ch','Ш' => 'sh','Щ' => 'sch',
'Ъ' => '-', 'Ы' => 'y', 'Ь' => '-', 'Э' => 'je','Ю' => 'ju','Я' => 'ja',
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e',
'ж' => 'zh','з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l',
'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's',
'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch',
'ш' => 'sh','щ' => 'sch','ъ' => '-','ы' => 'y', 'ь' => '-', 'э' => 'je',
'ю' => 'ju','я' => 'ja','ё' => 'jo','є' => 'e', 'і' => 'i', 'ї' => 'i',
'Ґ' => 'g', 'ґ' => 'g', 'א' => 'a', 'ב' => 'b', 'ג' => 'g', 'ד' => 'd',
'ה' => 'h', 'ו' => 'v', 'ז' => 'z', 'ח' => 'h', 'ט' => 't', 'י' => 'i',
'ך' => 'k', 'כ' => 'k', 'ל' => 'l', 'ם' => 'm', 'מ' => 'm', 'ן' => 'n',
'נ' => 'n', 'ס' => 's', 'ע' => 'e', 'ף' => 'p', 'פ' => 'p', 'ץ' => 'C',
'צ' => 'c', 'ק' => 'q', 'ר' => 'r', 'ש' => 'w', 'ת' => 't', '™' => 'tm',
];
return strtr($value, $convertTable);
}
}
lib/Logger.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Logger
{
public const int EMERG = 0; // Emergency: system is unusable
public const int ALERT = 1; // Alert: action must be taken immediately
public const int CRIT = 2; // Critical: critical conditions
public const int ERR = 3; // Error: error conditions
public const int WARN = 4; // Warning: warning conditions
public const int NOTICE = 5; // Notice: normal but significant condition
public const int INFO = 6; // Informational: informational messages
public const int DEBUG = 7; // Debug: debug messages
private string $logFile;
private string $logDir;
/**
* @throws Exception
*/
public function __construct(string $logDir = './', string $logFile = 'app.log')
{
$this->setLogDir($logDir);
$this->setLogFile($logFile);
}
/**
* Add a log
*
* @param mixed $message
* @param int $level
* @return int
*/
public function add(mixed $message, int $level = self::INFO): int
{
return $this->write($this->parseMessage($message, $level));
}
/**
* Write the log in the log file
*
* @param string $message
* @return int
*/
protected function write(string $message): int
{
return (int)file_put_contents($this->getLogPath(), $message, FILE_APPEND);
}
/**
* Parse the message before writing
*
* @param mixed $message
* @param int $level
* @return string
*/
protected function parseMessage(mixed $message, int $level): string
{
if ($message instanceof Throwable) {
$message = $message->getMessage() . "\n" . $message->getTraceAsString();
}
if (is_array($message) || is_object($message)) {
$message = print_r($message, true);
}
return $this->getPrefix($level) . $message . "\n";
}
/**
* Log line prefix
*
* @param int $level
* @return string
*/
protected function getPrefix(int $level): string
{
$data = [
date('Y-m-d H:i:s'),
str_pad(strtoupper($this->getLevelLabel($level)), 10),
''
];
return join(' | ', $data);
}
/**
* Retrieve log label
*
* @param int $level
* @return string
*/
public function getLevelLabel(int $level): string
{
$levels = $this->getLevels();
return $levels[$level] ?? '';
}
/**
* Retrieve log levels
*
* @return string[]
*/
public function getLevels(): array
{
return [
self::EMERG => 'Emergency',
self::ALERT => 'Alert',
self::CRIT => 'Critical',
self::ERR => 'Error',
self::WARN => 'Warning',
self::NOTICE => 'Notice',
self::INFO => 'Info',
self::DEBUG => 'Debug',
];
}
/**
* Retrieve full log file path
*
* @return string
*/
public function getLogPath(): string
{
return $this->getLogDir() . $this->getLogFile();
}
/**
* Check the log directory
*
* @throws Exception
*/
private function checkLogDir(): void
{
if (!is_dir($this->getLogDir()) && !mkdir($this->getLogDir(), 0775, true)) {
throw new Exception('Unable to create directory ' . $this->getLogDir());
}
if (!is_readable($this->getLogDir()) || !is_writable($this->getLogDir())) {
if (!chmod($this->getLogDir(), 0775)) {
throw new Exception($this->getLogDir() . ' must be readable and writable');
}
}
}
/**
* The log directory
*
* @param string $logDir
* @return Logger
* @throws Exception
*/
public function setLogDir(string $logDir): Logger
{
$logDir = preg_replace('/[\/\\\\]/', DIRECTORY_SEPARATOR, $logDir);
$this->logDir = rtrim($logDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->checkLogDir();
return $this;
}
/**
* The log file name
*
* @param string $logFile
* @return Logger
*/
public function setLogFile(string $logFile): Logger
{
$this->logFile = preg_replace('/[^0-9a-zA-Z.]/', '-', $logFile);
return $this;
}
/**
* Retrieve log directory
*
* @return string
*/
public function getLogDir(): string
{
return $this->logDir;
}
/**
* Retrieve log file name
*
* @return string
*/
public function getLogFile(): string
{
return $this->logFile;
}
}
lib/Escaper.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Escaper
{
protected string $encoding = 'utf-8';
/**
* XSS Filtration Pattern
*
* @var string
*/
protected string $xssFiltrationPattern = '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|'
. '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|'
. '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i';
/**
* List of all encoding supported by this class
*/
protected array $supportedEncodings = [
'iso-8859-1',
'iso8859-1',
'iso-8859-5',
'iso8859-5',
'iso-8859-15',
'iso8859-15',
'utf-8',
'cp866',
'ibm866',
'866',
'cp1251',
'windows-1251',
'win-1251',
'1251',
'cp1252',
'windows-1252',
'1252',
'koi8-r',
'koi8-ru',
'koi8r',
'big5',
'950',
'gb2312',
'936',
'big5-hkscs',
'shift_jis',
'sjis',
'sjis-win',
'cp932',
'932',
'euc-jp',
'eucjp',
'eucjp-win',
'macroman',
];
/**
* Constructor: Single parameter allows setting of global encoding for use by the current object.
*
* @param string|null $encoding
* @throws Exception
*/
public function __construct(?string $encoding = null)
{
if ($encoding !== null) {
if ($encoding === '') {
throw new Exception(
self::class . ' constructor parameter does not allow a blank value'
);
}
$encoding = strtolower($encoding);
if (!in_array($encoding, $this->supportedEncodings)) {
throw new Exception(
'Value of \'' . $encoding . '\' passed to ' . self::class
. ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()'
);
}
$this->encoding = $encoding;
}
}
/**
* HTML escaper
*
* @param string $string
* @return string
*/
public function escapeHtml(string $string): string
{
return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $this->encoding, false);
}
/**
* HTML attribute Escaper
*
* @param string $string
* @return string
*/
public function escapeHtmlAttr(string $string): string
{
return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $this->encoding, false);
}
/**
* URL escaper
*
* @param string $string
* @return string
*/
public function escapeUrl(string $string): string
{
$value = html_entity_decode($string);
$filteredData = preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $value);
if ($filteredData === false || $filteredData === '') {
return '';
}
$value = preg_replace($this->xssFiltrationPattern, ':', $filteredData);
if ($value === false) {
return '';
}
if (preg_match($this->xssFiltrationPattern, $value)) {
$value = $this->escapeUrl($value);
}
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 | ENT_HTML401, 'UTF-8', false);
}
/**
* Escape quotes
*
* @param string $string
* @param string $quote
* @return string
*/
public function escapeQuotes(string $string, string $quote = '\''): string
{
return str_replace($quote, '\\' . $quote, $string);
}
}
lib/Encryption.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Encryption
{
public const string CIPHER_ALGO = 'aes-256-cbc';
private string $keyFile;
private string $keyDir;
private string $key = '';
private string $fixedIv = '';
/**
* @throws Exception
*/
public function __construct(string $keyDir = './', string $keyFile = '.encryption.key')
{
if (!extension_loaded('openssl')) {
throw new Exception('The OpenSSL PHP extension is required');
}
$this->setKeyDir($keyDir);
$this->setKeyFile($keyFile);
$this->writeKey();
$this->readKey();
}
/**
* The encryption key directory
*
* @param string $keyDir
* @return Encryption
*/
public function setKeyDir(string $keyDir): Encryption
{
$keyDir = preg_replace('/[\/\\\\]/', DIRECTORY_SEPARATOR, $keyDir);
$this->keyDir = rtrim($keyDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return $this;
}
/**
* The key file name
*
* @param string $keyFile
* @return Encryption
*/
public function setKeyFile(string $keyFile): Encryption
{
$this->keyFile = preg_replace('/[^0-9a-zA-Z.]/', '-', $keyFile);
return $this;
}
/**
* Crypt a message
*
* @param string $message
* @param bool $fixedIv: only use a fixed IV with a completely unique message
* @return string
*/
public function crypt(string $message, bool $fixedIv = false): string
{
$iv = $fixedIv ? $this->fixedIv : $this->getIv();
$text = openssl_encrypt($message, self::CIPHER_ALGO, $this->getKey(), 0, $iv);
if (!$text) {
return '';
}
$hash = hash_hmac('sha256', $text . $iv, $this->getKey());
return ($fixedIv ? '' : $iv) . $hash . strtr($text, '+/=', '-_,');
}
/**
* Decrypt a message
*
* @param string $message
* @param bool $fixedIv
* @return string
*/
public function decrypt(string $message, bool $fixedIv = false): string
{
$iv = $fixedIv ? $this->fixedIv : substr($message, 0, 16);
$hash = substr($message, $fixedIv ? 0 : 16, 64);
$text = strtr(substr($message, $fixedIv ? 64 : 80), '-_,', '+/=');
if (!hash_equals(hash_hmac('sha256', $text . $iv, $this->getKey()), $hash)) {
return '';
}
return openssl_decrypt($text, self::CIPHER_ALGO, $this->getKey(), 0, $iv) ?: '';
}
/**
* Read the encryption key from file
*
* @return void
* @throws Exception
*/
protected function readKey(): void
{
if (!is_readable($this->getKeyPath())) {
throw new Exception('Unable to read the encryption key file');
}
$permission = substr(sprintf('%o', fileperms($this->getKeyPath())), -4);
if ($permission !== '0400') {
throw new Exception('The encryption key file must have read-only permission from the owner (0400)');
}
$content = file_get_contents($this->getKeyPath());
if (!$content) {
throw new Exception('The encryption key file is empty');
}
if (!preg_match('/^[a-z0-9]{16}::[a-z0-9]{32}$/', $content)) {
throw new Exception('Wrong encryption key format');
}
$key = explode('::', $content);
$this->fixedIv = $key[0];
$this->key = $key[1];
}
/**
* Write the encryption key in file
*
* @throws Exception
*/
protected function writeKey(): void
{
if (!is_file($this->getKeyPath())) {
$this->checkKeyDir();
if (!file_put_contents($this->getKeyPath(), $this->generateKey())) {
throw new Exception('Unable to write the encryption key file');
}
if (!chmod($this->getKeyPath(), 0400)) {
throw new Exception('Unable to update the encryption key file permission');
}
}
}
/**
* Check the key directory
*
* @throws Exception
*/
private function checkKeyDir(): void
{
if (!is_dir($this->getKeyDir()) && !@mkdir($this->getKeyDir(), 0775, true)) {
throw new Exception('Unable to create directory ' . $this->getKeyDir());
}
if (!is_readable($this->getKeyDir()) || !is_writable($this->getKeyDir())) {
if (!chmod($this->getKeyDir(), 0775)) {
throw new Exception($this->getKeyDir() . ' must be readable and writable');
}
}
}
/**
* Generate a random 128-Bit key with a 64-bit fixed IV
*
* @return string
* @throws Exception
*/
protected function generateKey(): string
{
return $this->getIv() . '::' . bin2hex(openssl_random_pseudo_bytes(16));
}
/**
* Retrieve key file
*
* @return string
*/
protected function getKeyPath(): string
{
return $this->keyDir . $this->keyFile;
}
/**
* Retrieve the key directory
*
* @return string
*/
protected function getKeyDir(): string
{
return $this->keyDir;
}
/**
* Retrieve encryption key
*
* @return string
*/
private function getKey(): string
{
return $this->key;
}
/**
* Retrieve IV
*
* @return string
*/
private function getIv(): string
{
return bin2hex(openssl_random_pseudo_bytes(8));
}
}
lib/Database.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Database
{
protected PDO $pdo;
protected array $conditions = [];
protected array $joins = ['inner' => [], 'left' => [], 'right' => []];
protected ?string $groupBy = null;
protected ?string $orderBy = null;
protected ?int $limit = null;
protected ?int $page = 1;
protected bool $debug = false;
protected array $dump = ['query' => '', 'params' => []];
/**
* @throws PDOException
*/
public function __construct(
private readonly string $username,
private readonly string $password,
private readonly string $database,
private readonly string $host = 'localhost',
private readonly int $port = 3306,
private readonly ?string $charset = null
) {
$this->connect();
}
/**
* Get all rows.
*
* @param string $table
* @param array $columns
* @return array
*/
public function getAll(string $table, array $columns = ['*']): array
{
return $this->get($table, $columns)?->fetchAll() ?: [];
}
/**
* Get only the first row
*
* @param string $table
* @param array $columns
* @return array|null
*/
public function getRow(string $table, array $columns = ['*']): ?array
{
return $this->page(0)->limit(1)->get($table, $columns)?->fetch() ?: null;
}
/**
* Get ony the first value of the first row based on its column type
*
* @param string $table
* @param array $columns
* @return mixed
*/
public function getVal(string $table, array $columns = ['*']): mixed
{
return $this->page(0)->limit(1)->get($table, $columns)?->fetchColumn() ?: null;
}
/**
* Insert. Return last inserted id.
*
* @param string $table
* @param array $data
* @param array $updateOnDuplicate
* @return int
*/
public function insert(string $table, array $data, array $updateOnDuplicate = []): int
{
$columns = join(', ', array_keys($data));
$toBind = str_repeat('?,', count($data) - 1) . '?';
$query = 'INSERT INTO ' . $table . ' (' . $columns . ') VALUES (' . $toBind . ')';
if (!empty($updateOnDuplicate)) {
$values = [];
foreach ($updateOnDuplicate as $column => $value) {
if (is_numeric($column)) {
$values[] = (string)$value;
continue;
}
$values[] = $column . ' = ?';
$data[] = $value;
}
$query .= ' ON DUPLICATE KEY UPDATE ' . join(', ', $values);
}
$this->query($query, array_values($data));
return (int)$this->getConnection()->lastInsertId();
}
/**
* Update. Return affected rows.
*
* @param string $table
* @param array $data
* @return int
*/
public function update(string $table, array $data): int
{
$columns = [];
$values = [];
foreach ($data as $column => $value) {
if (is_numeric($column)) {
$columns[] = (string)$value;
continue;
}
$columns[] = $column . ' = ?';
$values[] = $value;
}
$where = $this->buildWhere($this->conditions);
$params = array_merge($values, array_values($where[1] ?? []));
$result = $this->query(
'UPDATE ' . $table . ' SET ' . join(', ', $columns) . (!empty($where[0] ?? []) ? ' WHERE ' . $where[0] : ''),
$params
);
return $result->rowCount();
}
/**
* Delete. Return affected rows.
*
* @param string $table
* @return int
*/
public function delete(string $table): int
{
$where = $this->buildWhere($this->conditions);
$result = $this->query(
'DELETE FROM ' . $table . (!empty($where[0] ?? []) ? ' WHERE ' . $where[0] : ''),
$where[1]
);
return $result->rowCount();
}
/**
* Get Data.
*
* @param string $table
* @param array $columns
* @return PDOStatement|null
*/
public function get(string $table, array $columns = ['*']): ?PDOStatement
{
$where = $this->buildWhere($this->conditions);
$joins = $this->buildJoins($this->joins);
$params = array_merge($joins[1] ?? [], $where[1] ?? []);
$query = array_filter(
[
'SELECT ' . join(', ', $this->cleanArray($columns)),
'FROM ' . $table,
(!empty($joins[0] ?? []) ? $joins[0] : ''),
(!empty($where[0] ?? []) ? 'WHERE ' . $where[0] : ''),
$this->groupBy ? 'GROUP BY ' . $this->groupBy : '',
$this->orderBy ? 'ORDER BY ' . $this->orderBy : '',
$this->limit !== null ? 'LIMIT ' . $this->getOffset() . ',' . $this->limit : '',
]
);
return $this->query(join(' ', $query), $params) ?: null;
}
/**
* Execute a query
*
* @param string $sql
* @param array $params
* @return false|PDOStatement
*/
public function query(string $sql, array $params = []): false|PDOStatement
{
$statement = $this->getConnection()->prepare($sql);
$this->dump = ['query' => $sql, 'params' => $params];
if (!$this->debug) {
$statement->execute($params);
}
$this->reset();
return $statement;
}
/**
* Add conditions
* $conditions = [
* [
* 'field1 =' => 1,
* // OR
* 'field2 = field3',
* ],
* // AND
* ['field4 IN' => [1, 2]],
* // AND
* ['field5 LIKE' => '%value%'],
* ]
*
* Limited to the following logics:
*
* a OR b OR c
* a AND b AND c
* (a OR b OR c) AND (d OR e) AND (f)
*
* @param array $conditions
* @return $this
*/
public function where(array $conditions): Database
{
$this->conditions = $this->cleanArray($conditions);
return $this;
}
/**
* Add left joins with conditions
* $joins = [
* 'table_1 t1' => $conditions,
* 'table_2 t2' => $conditions,
* ]
*
* @param array $joins
* @return $this
*/
public function leftJoins(array $joins): Database
{
$this->setJoins($joins, 'left');
return $this;
}
/**
* Add right joins with conditions
* $joins = [
* 'table_1 t1' => $conditions,
* 'table_2 t2' => $conditions,
* ]
*
* @param array $joins
* @return $this
*/
public function rightJoins(array $joins): Database
{
$this->setJoins($joins, 'right');
return $this;
}
/**
* Add inner joins with conditions
* $joins = [
* 'table_1 t1' => $conditions,
* 'table_2 t2' => $conditions,
* ]
*
* @param array $joins
* @return $this
*/
public function innerJoins(array $joins): Database
{
$this->setJoins($joins);
return $this;
}
/**
* Group by field
*
* @param string|null $groupBy
* @return $this
*/
public function groupBy(?string $groupBy): Database
{
$this->groupBy = $groupBy;
return $this;
}
/**
* Order by field with direction
*
* @param string|null $column
* @param string $direction
* @return $this
*/
public function orderBy(?string $column, string $direction = 'ASC'): Database
{
if (!in_array(strtoupper($direction), ['ASC', 'DESC'])) {
$direction = 'ASC';
}
$this->orderBy = $column ? $column . ' ' . strtoupper($direction) : null;
return $this;
}
/**
* Add limit
*
* @param int|null $limit
* @return $this
*/
public function limit(?int $limit): Database
{
$this->limit = $limit >= 0 ? $limit : 0;
return $this;
}
/**
* Add a page number to calculate offset when a limit is defined
*
* @param int|null $page
* @return $this
*/
public function page(?int $page): Database
{
$this->page = $page >= 1 ? $page : 1;
return $this;
}
/**
* Reset all query builders
*
* @return $this
*/
public function reset(): Database
{
$this->conditions = [];
$this->joins = ['inner' => [], 'left' => [], 'right' => []];
$this->groupBy = null;
$this->orderBy = null;
$this->limit = null;
$this->page = 1;
return $this;
}
/**
* Retrieve the connection
*
* @return PDO
*/
public function getConnection(): PDO
{
return $this->pdo;
}
/**
* Enable debug mode. When debug mode is enabled, the query is not executed.
*
* @param bool $debug
* @return $this
*/
public function debug(bool $debug): Database
{
$this->debug = $debug;
return $this;
}
/**
* Retrieve last query dump
*
* @return array
*/
public function dump(): array
{
return $this->dump;
}
/**
* Retrieve offset
*
* @return int
*/
protected function getOffset(): int
{
return ($this->page - 1) * $this->limit;
}
/**
* Add joins according the type
*
* @param array $joins
* @param string $type
* @return $this
*/
protected function setJoins(array $joins, string $type = 'inner'): Database
{
$joins = $this->cleanArray($joins);
foreach ($joins as $table => $conditions) {
if (!is_array($conditions)) {
continue;
}
$this->joins[$type][(string)$table] = $this->cleanArray($conditions);
}
return $this;
}
/**
* Build joins
*
* @param array $joins
* @return array
*/
protected function buildJoins(array $joins): array
{
$joins = $this->cleanArray($joins);
if (empty($joins)) {
return [];
}
$result = [];
$values = [];
foreach ($joins as $type => $join) {
if (!in_array($type, ['inner', 'left', 'right'])) {
continue;
}
foreach ($join as $table => $conditions) {
if (!is_array($conditions)) {
continue;
}
$where = $this->buildWhere($conditions);
$values = array_merge($values, $where[1]);
$result[] = strtoupper($type) . ' JOIN ' . $table . ' ON ' . $where[0];
}
}
return [join(' ', $result), $values];
}
/**
* Generate conditions
*
* @param array $conditions
* @return array
*/
protected function buildWhere(array $conditions): array
{
$conditions = $this->cleanArray($conditions);
if (empty($conditions)) {
return [];
}
if (!isset($conditions[0]) || !is_array($conditions[0])) {
$conditions = [$conditions];
}
$values = [];
$ands = [];
foreach ($conditions as $condition) {
$condition = $this->cleanArray($condition);
$ors = [];
foreach ($condition as $key => $value) {
if (is_numeric($key)) {
if (is_array($value)) {
continue;
}
$ors[] = (string)$value;
continue;
}
$key = trim($key);
if (!preg_match(
'/(( (<|<=>|<>|>|>=|!=|<=|=|NOT LIKE|LIKE|NOT IN|IN))|(STRCMP|GREATEST|LEAST))$/i',
$key
)) {
$key .= ' =';
}
if (preg_match('/(( (NOT IN|IN))|(STRCMP|GREATEST|LEAST))$/i', $key)) {
if (!is_array($value)) {
$value = [$value];
}
$values = array_merge($values, $value);
$ors[] = $key . ' (' . str_repeat('?,', count($value) - 1) . '?' . ')';
continue;
}
if (is_array($value)) {
foreach ($value as $v) {
$values[] = (string)$v;
$ors[] = $key . ' ?';
}
continue;
}
$values[] = (string)$value;
$ors[] = $key . ' ?';
}
if (!empty($ors)) {
$ands[] = '(' . join(' OR ', $ors) . ')';
}
}
return !empty($ands) ? [join(' AND ', $ands), $values] : [];
}
/**
* Connect to the database
*
* @return void
* @throws PDOException
*/
protected function connect(): void
{
$dsn = [
'host=' . $this->host,
'dbname=' . $this->database,
];
if ($this->charset) {
$dsn[] = 'charset=' . $this->charset;
}
if ($this->port) {
$dsn[] = 'port=' . $this->port;
}
$this->pdo = new PDO(
'mysql:' . join(';', $dsn),
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
}
/**
* Remove empty values in array
*
* @param array $array
* @return array
*/
protected function cleanArray(array $array): array
{
return array_filter($array, function($value) { return !empty($value); });
}
}
lib/DataObject.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class DataObject
{
protected array $data = [];
public function __construct()
{
$args = func_get_args();
if (empty($args[0])) {
$args[0] = [];
}
$this->data = $args[0];
}
/**
* Add data
*
* @param array $values
* @return DataObject
*/
public function addData(array $values): DataObject
{
foreach ($values as $index => $value) {
$this->setData($index, $value);
}
return $this;
}
/**
* Set data
*
* @param mixed $key
* @param mixed $value
* @return DataObject
*/
public function setData(mixed $key, mixed $value = null): DataObject
{
if (is_array($key)) {
$this->data = $key;
} else {
$this->data[$key] = $value;
}
return $this;
}
/**
* Retrieve data
*
* @param string $key
* @return mixed
*/
public function getData(string $key = ''): mixed
{
if ($key === '') {
return $this->data;
}
return $this->data[$key] ?? null;
}
/**
* Extract specific object data keys in new array
*
* @param array $keys
* @return array
*/
public function keep(array $keys = []): array
{
$data = [];
foreach ($keys as $key) {
if (!$this->hasData($key)) {
continue;
}
$data[$key] = $this->data[$key];
}
return $data;
}
/**
* Unset data
*
* @param string|null $key
* @return DataObject
*/
public function unsetData(?string $key = null): DataObject
{
if ($key) {
unset($this->data[$key]);
}
return $this;
}
/**
* Check data
*
* @param string $key
* @return bool
*/
public function hasData(string $key = ''): bool
{
if (empty($key)) {
return !empty($this->data);
}
return array_key_exists($key, $this->data);
}
/**
* Check data is empty
*
* @return bool
*/
public function isEmpty(): bool
{
if (empty($this->data)) {
return true;
}
return false;
}
/**
* Magic method
*
* @param string $method
* @param array $args
*
* @return mixed
* @throws Exception
*/
public function __call(string $method, array $args): mixed
{
switch (substr($method, 0, 3)) {
case 'get':
$key = $this->underscore(substr($method, 3));
return $this->getData($key);
case 'set':
$key = $this->underscore(substr($method, 3));
return $this->setData($key, $args[0] ?? null);
case 'uns':
$key = $this->underscore(substr($method, 3));
return $this->unsetData($key);
}
throw new Exception('Invalid method ' . get_class($this) . '::' . $method);
}
/**
* Format method
*
* @param string $name
* @return string
*/
protected function underscore(string $name): string
{
return strtolower(preg_replace('/(.)([A-Z])/', "$1_$2", $name));
}
/**
* Object to array
*
* @param array $arrAttributes
* @return array
*/
public function toArray(array $arrAttributes = []): array
{
if (empty($arrAttributes)) {
return $this->data;
}
$arrRes = [];
foreach ($arrAttributes as $attribute) {
if (isset($this->data[$attribute])) {
$arrRes[$attribute] = $this->data[$attribute];
} else {
$arrRes[$attribute] = null;
}
}
return $arrRes;
}
}
lib/Config.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Config
{
protected array $files = [];
protected array $configurations = [];
protected array $result = [];
/**
* Set all configurations
*
* @param array $configurations
*/
public function setConfigurations(array $configurations): void
{
foreach ($configurations as $configuration) {
$this->addConfiguration($configuration);
}
}
/**
* Add a configuration
*
* @param array $configuration
* @return void
*/
public function addConfiguration(array $configuration): void
{
$this->configurations[] = $configuration;
}
/**
* Set config files
*
* @param string[] $files
*/
public function setFiles(array $files): void
{
foreach ($files as $file) {
$this->addFile($file);
}
}
/**
* Add configuration from file
*
* @param string $file
* @return void
*/
public function addFile(string $file): void
{
$this->configurations[] = $this->loadFile($file);
}
/**
* Retrieve config value
*
* @param string $path
* @return mixed
*/
public function get(string $path): mixed
{
return array_key_exists($path, $this->result) ? $this->result[$path] : null;
}
/**
* Set config value
*
* @param string $path
* @param mixed $value
*/
public function set(string $path, mixed $value): void
{
$this->result[$path] = $value;
}
/**
* Load config
*/
public function load(): void
{
$result = [];
foreach ($this->configurations as $configuration) {
$result = array_replace_recursive($result, $configuration);
}
$this->result = $this->flat($result);
}
/**
* Load config file
*
* @param string $file
* @return array
*/
public function loadFile(string $file): array
{
if (is_file($file)) {
include_once $file;
return is_array($config ?? []) ? $config ?? [] : [];
}
return [];
}
/**
* Flat config
*
* @param array $config
* @param array $result
* @param string $flat
*
* @return array
*/
protected function flat(array $config, array $result = [], string $flat = ''): array
{
foreach ($config as $key => $value) {
$flatKey = ltrim($flat . '.' . $key, '.');
$result[$flatKey] = $value;
if (is_array($value)) {
$result = $this->flat($value, $result, $flatKey);
}
}
return $result;
}
}
lib/Captcha.php
<?php
declare(strict_types=1);
class Captcha
{
private ?string $text = null;
protected string $font = __DIR__ . '/Captcha/font.ttf';
protected string $color = '#000000';
protected string $background = '#FFFFFF';
protected int $length = 5;
protected int $width = 105;
protected int $height = 40;
protected string $chars = 'abcdefghijkmnopqrstuvwxyz23456789';
protected int $fontSize = 20;
protected int $angle = 0;
protected int $paddingLeft = 12;
protected int $paddingTop = 28;
/**
* Retrieve text to print
*
* @return string
*/
public function getText(): string
{
if ($this->text !== null) {
return $this->text;
}
$this->text = substr(str_shuffle(str_repeat($this->getChars(), 5)), 0, $this->getLength());
return $this->text;
}
/**
* Retrieve PNG image data
*
* @return string
*/
public function getImage(): string
{
$image = imagecreate($this->getWidth(), $this->getHeight());
$color = $this->hexToRgb($this->getBackground());
imagecolorallocate($image, (int)$color['r'], (int)$color['g'], (int)$color['b']);
$color = $this->hexToRgb($this->getColor());
$ftColor = imagecolorallocate($image, (int)$color['r'], (int)$color['g'], (int)$color['b']);
imagettftext(
$image,
$this->getFontSize(),
$this->getAngle(),
$this->getPaddingLeft(),
$this->getPaddingTop(),
$ftColor,
$this->getFont(),
$this->getText()
);
ob_start();
imagepng($image);
$result = ob_get_clean();
imagedestroy($image);
return $result;
}
/**
* Gets the HTML inline base64
*
* @return string
*/
public function inline(): string
{
return 'data:image/png;base64,' . base64_encode($this->getImage());
}
/**
* Retrieve RGB color from hexadecimal
*
* @param string $color
*
* @return string[]
*/
public function hexToRgb(string $color): array
{
$hex = ltrim($color, '#');
return [
'r' => hexdec(substr($hex, 0, 2)),
'g' => hexdec(substr($hex, 2, 2)),
'b' => hexdec(substr($hex, 4, 2))
];
}
/**
* Retrieve font
*
* @return string
*/
public function getFont(): string
{
return $this->font;
}
/**
* Absolute font file path in ttf format
*
* @param string $font
* @return Captcha
*/
public function setFont(string $font): Captcha
{
$this->font = $font;
return $this;
}
/**
* Retrieve text color
*
* @return string
*/
public function getColor(): string
{
return $this->color;
}
/**
* Set text color in hexadecimal
*
* @param string $color
* @return Captcha
*/
public function setColor(string $color): Captcha
{
$this->color = $color;
return $this;
}
/**
* Retrieve background color
*
* @return string
*/
public function getBackground(): string
{
return $this->background;
}
/**
* Set background color in hexadecimal
*
* @param string $background
* @return Captcha
*/
public function setBackground(string $background): Captcha
{
$this->background = $background;
return $this;
}
/**
* Retrieve allowed chars
*
* @return string
*/
public function getChars(): string
{
return $this->chars;
}
/**
* Set allowed chars
*
* @param string $chars
* @return Captcha
*/
public function setChars(string $chars): Captcha
{
$this->chars = $chars;
return $this;
}
/**
* Retrieve text length
*
* @return int
*/
public function getLength(): int
{
return $this->length;
}
/**
* Set text length
*
* @param int $length
* @return Captcha
*/
public function setLength(int $length): Captcha
{
$this->length = $length;
return $this;
}
/**
* Retrieve image width
*
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* Set image width in px
*
* @param int $width
* @return Captcha
*/
public function setWidth(int $width): Captcha
{
$this->width = $width;
return $this;
}
/**
* Retrieve image height
*
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* Set image height in px
*
* @param int $height
* @return Captcha
*/
public function setHeight(int $height): Captcha
{
$this->height = $height;
return $this;
}
/**
* Retrieve font size
*
* @return int
*/
public function getFontSize(): int
{
return $this->fontSize;
}
/**
* Set font size
*
* @param int $fontSize
* @return Captcha
*/
public function setFontSize(int $fontSize): Captcha
{
$this->fontSize = $fontSize;
return $this;
}
/**
* Retrieve padding top
*
* @return int
*/
public function getPaddingTop(): int
{
return $this->paddingTop;
}
/**
* Set padding top in px
*
* @param int $paddingTop
* @return Captcha
*/
public function setPaddingTop(int $paddingTop): Captcha
{
$this->paddingTop = $paddingTop;
return $this;
}
/**
* Retrieve padding left
*
* @return int
*/
public function getPaddingLeft(): int
{
return $this->paddingLeft;
}
/**
* Set padding top in px
*
* @param int $paddingLeft
* @return Captcha
*/
public function setPaddingLeft(int $paddingLeft): Captcha
{
$this->paddingLeft = $paddingLeft;
return $this;
}
/**
* Retrieve angle
*
* @return int
*/
public function getAngle(): int
{
return $this->angle;
}
/**
* Set angle
*
* @param int $angle
* @return Captcha
*/
public function setAngle(int $angle): Captcha
{
$this->angle = $angle;
return $this;
}
}
lib/Cache.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Cache
{
public const string KEY_DATA = 'data';
public const string KEY_LIFETIME = 'lifetime';
public const string KEY_TIME = 'time';
private array $data = [];
private int $lifetime = 3600;
private string $cacheFile;
private string $cacheDir;
/**
* @throws Exception
*/
public function __construct(string $cacheDir = './', string $cacheFile = 'default.cache') {
$this->setCacheDir($cacheDir);
$this->setCacheFile($cacheFile);
}
/**
* Set new cache key with the value
*
* @param string $key
* @param mixed $data
* @return Cache
*/
public function set(string $key, mixed $data): Cache
{
if (!$this->canCache()) {
return $this;
}
$this->data[$key] = $this->value($data);
$this->persist();
return $this;
}
/**
* Bulk data in cache
*
* @param array $data
* @return Cache
*/
public function bulk(array $data): Cache
{
if (!$this->canCache()) {
return $this;
}
foreach ($data as $key => $item) {
$this->data[$key] = $this->value($item);
}
$this->persist();
return $this;
}
/**
* Retrieve the cache for the given key
*
* @param string $key
* @return mixed
*/
public function get(string $key): mixed
{
if (!$this->cleanByKey($key)->isCached($key)) {
return null;
}
return unserialize($this->data[$key][self::KEY_DATA]);
}
/**
* Retrieve all cached data
*
* @return array
*/
public function all(): array
{
return array_map(function ($value) { return unserialize($value[self::KEY_DATA]); }, $this->data);
}
/**
* Clean cache by key
*
* @param string $key
* @return Cache
*/
public function cleanByKey(string $key): Cache
{
if (!$this->isExpired($key)) {
return $this;
}
unset($this->data[$key]);
$this->persist();
return $this;
}
/**
* Clean all expired keys
*/
public function cleanExpired(): Cache
{
foreach ($this->data as $key => $item) {
if (!$this->isExpired($key)) {
continue;
}
unset($this->data[$key]);
}
$this->persist();
return $this;
}
/**
* Clean all keys
*/
public function cleanAll(): Cache
{
$this->data = [];
$this->persist();
return $this;
}
/**
* Update the cache lifetime
*
* @param int $lifetime
* @return Cache
*/
public function setLifetime(int $lifetime): Cache
{
$this->lifetime = $lifetime;
return $this;
}
/**
* Retrieve the current lifetime
*
* @return int
*/
public function getLifetime(): int
{
return $this->lifetime;
}
/**
* Check if the key can be cached
*
* @return bool
*/
public function canCache(): bool
{
return $this->getLifetime() > 0;
}
/**
* Is cache exists for the given key
*
* @param string $key
* @return bool
*/
protected function isCached(string $key): bool
{
return isset($this->data[$key]);
}
/**
* Persist the cache in the cache file
*
* @return int
*/
protected function persist(): int
{
return (int)file_put_contents($this->getCachePath(), json_encode($this->data, JSON_PRETTY_PRINT));
}
/**
* Load the cache from the cache file
*
* @return void
*/
protected function load(): void
{
$this->data = [];
$file = $this->getCachePath();
if (file_exists($file)) {
$this->data = json_decode(file_get_contents($file), true) ?: [];
}
}
/**
* Check if the cache key is expired
*
* @param string $key
* @return bool
*/
protected function isExpired(string $key): bool
{
if (!$this->isCached($key)) {
return true;
}
if ($this->data[$key][self::KEY_LIFETIME] <= 0) {
return true;
}
if (time() - $this->data[$key][self::KEY_TIME] > $this->data[$key][self::KEY_LIFETIME]) {
return true;
}
return false;
}
/**
* The cache value
*
* @param mixed $data
* @return array
*/
protected function value(mixed $data): array
{
return [
self::KEY_TIME => time(),
self::KEY_LIFETIME => $this->getLifetime(),
self::KEY_DATA => serialize($data),
];
}
/**
* Check the cache directory
*
* @throws Exception
*/
private function checkCacheDir(): void
{
if (!is_dir($this->getCacheDir()) && !mkdir($this->getCacheDir(), 0775, true)) {
throw new Exception('Unable to create directory ' . $this->getCacheDir());
}
if (!is_readable($this->getCacheDir()) || !is_writable($this->getCacheDir())) {
if (!chmod($this->getCacheDir(), 0775)) {
throw new Exception($this->getCacheDir() . ' must be readable and writable');
}
}
}
/**
* Retrieve full cache path
*
* @return string
*/
public function getCachePath(): string
{
return $this->getCacheDir() . $this->getCacheFile();
}
/**
* Set the cache directory
*
* @param string $cacheDir
* @return Cache
* @throws Exception
*/
protected function setCacheDir(string $cacheDir): Cache
{
$cacheDir = preg_replace('/[\/\\\\]/', DIRECTORY_SEPARATOR, $cacheDir);
$this->cacheDir = rtrim($cacheDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->checkCacheDir();
return $this;
}
/**
* The cache file name
*
* @param string $cacheFile
* @return Cache
*/
public function setCacheFile(string $cacheFile): Cache
{
$this->cacheFile = preg_replace('/[^0-9a-zA-Z.]/', '-', $cacheFile);
$this->load();
return $this;
}
/**
* Retrieve the cache file
*
* @return string
*/
public function getCacheFile(): string
{
return $this->cacheFile;
}
/**
* Retrieve the cache directory
*
* @return string
*/
public function getCacheDir(): string
{
return $this->cacheDir;
}
}
app/Core/Template/Abstract.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
abstract class Core_Template_Abstract extends DataObject
{
protected const array ALLOWED_TEMPLATE_EXTENSIONS = ['phtml', 'tpl.php'];
public function __construct()
{
parent::__construct();
}
/**
* Include a template file
*
* @param string|null $template
* @return string
*/
public function include(?string $template): string
{
if ($template) {
$template = App::getPackagePath('template' . DS . $template);
foreach (self::ALLOWED_TEMPLATE_EXTENSIONS as $extension) {
if (is_file($template . '.' . $extension) && !str_starts_with(basename($template), '.')) {
ob_start();
include $template . '.' . $extension;
return ob_get_clean();
}
}
}
return '';
}
/**
* Render HTML
*
* @return string
*/
public function render(): string
{
if (!$this->getTemplate()) {
return '';
}
return $this->include($this->getTemplate());
}
/**
* Retrieve template file
*
* @return string|null
*/
public function getTemplate(): ?string
{
return $this->getData('template');
}
/**
* Set the template file name
*
* @param string|null $template
* @return $this
*/
public function setTemplate(?string $template): Core_Template_Abstract
{
$this->setData('template', $template);
return $this;
}
/**
* Retrieve block
*
* @param string|null $template
* @param array $data
* @param string|null $identifier
* @return string
*/
public function getBlock(?string $template, array $data = [], ?string $identifier = null): string
{
/** @var Core_Block $block */
$block = App::getObject($identifier, Core_Block::TYPE);
if (!$template) {
$template = $block->getTemplate();
}
if (!$template) {
return '';
}
unset($data['_object_identifier']);
unset($data['_object_type']);
$block->addData($data);
$block->setTemplate($template);
return $block->render();
}
/**
* Retrieve package asset URL
*
* @param string $path
* @return string
*/
public function getAssetUrl(string $path = ''): string
{
return App::escapeUrl(App::getAssetUrl($path));
}
/**
* Retrieve URL
*
* @param string $path
* @param array $params
* @param bool $prefix
* @return string
*/
public function getUrl(string $path = '', array $params = [], bool $prefix = true): string
{
return App::escapeUrl(App::getUrl($path, $params, $prefix));
}
/**
* Retrieve object identifier
*
* @return string|null
*/
public function getObjectIdentifier(): ?string
{
return $this->getData('_object_identifier');
}
/**
* Retrieve Object Type (page, block, mail, helper, page)
*
* @return string|null
*/
public function getObjectType(): ?string
{
return $this->getData('_object_type');
}
}
app/Core/Page.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Page extends Core_Template_Abstract
{
public const string TYPE = 'page';
/**
* Retrieve content
*
* @return string|null
*/
public function getContent(): ?string
{
return $this->getData('content');
}
/**
* Retrieve only POST params
*
* @return DataObject
*/
public function dataPost(): DataObject
{
$object = new DataObject;
foreach ($_POST ?? [] as $field => $value) {
$object->setData($field, $value);
}
return $object;
}
/**
* Retrieve only Get params
*
* @return DataObject
*/
public function dataGet(): DataObject
{
$object = new DataObject;
foreach ($_GET ?? [] as $field => $value) {
$object->setData($field, $value);
}
return $object;
}
/**
* Redirect
*
* @param string|null $link
* @return void
*/
public function redirect(?string $link = null): void
{
if (App::isCli()) {
return;
}
if (!$link) {
$link = $this->getUrl();
}
if (!preg_match('#^https?://#i', $link)) {
$link = $this->getUrl($link);
}
http_response_code(301);
header('Location: ' . $link);
exit();
}
/**
* Reload page with other request
*
* @param $request
* @return Core_Page
*/
public function forward($request): Core_Page
{
$page = App::getObject((string)$request, Core_Page::TYPE);
$this->addData($page->getData());
return $this;
}
/**
* Set error message
*
* @param string $message
* @return void
*/
public function setErrorMessage(string $message): void
{
App::session()?->set('error_message', $message);
}
/**
* Set success message
*
* @param string $message
* @return void
*/
public function setSuccessMessage(string $message): void
{
App::session()?->set('success_message', $message);
}
/**
* Retrieve error message
*
* @return string
*/
public function getErrorMessage(): string
{
return (string)App::session(false)?->pull('error_message');
}
/**
* Retrieve success message
*
* @return string
*/
public function getSuccessMessage(): string
{
return (string)App::session(false)?->pull('success_message');
}
}
app/Core/Object/Factory.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Object_Factory
{
/**
* Retrieve Object
*
* @param string|null $type
* @param string|null $identifier
* @param string|null $package
* @return object
*/
public function getObject(?string $type, ?string $identifier = null, ?string $package = null): object
{
$type = $type ?: 'default';
$identifier = $identifier ?: 'default';
$package = $package ?: App::getPackage();
$paths = array_unique(
[
'default' . '.' . 'default' . '.' . 'default',
'default' . '.' . 'default' . '.' . $identifier,
'default' . '.' . $type . '.' . 'default',
'default' . '.' . $type . '.' . $identifier,
$package . '.' . 'default' . '.' . 'default',
$package . '.' . 'default' . '.' . $identifier,
$package . '.' . $type . '.' . 'default',
$package . '.' . $type . '.' . $identifier,
]
);
$config = [];
foreach ($paths as $path) {
$config = array_merge($config, App::getConfig($path, []));
}
$class = $this->getClass($type, $identifier, $package);
if (isset($config['class'])) {
$class = $config['class'];
}
/* @var $object DataObject */
$object = class_exists($class) ? new $class() : new DataObject();
if ($object instanceof DataObject) {
$object->setData('_object_type', $type);
$object->setData('_object_identifier', $identifier);
$object->addData($config);
}
if (method_exists($object, 'execute')) {
$object->execute();
}
return $object;
}
/**
* Retrieve default class name
*
* @param string $type
* @param string $identifier
* @param string $package
*
* @return string
*/
private function getClass(string $type, string $identifier, string $package): string
{
$type = $this->format($type);
$identifier = $this->format($identifier);
$package = $this->format($package);
$classes = [
$package . '_' . $type . '_' . $identifier,
'Core_' . $type . '_' . $identifier,
$package . '_' . $type,
'Core_' . $type
];
foreach ($classes as $class) {
if (class_exists($class)) {
return $class;
}
}
return DataObject::class;
}
/**
* Format String
*
* @param string $string
* @return string
*/
private function format(string $string): string
{
return preg_replace('/ /', '_', ucwords(preg_replace('/_/', ' ', $string)));
}
}
app/Core/Model.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Model extends DataObject
{
public const string TYPE = 'model';
}
app/Core/Model/Form.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Model_Form extends DataObject
{
/**
* Validate form
*
* @return Core_Model_Form
*/
public function validate(): Core_Model_Form
{
$post = $this->getPost();
if (!empty($post)) {
$this->load();
if ($this->isSpam($post)) {
return $this->setError(true);
}
$this->validateFields($post) && $this->validateValues($post);
}
return $this;
}
/**
* Load
*
* @param array|null $data
* @param array $keys
* @return Core_Model_Form
*/
public function load(?array $data = null, array $keys = []): Core_Model_Form
{
$post = $data ?: $this->getPost();
if (empty($post)) {
return $this;
}
foreach ($post as $field => $value) {
if (is_array($value)) {
$this->load($value, array_merge($keys, [$field]));
continue;
}
$this->setData(join('_', array_merge($keys, [$field])), $value);
}
return $this;
}
/**
* Reset from
*
* @return Core_Model_Form
*/
public function reset(): Core_Model_Form
{
$this->setData([]);
return $this;
}
/**
* Valid form field
*
* @param array $post
* @param array|null $required
* @return bool
*/
public function validateFields(array $post, ?array $required = null): bool
{
if ($required === null) {
$required = $this->getFormRequiredFields();
}
$post = $this->flattenPostArray($post);
foreach ($required as $field) {
if (!($post[$field] ?? false)) {
$this->setError(true);
$this->setErrorFieldName($field);
$this->setErrorFieldLabel($this->getFormFields()[$field] ?? $field);
return false;
}
}
return true;
}
/**
* Valid form values
*
* @param array $post
* @param array|null $excepted
* @return bool
*/
public function validateValues(array $post, ?array $excepted = null): bool
{
if ($excepted === null) {
$excepted = $this->getFormExceptedValues();
}
$post = $this->flattenPostArray($post);
foreach ($excepted as $field => $test) {
if (!$post[$field]) {
continue;
}
$isValid = true;
if (is_array($test) && !in_array($post[$field], $test)) {
$isValid = false;
}
if (is_string($test) && !preg_match($test, $post[$field])) {
$isValid = false;
}
if (is_int($test) && !filter_var($post[$field], $test)) {
$isValid = false;
}
if (!$isValid) {
$this->setError(true);
$this->setErrorFieldName($field);
$this->setErrorFieldLabel($this->getFormFields()[$field] ?? $field);
return false;
}
}
return true;
}
/**
* Check spam, field needs to be posted without value
*
* @param array $post
* @return bool
*/
public function isSpam(array $post): bool
{
if ($this->getFormSpamField()) {
if (!isset($post[$this->getFormSpamField()])) {
return true;
}
if (!empty($post[$this->getFormSpamField()])) {
return true;
}
}
return false;
}
/**
* Send mail
*
* @return Core_Model_Form
*/
public function sendMail(): Core_Model_Form
{
if (!$this->isMailEnabled()) {
return $this;
}
if (!($this->getMailSendTo() && $this->getMailSubject())) {
return $this;
}
if (!$this->getMailMessage()) {
$this->setMailMessage($this->getDefaultMessage());
}
mail($this->getMailSendTo(), $this->getMailSubject(), $this->getMailMessage(), $this->getMailHeader());
return $this;
}
/**
* Retrieve default message when the mail message is missing
*
* @return string
*/
protected function getDefaultMessage(): string
{
$post = $this->getPost();
if (empty($post)) {
return '';
}
if ($this->getFormSpamField()) {
unset($post[$this->getFormSpamField()]);
}
$message = [];
foreach ($post as $field => $value) {
$message[] = '<strong>' . ($this->getFormFields()[$field] ?? $field) . ':</strong><br />' .
nl2br(App::escapeHtml($value));
}
return join('<br /><br />', $message);
}
/**
* Retrieve Form Header
*
* @return string
*/
public function getMailHeader(): string
{
$header = '';
if ($this->getMailFromName() && $this->getMailFromEmail()) {
$header .= 'From: "' . $this->getMailFromName() . '"<' . $this->getMailFromEmail() . '>' . "\r\n";
}
if ($this->getMailReplyTo()) {
$header .= 'Reply-To: ' . $this->getMailReplyTo() . "\r\n";
}
$header .= 'MIME-Version: 1.0' . "\r\n";
$header .= 'Content-Type: text/html; charset="UTF-8"' . "\r\n";
return $header;
}
public function isPost(): bool
{
return !empty($this->getPost());
}
public function getPost(): array
{
return $_POST;
}
public function isMailEnabled(): bool
{
return (bool)$this->getData('_mail_enabled');
}
public function getMailSubject(): string
{
return (string)$this->getData('_mail_subject');
}
public function setMailSubject(string $subject): Core_Model_Form
{
$this->setData('_mail_subject', $subject);
return $this;
}
public function getMailFromName(): string
{
return (string)$this->getData('_mail_from_name');
}
public function setMailFromName(string $fromName): Core_Model_Form
{
$this->setData('_mail_from_name', $fromName);
return $this;
}
public function getMailFromEmail(): string
{
return (string)$this->getData('_mail_from_email');
}
public function setMailFromEmail(string $fromEmail): Core_Model_Form
{
$this->setData('_mail_from_email', $fromEmail);
return $this;
}
public function getMailReplyTo(): string
{
return (string)$this->getData('_mail_reply_to');
}
public function setMailReplyTo(string $replyTo): Core_Model_Form
{
$this->setData('_mail_reply_to', $replyTo);
return $this;
}
public function getMailSendTo(): string
{
return (string)$this->getData('_mail_send_to');
}
public function setMailSendTo(string $sendTo): Core_Model_Form
{
$this->setData('_mail_send_to', $sendTo);
return $this;
}
public function getMailMessage(): string
{
return (string)$this->getData('_mail_message');
}
public function setMailMessage(string $message): Core_Model_Form
{
$this->setData('_mail_message', $message);
return $this;
}
public function getFormSpamField(): string
{
return (string)$this->getData('_form_spam_field');
}
public function setFormSpamField(string $field): Core_Model_Form
{
$this->setData('_form_spam_field', $field);
return $this;
}
public function getFormExceptedValues(): array
{
return $this->getData('_form_excepted_values') ?: [];
}
public function setFormExceptedValues(array $values): Core_Model_Form
{
$this->setData('_form_excepted_values', $values);
return $this;
}
public function getFormRequiredFields(): array
{
return $this->getData('_form_required_fields') ?: [];
}
public function setFormRequiredFields(array $fields): Core_Model_Form
{
$this->setData('_form_required_fields', $fields);
return $this;
}
public function getFormFields(): array
{
return $this->getData('_form_fields') ?: [];
}
public function setFormFields(array $fields): Core_Model_Form
{
$this->setData('_form_fields', $fields);
return $this;
}
public function getErrorFieldName(): string
{
return (string)$this->getData('_error_field_name');
}
public function setErrorFieldName(string $field): Core_Model_Form
{
$this->setData('_error_field_name', $field);
return $this;
}
public function getErrorFieldLabel(): string
{
return (string)$this->getData('_error_field_label');
}
public function setErrorFieldLabel(string $field): Core_Model_Form
{
$this->setData('_error_field_label', $field);
return $this;
}
public function getError(): bool
{
return (bool)$this->getData('_error');
}
public function setError(bool $isError): Core_Model_Form
{
$this->setData('_error', $isError);
return $this;
}
/**
* Retrieve posted fields with brackets
*
* @param array $array
* @param string $prefix
* @return array
*/
protected function flattenPostArray(array $array, string $prefix = ''): array
{
$result = [];
foreach ($array as $key => $value) {
$newKeys = [$key];
if ($prefix !== '') {
$newKeys = [$prefix . '[' . $key . ']'];
if (is_numeric($key)) {
$newKeys[] = $prefix . '[]';
}
}
foreach ($newKeys as $newKey) {
if (is_array($value)) {
$result = array_merge($result, $this->flattenPostArray($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
}
return $result;
}
}
app/Core/Model/Database.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Model_Database extends DataObject
{
private static array $pdo = [];
/**
* @throws Exception
*/
public function getConnection(): Database
{
if (!isset(self::$pdo[$this->getDbDatabase()])) {
try {
$pdo = new Database(
$this->getDbUsername(),
$this->getDbPassword(),
$this->getDbDatabase(),
$this->getDbHost(),
(int)$this->getDbPort(),
$this->getDbCharset(),
);
} catch (Exception $exception) {
App::log($exception->getMessage(), LOG_ERR);
throw new Exception('Unable to connect to database');
}
if ($this->getLcTimeNames()) {
$pdo->getConnection()->prepare('SET lc_time_names = ?')->execute([$this->getLcTimeNames()]);
}
if ($this->getTimeZone()) {
$pdo->getConnection()->prepare('SET time_zone = ?')->execute([$this->getTimeZone()]);
}
self::$pdo[$this->getDbDatabase()] = $pdo;
}
return self::$pdo[$this->getDbDatabase()];
}
public function getDbHost(): string
{
return (string)$this->getData('db_host');
}
public function getDbUsername(): string
{
return (string)$this->getData('db_username');
}
public function getDbPassword(): string
{
return (string)$this->getData('db_password');
}
public function getDbCharset(): string
{
return (string)$this->getData('db_charset');
}
public function getDbDatabase(): string
{
return (string)$this->getData('db_database');
}
public function getDbPort(): string
{
return (string)$this->getData('db_port');
}
public function getLcTimeNames(): string
{
return (string)$this->getData('lc_time_names');
}
public function getTimeZone(): string
{
return (string)$this->getData('time_zone');
}
}
app/Core/Mail.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Mail extends Core_Template_Abstract
{
public const string TYPE = 'mail';
}
app/Core/Helper.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Helper extends DataObject
{
public const string TYPE = 'helper';
}
app/Core/Console/Interface.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
interface Core_Console_Interface
{
public const string TYPE = 'console';
public const int SUCCESS = 0;
public const int ERROR = 1;
public function run(array $args): int;
}
app/Core/Block.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
class Core_Block extends Core_Template_Abstract
{
public const string TYPE = 'block';
}
App.php
<?php
/**
* Copyright (C) 2025 MageWork by Magentix
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
const DS = DIRECTORY_SEPARATOR;
const US = '/';
final class App
{
public const string VERSION = '2.3.1';
public const string CONFIG_DIR = 'etc';
public const string PACKAGES_DIR = 'packages';
public const string PUBLIC_DIR = 'pub';
public const string PUBLIC_ASSETS_DIR = 'assets';
public const string VAR_DIR = 'var';
public const string CACHE_DIR = self::VAR_DIR . '/cache';
public const string LOG_DIR = self::VAR_DIR . '/log';
public const string ENCRYPTION_DIR = self::VAR_DIR . '/encryption';
public const string SESSION_DIR = self::VAR_DIR . '/session';
protected static array $singleton = [];
protected static ?Core_Object_Factory $factory = null;
protected static ?string $rootDir = null;
protected static ?string $baseUrl = null;
protected static ?array $packages = null;
protected static ?string $package = null;
protected static ?string $route = null;
protected static ?string $environment = null;
protected static string $packagePrefix = '';
protected static ?Core_Page $page = null;
protected static ?Session $session = null;
protected static ?Config $config = null;
protected static ?Cache $cache = null;
protected static ?Encryption $encryption = null;
protected static ?Logger $logger = null;
protected static ?escaper $escaper = null;
/**
* Run application
*
* @return void
*/
public static function run(): void
{
try {
echo self::getPage(self::getRequestUri());
} catch (Exception $e) {
echo $e->getMessage();
}
}
/**
* Execute a command
*
* @param string $identifier
* @param string $package
* @param array $args
* @return int
* @throws Exception
*/
public static function exec(string $identifier, string $package, array $args = []): int
{
self::setPackage($package);
/** @var Core_Console_Interface $object */
$object = self::getObject($identifier, Core_Console_Interface::TYPE);
if (!$object instanceof Core_Console_Interface) {
throw new Exception('"' . $identifier . '" object does not exist or is not a Core_Console_Interface.');
}
return $object->run($args);
}
/**
* Retrieve the cache
*
* @return Cache|null
*/
public static function cache(): ?Cache
{
try {
if (self::$cache === null) {
self::createVarDirectory();
self::$cache = new Cache(self::getBaseDir(self::CACHE_DIR), 'app.cache')->setLifetime(86400);
}
return self::$cache;
} catch (Exception) {
return null;
}
}
/**
* Log data
*
* @param mixed $message
* @param int $level
* @return void
*/
public static function log(mixed $message, int $level = Logger::INFO): void
{
try {
if (self::$logger === null) {
self::createVarDirectory();
self::$logger = new Logger(self::getBaseDir(self::LOG_DIR));
}
self::$logger->add($message, $level);
} catch (Exception) {
return;
}
}
/**
* Retrieve Session
*
* @param bool $init
* @return Session|null
*/
public static function session(bool $init = true): ?Session
{
try {
if (!$init && isset($_COOKIE[self::getPackage()])) {
$init = true;
}
if ($init && self::$session === null) {
self::createVarDirectory();
$session = new Session(self::getBaseDir(self::SESSION_DIR), self::getPackage());
$session->init(
(int)self::getConfig('app.session_lifetime', 0),
self::isSsl(),
(bool)self::getConfig('app.cookie_http_only', true),
(string)self::getConfig('app.cookie_same_site', 'Lax'),
);
self::$session = $session;
}
return self::$session;
} catch (Exception) {
return null;
}
}
/**
* Retrieve Encryption
*
* @return Encryption|null
*/
public static function encryption(): ?Encryption
{
try {
if (self::$encryption === null) {
self::createVarDirectory();
$encryption = new Encryption(self::getBaseDir(self::ENCRYPTION_DIR));
self::$encryption = $encryption;
}
return self::$encryption;
} catch (Exception) {
return null;
}
}
/**
* Retrieve connection to database
*
* @param string $model
* @return Database
*/
public static function db(string $model = 'database'): Database
{
try {
/** @var Core_Model_Database $object */
$object = self::getSingleton($model, Core_Model::TYPE);
return $object->getConnection();
} catch (Exception) {
exit('Database connection error');
}
}
/**
* Retrieve current page object
*
* @return Core_Page|null
*/
public static function page(): ?Core_Page
{
return self::$page;
}
/**
* Retrieve current route
*
* @çeturn string
*/
public static function getRoute(): string
{
return self::$route;
}
/**
* Retrieve all page routes
*
* @param string|null $package
* @return array
*/
public static function getAllRoutes(?string $package = null): array
{
if ($package === null) {
$package = self::getPackage();
}
return array_merge(
array_keys(self::getConfig('default.' . Core_Page::TYPE) ?? []),
array_keys(self::getConfig($package . '.' . Core_Page::TYPE) ?? []),
);
}
/**
* Retrieve object singleton instance
*
* @param string|null $identifier
* @param string|null $type
* @param string|null $package
* @return object
*/
public static function getSingleton(?string $identifier = null, ?string $type = null, ?string $package = null): object
{
$key = join('_', array_filter([$identifier, $type, $package]));
if (!isset(self::$singleton[$key])) {
self::$singleton[$key] = self::getObject($identifier, $type, $package);
}
return self::$singleton[$key];
}
/**
* Retrieve object
*
* @param string|null $identifier
* @param string|null $type
* @param string|null $package
* @return object
*/
public static function getObject(?string $identifier = null, ?string $type = null, ?string $package = null): object
{
return self::getObjectFactory()->getObject($type, $identifier, $package);
}
/**
* Retrieve an URL
*
* @param string $path
* @param array $params
* @param bool $prefix
* @return string
*/
public static function getUrl(string $path = '', array $params = [], bool $prefix = false): string
{
$fragments = [
$prefix && self::$packagePrefix ? self::$packagePrefix : null,
$path ? self::normalizePath($path, US) : null,
];
$query = !empty($params) ? '?' . http_build_query($params, '', '&') : '';
return self::getBaseUrl() . join(US, array_filter($fragments)) . $query;
}
/**
* Retrieve base URL
*
* @return string
*/
public static function getBaseUrl(): string
{
return self::$baseUrl ?? ((self::isSsl() ? 'https://' : 'http://') . self::getHost() . US);
}
/**
* Retrieve asset URL path
*
* @param string $path
* @param string|null $package
* @return string
*/
public static function getAssetUrl(string $path = '', ?string $package = null): string
{
if ($package === null) {
$package = self::getPackage();
}
return self::getUrl(join(US, array_filter([self::PUBLIC_ASSETS_DIR, $package, $path])));
}
/**
* Retrieve an asset file path from the package asset root
*
* @param string $path
* @param string|null $package
* @return string
*/
public static function getAssetPath(string $path = '', ?string $package = null): string
{
if ($package === null) {
$package = self::getPackage();
}
return self::getBasePath(join(DS, array_filter([self::PUBLIC_DIR, self::PUBLIC_ASSETS_DIR, $package, $path])));
}
/**
* Retrieve an absolute base directory path
*
* @param string $path
* @return string
*/
public static function getBasePath(string $path = ''): string
{
return rtrim(self::getBaseDir($path), DS);
}
/**
* Retrieve the absolute base directory with a directory separator at the end
*
* @param string $path
* @return string
*/
public static function getBaseDir(string $path = ''): string
{
$fragments = [
rtrim(self::$rootDir ?? $_SERVER['DOCUMENT_ROOT'], DS),
self::normalizePath($path),
];
return join(DS, array_filter($fragments)) . DS;
}
/**
* Set Root Directory
*
* @param $rootDir
* @return void
*/
public static function setRootDir($rootDir): void
{
self::$rootDir = rtrim($rootDir, '\\/') . DS;
}
/**
* Retrieve package path
*
* @param string $path
* @param string|null $package
* @return string
*/
public static function getPackagePath(string $path = '', ?string $package = null): string
{
return rtrim(self::getPackageDir($path, $package), DS);
}
/**
* Retrieve package directory with directory separator at the end
*
* @param string $path
* @param string|null $package
* @return string
*/
public static function getPackageDir(string $path = '', ?string $package = null): string
{
if ($package === null) {
$package = self::getPackage();
}
$fragments = [
self::getPackagesDir() . $package,
self::normalizePath($path)
];
return join(DS, array_filter($fragments)) . DS;
}
/**
* Retrieve directory path containing packages
*
* @return string
*/
public static function getPackagesDir(): string
{
return self::getBaseDir(self::PACKAGES_DIR);
}
/**
* Retrieve all current packages in directory
*
* @return array
*/
public static function getPackages(): array
{
if (self::$packages === null) {
$directories = self::getDirectories(self::getPackagesDir());
foreach ($directories as $package) {
self::$packages[] = $package;
}
}
return self::$packages ?: [];
}
/**
* Retrieve all directory names in a folder
*
* @param string $root
* @return array
*/
public static function getDirectories(string $root): array
{
$directories = [];
$root = rtrim($root, DS) . DS;
if (is_dir($root)) {
$children = scandir($root, 1);
foreach ($children as $child) {
if ($child != '.' && $child != '..' && is_dir($root . $child)) {
$directories[] = $child;
}
}
}
return $directories;
}
/**
* Retrieve current package
*
* @return string
*/
public static function getPackage(): string
{
return (string)self::$package;
}
/**
* Retrieve config value
*
* @param string|null $path
* @param mixed $default
* @return mixed
*/
public static function getConfig(?string $path = null, mixed $default = null): mixed
{
if (self::$config === null) {
self::loadConfig();
}
$value = self::$config->get($path);
return $value === null ? $default : $value;
}
/**
* Is developer mode
*
* @return bool
*/
public static function isDeveloperMode(): bool
{
return (bool)(getenv('MW_DEVELOPER_MODE') ?: false);
}
/**
* Retrieve environment
*
* @return string
*/
public static function getEnvironment(): string
{
if (self::$environment === null) {
self::$environment = getenv('MW_ENVIRONMENT') ?: 'default';
}
return self::$environment;
}
/**
* Set Environment
*
* @param string $environment
* @return void
*/
public static function setEnvironment(string $environment): void
{
self::$environment = $environment;
}
/**
* Retrieve request Uri
*
* @return string
*/
public static function getRequestUri(): string
{
return (string)($_SERVER['REQUEST_URI'] ?? US) ?: US;
}
/**
* Retrieve Server port
*
* @return string
*/
public static function getServerPort(): string
{
return (string)($_SERVER['SERVER_PORT'] ?? '');
}
/**
* SSL is active
*
* @return bool
*/
public static function isSsl(): bool
{
return self::getServerPort() === (string)self::getConfig('app.secured_port', '443');
}
/**
* Retrieve Server name
*
* @return string
*/
public static function getHost(): string
{
$host = getenv('MW_WEBSITE') ?: null;
if ($host === null) {
$host = $_SERVER['HTTP_HOST'];
}
return (string)$host;
}
/**
* Retrieve Escaper
*
* @return Escaper
*/
public static function escaper(): Escaper
{
if (self::$escaper === null) {
self::$escaper = new Escaper();
}
return self::$escaper;
}
/**
* Escape to HTML
*
* @param mixed $value
* @return string
*/
public static function escapeHtml(mixed $value): string
{
return self::escaper()->escapeHtml((string)$value);
}
/**
* Escape HTML attribute
*
* @param mixed $value
* @return string
*/
public static function escapeHtmlAttr(mixed $value): string
{
try {
return self::escaper()->escapeHtmlAttr((string)$value);
} catch (Exception) {
return '';
}
}
/**
* Escape URL
*
* @param string $value
* @return string
*/
public static function escapeUrl(string $value): string
{
return self::escaper()->escapeUrl($value);
}
/**
* Retrieve page
*
* @param string $route
* @return string
* @throws Exception
*/
public static function getPage(string $route): string
{
$isDir = str_ends_with(self::removeQueryParameters($route), US);
$identifier = self::configure($route);
self::readAssetFile($identifier);
$routes = App::getAllRoutes();
if ($isDir && !in_array(rtrim($identifier, US) . US, $routes)) {
$identifier = '404';
} else {
if (!in_array($identifier, $routes)) {
$identifier .= US;
}
if (!in_array($identifier, $routes)) {
$identifier = '404';
}
}
/** @var Core_Page $page */
$page = self::getObject($identifier, Core_Page::TYPE);
if (!self::isCli()) {
if ($page->getData('_http_code')) {
http_response_code((int)$page->getData('_http_code'));
}
foreach ($page->getData('_headers') ?? [] as $header => $value) {
header($header . ': ' . $value);
}
}
self::$page = $page;
if (method_exists($page, 'render')) {
return $page->render();
}
return '';
}
/**
* Set Base URL
*
* @param string $baseUrl
* @return void
*/
public static function setBaseUrl(string $baseUrl): void
{
self::$baseUrl = rtrim($baseUrl, '\\/') . US;
}
/**
* Set package
*
* @param string $package
* @return void
* @throws Exception
*/
public static function setPackage(string $package): void
{
$packages = self::getPackages();
if (!in_array($package, $packages)) {
throw new Exception(
'Package "' . $package . '" does not exist. Available package(s): ' . implode(', ', $packages) . '.'
);
}
self::$package = $package;
}
/**
* Is CLI mode
*
* @return bool
*/
public static function isCli(): bool
{
return php_sapi_name() === 'cli';
}
/**
* Normalize a path
*
* @param string $path
* @param string $s
* @return string
*/
public static function normalizePath(string $path, string $s = DS): string
{
return trim(str_replace('..' . $s, '', preg_replace('#[/\\\]+#', $s, rawurldecode(rawurldecode($path)))), $s);
}
/**
* Create var directory if empty
*
* @return bool
*/
public static function createVarDirectory(): bool
{
$var = self::getBaseDir(self::VAR_DIR);
if (!is_dir($var)) {
return mkdir($var, 0755, true);
}
return true;
}
/**
* Allow to use external libraries
*
* @return void
*/
protected static function composer(): void
{
$autoload = self::getBasePath('vendor' . DS . 'autoload.php');
if (is_file($autoload)) {
require $autoload;
}
}
/**
* Set current package from URI (if needed) and final route path
*
* @param string $route
* @return string
* @throws Exception
*/
protected static function configure(string $route): string
{
self::composer();
$route = self::cleanRoute($route);
if (!App::getPackage()) {
$packages = self::getConfig('packages.' . self::getHost()) ?? [];
foreach ($packages as $package => $prefix) {
if (!preg_match('/^[A-Z]/', (string)$package)) {
throw new Exception('Package "' . $package . '" must start with a capital letter');
}
$search = preg_quote((string)$prefix, '/');
if (!$search) {
self::setPackage($package);
}
if ($search &&
(preg_match('/^\/' . $search . '\//', $route) || preg_match('/^\/' . $search . '$/', $route)))
{
self::$packagePrefix = (string)$prefix;
self::setPackage($package);
$route = preg_replace('/^\/' . $search . '/', '', $route) ?: '/';
break;
}
}
}
if (!App::getPackage()) {
throw new Exception('No package found');
}
if (!is_dir(self::getPackageDir('', App::getPackage()))) {
throw new Exception('Package "' . App::getPackage() . '" does not exist');
}
return self::setRoute($route);
}
/**
* Set current route and check for rewrites
*
* @param string $route
* @return string
*/
protected static function setRoute(string $route): string
{
self::$route = self::cleanRoute($route);
$rewrite = self::getObject('rewrite', Core_Model::TYPE);
if ($rewrite->getData('route')) {
self::$route = self::cleanRoute($rewrite->getData('route'));
}
return self::$route;
}
/**
* Clean a route
*
* @param string $route
* @return string
*/
protected static function cleanRoute(string $route): string
{
return rtrim(self::removeQueryParameters($route), US) ?: US;
}
/**
* Remove all parameters in route
*
* @param string $route
* @return string
*/
protected static function removeQueryParameters(string $route): string
{
preg_match('/\\?(?P<params>.*)/', $route, $params);
if (isset($params['params'])) {
$route = preg_replace('/\\?.*/', '', $route);
}
return $route;
}
/**
* Reads the asset file if the request matches an existing asset file for the package
*
* @param string $path
* @return void
*/
protected static function readAssetFile(string $path): void
{
$assetFile = self::getAssetPath($path);
if (!str_starts_with(basename($assetFile), '.') && is_file($assetFile)) {
if (!str_ends_with($assetFile, '.html') && extension_loaded('fileinfo')) {
header('Content-Type: ' . mime_content_type($assetFile));
}
readfile($assetFile);
exit;
}
}
/**
* Retrieve object factory class
*
* @return Core_Object_Factory
*/
protected static function getObjectFactory(): Core_Object_Factory
{
if (self::$factory === null) {
self::$factory = new Core_Object_Factory();
}
return self::$factory;
}
/**
* Load Config
*
* @return Config
*/
protected static function loadConfig(): Config
{
$config = new Config();
$files = [
self::getBasePath(self::CONFIG_DIR . DS . 'config.' . self::getEnvironment() . '.php'),
];
$configurations = [];
$packages = self::getPackages();
foreach ($packages as $package) {
$directory = self::getPackageDir(self::CONFIG_DIR, $package);
if (!is_dir($directory)) {
continue;
}
$configFiles = scandir($directory, 1);
foreach ($configFiles as $configFile) {
if (str_ends_with($configFile, '.php') && is_file($directory . $configFile)) {
$configuration = $config->loadFile($directory . $configFile);
$configurations[] = [$package => $configuration];
}
}
}
$config->setFiles($files);
$config->setConfigurations($configurations);
$config->load();
self::$config = $config;
return self::$config;
}
/**
* Autoload
*
* @param string $class
* @return void
*/
public static function autoload(string $class): void
{
if (preg_match('/\\\\/', $class)) {
$class = join(DS, explode('\\', $class));
} else {
$class = join(DS, explode('_', $class));
}
$classes = [
self::getBasePath('core' . DS . 'app'. DS . $class . '.php'),
self::getBasePath('core' . DS . 'lib'. DS . $class . '.php'),
self::getBasePath('lib'. DS . $class . '.php'),
self::getBasePath(self::PACKAGES_DIR . DS . $class . '.php'),
];
foreach ($classes as $file) {
if (is_file($file)) {
require_once $file;
}
}
}
}
spl_autoload_register(['App', 'autoload']);