Extension

Adds features to the core application.

Table of Contents
  1. Structure
  2. Files
    1. .gitattributes
    2. .gitignore
    3. .htaccess
    4. LICENSE
    5. README.md
    6. about.page
    7. composer.json
    8. index.css
    9. index.js
    10. index.php
    11. index.png
    12. package.json
    13. state.php
    14. task.php
  3. Folders
    1. engine\
    2. index\
    3. state\

An extension doesn’t necessarily have to modify the core functionality of your application. It can also serve as an API enhancer that does nothing until it’s implemented in a specific layout and utilized by other extensions that depend on it.

Structure

Each extension is stored as a folder within the .\lot\x directory, with a distinct name:

.\
└── lot\
    └── x\
        ├── extension-1\
        ├── extension-2\
        ├── extension-3\
        └── …

The following files are typically included in a standard extension:

.\lot\x\extension-1\
├── engine\
│   ├── kernel\
│   │   └── extension-1.php
│   └── plug\
│       └── extension-1.php
├── .gitattributes
├── .gitignore
├── .htaccess
├── LICENSE
├── README.md
├── about.page
├── composer.json
├── index.css
├── index.js
├── index.php
├── index.png
├── package.json
├── state.php
└── task.php

Alternatively, if you prefer a minimalist approach, you can create an extension that consists solely of the index.php file, as demonstrated in this example:

.\lot\x\extension-1\
└── index.php

Files

.gitattributes

You can add this file to specify which files or folders should be excluded from the archive content when downloading a repository for production. Typically, this file contains a list of commands, such as the following example, which excludes the .gitignore, README.md, and the .gitattributes file itself:

.gitattributes export-ignore
.gitignore export-ignore
README.md export-ignore

.gitignore

This Git-specific file excludes certain files/folders from being committed to the repository, preventing accidental inclusion. However, it should be removed in the production version. Certain package files are also excluded, as they should be freshly installed using CLI commands rather than included in the repository for others to clone:

composer.lock
node_modules
package-lock.json
vendor

.htaccess

You can use this file to enable direct access to files with specific extensions within the folder. Keep in mind that for security reasons, all files in the .\lot folder are not directly accessible by default. Here’s an example Apache server command that allows direct access to files with .css, .js, and .png extensions:

<Files ~ '[.](css|js|png)$'>
  Allow from all
</Files>

To improve readability, consider using the <FilesMatch> directive. Use the <Files> directive only to specify exact file names. Keep in mind that this approach can result in lengthy and cumbersome directives:

<Files 'index.css'>
  Allow from all
</Files>

<Files 'index.html'>
  Allow from all
</Files>

<Files 'index.js'>
  Allow from all
</Files>

<FilesMatch '^index\.(a?png|gif|jpe?g)$'>
  Allow from all
</FilesMatch>

LICENSE

Mecha is GPL-3.0 licensed, so include a paragraph in this file that specifies the same license.

README.md

This file isn’t essential to the extension but may be necessary for the repository to appear complete on platforms like GitHub and GitLab. Since it’s ignored in the .gitattributes file, it won’t be included in the archive.

about.page

This file provides information that appears in the control panel’s information page and functions similarly to the README.md file. It’s designed to be read by the official control panel feature:

---
title: Extension Title
description: Extension description.
type: Markdown
author: Your Name
link: 'http://example.com'
version: 1.0.0
...

Extension usage instruction, written with Markdown syntax.

Below are commonly used about.page properties and their priority:

NamePriorityUsage
author2Determines the extension’s author.
color3Determines the extension’s icon background color. This data is used exclusively by the control panel extension. Other extensions can use this data for other purposes or can simply ignore it.
description2Determines the extension’s description.
icon2Determines the extension’s icon path. This data is used exclusively by the control panel extension. Other extensions can use this data for other purposes or can simply ignore it.
image2Determines the extension’s image preview.
images2Determines the extension’s image previews, usually to be presented in a carousel.
link2Determines the extension’s related link.
price2Determines the extension’s default price.
prices2Determines the extension’s price or prices details.
stroke4Determines the extension’s icon color. This data is used exclusively by the control panel extension. Other extensions can use this data for other purposes or can simply ignore it.
title1Determines the extension’s title.
type2Determines the page content’s type.
version1Determines the extension’s version. Without this property, the control panel’s automatic extension version check feature might not apply to the extension.

External extensions can access this file like a regular page file:

$page = new Page(__DIR__ . D . 'x' . D . 'extension-name' . D . 'about.page');

echo '<h2>' . $page->title . '</h2>';
echo '<p>' . $page->description . '</p>';
echo '<div>' . $page->content . '</div>';

composer.json

Add this file to your extension repository to publish it on Packagist and enable installation via Composer commands. Ensure that the package name follows the correct pattern for our Composer plugin to install it correctly in the extension folder:

{vendor}/x.{folder}

Below is an example of a composer.json file for your extension repository, assuming it’s already published on GitHub at https://github.com/your-name/x.your-extension-name:

{
  "description": "Your extension description.",
  "name": "your-name/x.your-extension-name"
}

To make your extension available for installation via Composer, you’ll need to create a stable version release in your repository and publish it on Packagist. You can do this by visiting https://packagist.org/packages/submit and following the instructions. Once your package is successfully published, others can install your extension using the following Composer command:

composer require your-name/x.your-extension-name

Your new extension should be installed in the .\lot\x\your-extension-name location smoothly.

index.css

