Festi Plugin Architecture
Any system written on Festi Framework contains a variety of plugins. A plugin contains the business logic.
Plugin Structure
Plugins are located in the plugins
folder by default.
There are two ways to create a plugin.
The first way is to simply create a file in the plugins
folder with a class describing the plugin.
Each plugin class has a mandatory suffix in its name, by default Plugin
,
but the suffix can be overridden, and it must inherit from AbstractPlugin
, ApiPlugin
, ObjectPlugin
, DisplayPlugin
.
It is recommended to use DisplayPlugin
.
The simplified form of a plugin will look like this:
plugins/MyPlugin.php
:
class MyPlugin extends DisplayPlugin
{
}
The second, more commonly used way is to create a full plugin. It is created in a folder that corresponds to the plugin name without the postfix. In this folder, a class is created as in the simplified version. Here is how the structure usually looks:
My/
|-- domain/
| |-- model/
| |-- service/
|-- static/
|-- templates/
|-- MyPlugin.php *
|-- MyObject.php
|-- init.php
In the static
folder, you need to place various JS, CSS, and image files.
The system always has a templates
folder, but you can also create it in the plugin and place templates
that your plugin needs in it. The system will look for a template in this folder first, and if it doesn't find it,
it will go to the root templates
folder. In the domain
folder we can move some logic from plugin.
MyObject.php
is an object (DataAccessObject) for working with the database, which you can read about in another section.
Only plugin should have access to his object.
To connect JS and CSS, you need to use the includeStatic
method.
class MyPlugin extends DisplayPlugin
{
public function onDisplayDefault(Response &$response)
{
$this->includeStatic('test.js');
$this->includeStatic('test.css');
...
}
}
init.php
init.php
is an optional file, but if you put it in the plugin folder,
it will be called when the Core
is initialized in the system.
Formally, this file should be used to initialize some actions for the correct operation of the plugin.
For example, if your plugin depends on events of another plugin or you need to make sure that the system is configured correctly for the plugin to work.
This file is in the Core
scope, meaning that if you call $this
inside the file, you will get access to the Core
instance.
init.php
:
$plugin = $this->getPluginInstance("Jimbo");
Accessing Plugin Instances
There are several ways to get an instance of a plugin:
-
Get it through the
Core
:$plugin = Core::getInstance()->getPluginInstance("My");
-
If you need to get an instance of a plugin from another plugin, you can use
$this->plugin
:$this->plugin->my->getValues(); $plugin = $this->plugin->my;
You cannot create an instance of a plugin manually through the constructor!
System Plugin
Every project should have one system plugin, which differs from the ordinary one in that it should implement additional logic and define a set of rules in the system, such as:
- getting system settings;
- handling system installation and update;
- defining routing rules;
- defining response serialization rules;
- etc.
The system plugin should be implemented from the ISystemPlugin
interface and
its database object from the ISystemObject
interface.
In most cases, the Jimbo
plugin is used for web projects.
And most importantly, the system plugin must be explicitly specified in the Core
:
$core = Core::getInstance();
$systemPlugin = $core->getPluginInstance('Jimbo');
$core->setSystemPlugin($systemPlugin);
$systemPlugin->bindRequest();
The bindRequest
method is the entry point for request processing in any system,
but only if you use Festi Framework as the main tool, not as a supporting one.
Plugin Metadata Annotations
It is not necessary, but we recommend describing its parameters in the header of the plugin class:
/**
* Plugin description
*
* @pluginName PluginName
* @pluginUrl UrlToPluginSite
* @pluginSource UrlToPluginSource
* @pluginDependency DependencyPluginName <DependencyPluginSource>
*/
class MyPlugin extends ObjectPlugin
{
}
Method Naming Conventions for Routing
At the moment, the Jimbo
system plugin supports the following prefixes in methods:
onDisplay*
doAjaxResponse*
onAjax*
doJsonResponse*
onJson*
If you use these prefixes when routing requests,
an instance of the Response
object will automatically be passed as the first parameter with the already configured type of processing and
serialization of the response.
For more information about Response, see the section on Routing Requests.
Annotations for Plugin Methods
Methods in plugins have a number of additional annotations that make it easier to use the system and hide some of the system logic. Here is an example of all the annotations that the system supports:
/**
* Plugin description
*
* @pluginName PluginName
* @pluginUrl UrlToPluginSite
* @pluginSource UrlToPluginSource
* @pluginDependency DependencyPluginName <DependencyPluginSource>
* @pluginDependency DependencyPluginName2 <DependencyPluginSource2>
* @pluginVersion rc1.1
*/
class YourPlugin extends ObjectPlugin
{
/**
* @urlRule ~/test/([0-9])/~
* @section sectionName|sectionMask
* @area default
* @userType admin
* @interceptor ClassName
* @listener PluginName::methodName
* @prepare PluginName::methodName
*/
public function method($param)
{
}
/**
* @event Store::EVENT_BEFORE_INSERT
*/
public function onCreateNewItem(FestiEvent &$event)
{
}
}
-
urlRule
- describes the regular expression for which URL this method will be called. -
section
- the name of the section in which the permissions are defined for who can call this method. -
area
- from which zone this method can be called. sectionMask = (read|write|exec) -
userType
- which type of user can call this method, userType is related to section. -
interceptor
- the name of the interceptor class that will be called before and after calling this method. -
listener
- specifies the callback method that should be called after the current method is executed. -
prepare
- specifies the callback method that should be called before the current method is executed.
It is important to note that these annotations do not work "on the fly", if you have defined them for a method, you need to update the system plugin to be continued.
Plugin Lifecycle
- Plugin initialization occurs on the first call to the
getPluginInstance
method in theCore
. -
When the plugin is initialized, the
onInit
method is always called in it. If you need to add logic for the plugin during initialization, do not use the constructor, but use the overload of theonInit
method.class YourPlugin extends DisplayPlugin { public function onInit() { parent::onInit(); // Ваш код } }
-
The plugin object instance lives as a singleton and each of your calls corresponds to the same plugin object instance, regardless of whether you called it through a
Core
or through the plugin object itself.
Unit Tests
If you are developing a system or a plugin that will be used in different projects and has a separate repository, then you need to create a "tests" folder for it and add tests for its functionality.
Tests for the plugin are run only with the main project.
- Add to bootstrap.php:
require_once 'tests/FestiTestCase.php';
require_once 'test/PluginTestCase.php';
require_once 'tests/TestUtils.php';
...
TestUtils::execPlugins();
- Add at least one class with tests
[PLUGIN_NAME]PluginTest.php
:
class [PLUGIN_NAME]PluginTest extends PluginTestCase
{
public function testLogic()
{
...
}
}
Usage Examples
Calling a plugin method with passing parameters by reference
For example, we have a method in the Test
plugin:
public function onModuleConfig(&$groups)
{
...
}
And if we make a call from another plugin:
$this->plugin->$pluginName->onModuleConfig($groups);
then $groups
will be passed by reference, and if changes are made to $groups
in
onModuleConfig
, they will be applied.