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
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
In this 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->getBaseUrl($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->getBaseUrl('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($this->getBaseUrl('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="javascript: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!');
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::getTemplateDir() . $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',
],
],
];