This is a regular CSS file that is loaded in the extension. While it doesn’t have to be named index.css, I recommend that you use this name as it pairs neatly with index.php. Once you create the file, ensure that it is publicly accessible by including it in the .htaccess file, and load it using the Asset extension in the index.php file.

index.js

If you have an index.css file, chances are you will also need to include this file. The rule for using and loading it is the same as the index.css file.

index.php

This file serves as the main entry point for an extension, and is automatically loaded each time the user accesses the application. You can also use this file to load any other files that you want to include in the extension:

<?php

// Load `index.css` and `index.js` files to the page
if (class_exists('Asset')) {
    Asset::set(__DIR__ . D . 'index.css');
    Asset::set(__DIR__ . D . 'index.js');
}

index.png

You can use this image to give a quick demonstration of how your extension would look like. The actual name of the image is not specific. In order to display this image in the extension information page, you need to manually register it in the image property of the about.page file. I suggest naming the image index.png as it pairs well with index.php:

---
title: Extension Title
description: Extension description.
image: /lot/x/extension-name/index.png
type: Markdown
...

Review the `.htaccess` file specification if the image is not displaying on the extension information page.

Typically, standard image dimensions for extension demonstration purposes have a 4:3 aspect ratio, and can be of any size, though commonly made with a width and height of 1024×768 or 1200×900 pixels. Additionally, you may also use other image extensions such as .gif and .jpg if necessary.

Image

package.json

This file is used exclusively by NPM and does not need to be stored in the extension folder. It is necessary to include this file in order to add build tools for generating the index.css and index.js files from external NPM modules. Since Mecha’s extension is not designed to be published on NPM, you don’t need to provide a package name. This file is used to develop CSS and JavaScript code locally. It will be removed in the production version:

{
  "devDependencies": {
    "@taufik-nurrohman/factory": "*"
  },
  "private": true,
  "scripts": {
    "pack": "pack --clean=false --from=.factory --js-format=iife --to=."
  },
  "type": "module"
}

state.php

This file should return an array that can be used by the extension itself and other extensions that depend on it. The data in the array will be automatically loaded as $state->x->{'extension-name'} object. If this file doesn’t exist, the $state->x->{'extension-name'} object will return an empty array:

<?php

return [
    'active' => true,
    'type' => 2
];

The extension object is useful not only for configuring extension options but also for verifying if a particular extension is installed before activating certain features. Mecha does not have any specific method to check for extension availability, except by using this object or by verifying the existence of specific classes, files, and/or functions:

<?php

if (!extension_loaded('gd')) {
    abort(i('Missing %s extension.', ['PHP <a href="https://www.php.net/book.image" rel="nofollow" target="_blank">gd</a>']));
}

if (!isset($state->x->asset)) {
    abort(i('Missing %s extension.', ['<a href="https://github.com/mecha-cms/x.asset">Asset</a>']));
}

if (empty($state->x->{'extension-name'}->active)) {
    // Extension is disabled by the author via `state.php` file
    return;
}

$type = $state->x->{'extension-name'}->type ?? 1; // Set default extension type to `1`
if (1 === $type) {
    // Do something if `type` value is `1`
} else if (2 === $type) {
    // Do something if `type` value is `2`
} else {
    abort(i('Invalid %s value.', ['<code>type</code>']));
}

task.php

This optional file is loaded before the index.php file and allows you to perform specific tasks such as automatic file repair before the extension is loaded. Once all the tasks are completed, you can add a command to delete the file:

<?php

$done = 0;
$content = <<<PAGE
---
title: Example
description: Edit this page.
type: Markdown
...

You can delete or edit this page from the control panel.
PAGE;

if (!is_dir($folder = LOT . D . 'page' . D . 'store')) {
    mkdir($folder, 775, true);
} else {
    if (!is_file($file = $folder . D . 'example.page')) {
        file_put_contents($file, $content);
    } else {
        ++$done;
    }
    ++$done;
}

if (2 === $done) {
    unlink(__FILE__); // Self destruct!
}

Folders

engine\

It usually contains two other folders named kernel and plug. Store your application classes in the kernel folder so you don’t have to load them manually. Classes stored in this folder will be loaded automatically as long as you follow the class naming conventions. You can add custom tasks that need to be executed once a class is loaded inside the plug folder. If additional files specific to the classes are available then files with the same name as the class name will be loaded automatically:

.\lot\x\extension-1\
└── engine\
    ├── kernel\
    │   └── extension-1.php
    └── plug\
        ├── anemone.php
        └── extension-1.php

In the above extension file structure, the .\lot\x\extension-1\engine\kernel\extension-1.php file must contain the definition of the Extension1 class. Once you declare the Extension1 class somewhere, this file will be loaded automatically. Additionally, the .\lot\x\extension-1\engine\plug\extension-1.php file will also be loaded since the class has never been loaded before.

The file .\lot\x\extension-1\engine\plug\anemone.php will also be automatically loaded since it shares the same name as the Anemone core class. You can use this file to add custom methods and extend the features of the Anemone class.

index\

This folder can be used to store files related to other extensions, such as the panel.php file, which Panel extension can find and load for use. The name of this folder comes after index.php to make it look neat when paired up, but it is non-standard and not detected by any special feature in the core application. Currently, only Panel extension considers the existence of this folder.

state\

The recursive state data for an application can be stored in this folder to improve manageability. By keeping it separate, it becomes easier to load the state data based on specific conditions or requirements. This approach can help simplify the code and make it more organized.

Asset

Manages asset files without touching the layout files.

Layout

Common automation features to speed up the site creation process.

Link

Resolves relative links in content.

Page

Converts file and folder structure into web pages.