Table of Contents
- Quick Start
- Introduction
- Creating your first plugin
- More plugin examples
- Considerations
- Plugin API Reference
- Core API Notes
Can’t wait to start creating a plugin? Read on for a very quick walkthrough on how to put together a simple plugin in just a few minutes:
-
Make sure you have a working eFront installation, and you have access to its files (with permission to change them). See Installation for instructions on how to install eFront.
-
Visit your eFront system, sign in as an administrator and click on Plugins.
-
Click on the Create plugin button. Enter a name (Spaces and special or international characters are not allowed for the plugin name), title and description for your plugin, for example:
-
name: AcmeReports
-
title: Acme Reports
-
description: This is a fully-functional plugin that is created as part of eFront plugin guide.
Now click on Save. You will be redirected to the new plugin’s main page (which is empty). The “Acme reports” plugin will be visible in the plugins list, where you can deactivate or delete it.
-
-
The plugin has been created inside the server that hosts eFront. You can find its files at the same location where eFront is installed on the disk, under the path www/plugins/AcmeReports. You have to gain access to these files, in order to change them according to your needs. You can either:
-
Access the files directly on the server, if you have access, and edit them directly.
-
Under admin→plugins, click on the download link next to the “AcmeReports” entry, to download a zip file containing your files. You can then change them at your local environment; once you’re done, compress them into a zip file and click on the upgrade icon next to the entry.
-
Read on for a step-by-step walkthrough on how to create a general-purpose plugin.
Creating a plugin can be a simple or complicated process, based on your requirements. We’ll try to make your life as easy as possible, regardless of whether you’re an experienced or a new developer. The following guide assumes you have some very basic knowledge of PHP 5.
You will also need a working eFront installation. If you don’t have one, see Installation for instructions on how to install it. If you don’t have access to a working eFront installation for development purposes, contact us.
Based on our experience, the majority of the plugins created for an LMS are related to reports. In this guide, we will walk you through the procedure of creating a plugin that provides some custom reporting functionality, for a fictional company called “Acme”. Acme has requested for a plugin that.
-
Reports on the total number of users, courses and certificates
-
Presents a list of users that have not used the system recently
-
Presents a list of users that have not completed a specific course
-
Emails the list to a specified user
-
Allows saving and retrieving past reports
Scaffolding
Plugins are always located under the www/plugins folder of your eFront installation. In general, a plugin can have any file structure its creator wants, but it’s highly recommended that you follow this structure:
- A plugin’s folder structure
-
www/plugins/ ---- AcmeReports -------- assets/ ------------ images/ ---------------- plug.svg -------- Controller/ ------------ AcmeReportsController.php -------- i18n/ ------------ en_US/ ---------------- LC_MESSAGES/ -------- Model/ ------------ AcmeReports.php ------------ AcmeReportsPlugin.php -------- View/ ------------ acmeReports.tpl -------- plugin.ini
Note: Don’t bother with creating all these folders and files by hand; Use the “Create new plugin” option from admin→plugins to have the system create them for you. |
Installing/Configuring
A plugin needs to include at a minimum 2 files, one for configuring it called plugin.ini, and one for running it, called <name>Plugin.php (AcmeReportsPlugin.php in our example)
Note: If you have used the “Create new plugin” button, the files described below are already created for you, and the plugin is already installed. You can skip directly to Req. 1: Reports on the total number of users, courses and certificates (but don’t, because it’s worth reading nevertheless) |
[plugin] class = Efront\Plugin\AcmeReports\Model\AcmeReportsPlugin name = AcmeReports ;must match the path name under Plugins title = Acme Report version = 1.0 ;The plugin version, change when upgrade is needed requires = 4.1.0 ;minimum supported eFront version compatibility = 4.1.0 ;maximum compatible eFront version author = John Doe description = This is a fully-functional plugin that is created as part of eFront plugin guide
where:
-
class is the namespaced class of your plugin main file: Efront\Plugin\<name>\Model\<name>Plugin, where <name> is your plugin’s name
-
Name is the name of the plugin, as identified by the system. This should be a string with no spaces or special characters
-
Title is the plugin title that appears in the plugins list. It can be anything you like.
-
Version is the current version of the plugin. It is important to keep your version number up to date, because automatic upgrading depends on it (see Upgrading)
-
Requires is the minimum eFront version that this plugin can run on. If you try to install it in an older version, an error message will appear.
-
Compatibility is the most recent version of eFront that this plugin has been tested on. You can see your current eFront version under Admin → Maintenance → Registration.
-
Author is the name of the creator of the plugin, you are free to put whatever you like here.
-
Description is a field you can use to describe the intended use of the plugin.
The file AcmeReportsPlugin.php serves as the glue between the core system and our plugin. It extends the class AbstractPlugin
and implements all functions defined in the Plugin API. For now, we only need the bare minimum for this file, which is to implement the 3 abstract functions of the AbstractPlugin
class. Here is the contents it can have:
- AcmeReportsPlugin.php
-
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\AbstractPlugin; class AcmeReportsPlugin extends AbstractPlugin { const VERSION = '1.0'; public function installPlugin() { } public function uninstallPlugin() { } public function upgradePlugin() { } }
Note: You always have to implement the 3 abstract functions shown above, even if they’re left empty. |
-
Go to Admin → Plugins → Install new plugin, and click to install your plugin, which you must have compressed as a zip file.
-
Or copy the plugin files directly to the www/plugins/ folder on the eFront server. Then go to Admin → plugins and the plugin will appear as “not installed” in the list. Click on the “plus” icon next to it to install it.
After the plugin is installed successfully, it will display as such in the plugins list. Use the handles provided to deactivate it, download its files or upload a new version of your plugin.
Accessing the Plugin API
Every plugin can access the Plugin API through its AbstractPlugin
inheriting class. The AbstractPlugin
provides a set of methods that are called every time a plugin can be invoked. For example, when a page starts loading, the onPageLoadStart()
method is called for every plugin. Similarly, when the calendar is calculated, the onLoadCalendar()
method is called, with the calendar entries passed by reference. For a complete list of available methods, consult the Plugin API Reference section.
Capturing an event
Events are fired throughout the system multiple times during the execution of the script, for example when a user is created, a course is deleted, etc. A plugin may listen for these events to perform some action, via the onEvent()
method. onEvent()
is called for every event that happens in the system, so the plugin must make sure it picks out the one that it is interested in and ignores the rest. For example, to perform an action each time a user signs in or signs out, we would implement onEvent()
like this:
public function onEvent(Event $event) { switch (get_class($event)) { case 'Efront\Model\Event\UserSignedin': //perform an action for users that signed in break; case 'Efront\Model\Event\UserSignedout': //perform an action for users that signed out break; default: break; } }
For a complete list of available Events, see the Event Reference section.
Accessing the Core API
Plugins in eFront are not executed in a “walled garden”; instead, they have full access to the underlying system and functions. Although a complete reference of the Core API is not available (and out of the context of this guide), here is a very short list with examples of performing common operations using the core API:
Get the object of the user currently signed in:
$user = User::getCurrentUser()
Instantiate an object (for example, user with id 321):
$user = new User(321);
Update an object:
$user = new User(321); $user->setFields(array('email'=>'jdoe@example.com'))->save();
Delete an object:
$user = new User(321); $user->delete();
Query the database:
//Get the users that haven’t signed in for more than a month, order by last_login column Database::getInstance()->getTableData(User::DATABASE_TABLE, 'id,name,surname,email', 'last_login<'.time()-86400, 'last_login');
$obj→setFields()
to change an object’s properties and $obj→delete()
to delete an entry.Setting access links and icons
Our plugin is now installed, but does nothing. Since this plugin will be used by administrators, we will create a link to its page from the administrator’s dashboard. To do this, we employ the Plugin API’s onLoadIconList()
function, inside the AcmeReportsPlugin
file:
Note: The […] symbol in any code snippets, refers to code created previously that is present but has been omitted, for the sake of simplicity.
?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\AbstractPlugin; use Efront\Model\User; use Efront\Controller\UrlhelperController; class AcmeReportsPlugin extends AbstractPlugin { const VERSION = '1.0'; [...] public function onLoadIconList($list_name, &$options) { if ($list_name == 'dashboard' && User::getCurrentUser()->isAdministator()) { $options[] = array('text' => $this->plugin->title, 'image' => $this->plugin_url.'/assets/images/plug.svg', 'class' => 'medium', 'href' => UrlhelperController::url(array('ctg' => $this->plugin->name)), 'plugin' => true); return $options; } else { return null; } } }
The snippet above will make an icon appear in the Administrator’s dashboard.
-
For determining the current user’s type, see User types and restrictions
-
Each plugin can have a page of its own, for example under /AcmeReports. See Creating URLs on how to define its link, using the
URLHelperController
class -
Note the 2
use
statements we put on top
There are a few “icon lists” like the administrator’s dashboard in the system. Each has its own distinctive name, which you have to specify in an if clause when implementing the onLoadIconList()
function
Creating a dedicated page
Now that we have a link to our plugin’s page, it’s time to create the page itlself. For this, we will need a few new files: Model/AcmeReports.php, Controller/AcmeReportsController.php and View/acmereports.tpl First, we will create a model class, that will hold the bulk of our implementation. For now, we’ll leave it to the bare minimum:
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\BaseModel; class AcmeReports extends BaseModel { const PLUGIN_NAME = 'AcmeReports'; }
Now, we need to tell the system that every time the plugin page is requested, its controller will be loaded. To do this, we implement the onCtg()
method in our AcmeReportsPlugin
class:
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\AbstractPlugin; use Efront\Model\User; use Efront\Controller\UrlhelperController; use Efront\Controller\BaseController; use Efront\Plugin\AcmeReports\Controller\AcmeReportsController; class AcmeReportsPlugin extends AbstractPlugin { const VERSION = '1.0'; [...] public function onCtg($ctg) { if ($ctg == $this->plugin->name) { BaseController::getSmartyInstance()->assign("T_CTG", 'plugin')->assign("T_PLUGIN_FILE", $this->plugin_dir.'/View/AcmeReports.tpl'); $controller = new AcmeReportsController(); $controller->plugin = $this->plugin; return $controller; } } }
onCtg()
is called whenever we need to decide where to route a request. $ctg
always contains the first part of the request URL. For example, if our installation is installed on http://eFront.example.com/ and we request http://eFront.example.com/foo, then $ctg
will contain ‘foo’. This determines which controller will take effect. In our case, the controller used is AcmeReports
, so the icon link we created earlier points to /AcmeReports. Our onCtg()
implementation ensures that AcmeReportsController
is invoked, and that the proper view file, AcmeReports.tpl will be used.
BaseController
class:
<?php namespace Efront\Plugin\AcmeReports\Controller; use Efront\Controller\UrlhelperController; use Efront\Model\UserType; use Efront\Controller\BaseController; use Efront\Plugin\AcmeReports\Model\AcmeReports; class AcmeReportsController extends BaseController { public $plugin; protected function _requestPermissionFor() { return array(UserType::USER_TYPE_PERMISSION_PLUGINS, UserType::USER_TYPE_ADMINISTRATOR); } public function index() { $smarty = self::getSmartyInstance(); $this->_model = new AcmeReports(); $this->_base_url = UrlhelperController::url(array('ctg' => $this->plugin->name)); $smarty->assign("T_PLUGIN_TITLE", $this->plugin->title) ->assign("T_PLUGIN_NAME", $this->plugin->name) ->assign("T_BASE_URL", $this->_base_url); } }
Each controller should implement 2 functions: _requestPermissionFor()
is used to perform access control. See User types and restrictions for more information. index()
is the function called automatically by the system. Basecontroller
includes an implementation for index()
, so if you don’t implement it, the parent’s will be invoked
In the example above, we use $smarty = self::getSmartyInstance()
to get the view manager instance, which is based on Smarty3. We then call $smarty→assign(<name>, <variable>)
each time we need to pass to our template the value of a <variable>
and access it with <name>
. The Base URL is the URL of your controller, and it’s very convenient to have it handy at any given time, so we assign it to the controller and our template.
Finally, our template file can be as simple as this:
{eF_template_appendTitle title = $T_PLUGIN_TITLE link = $T_BASE_URL} {capture name = 't_code'} <!-- Anything you wish to display here --> {/capture} {eF_template_printBlock data = $smarty.capture.t_code}
The first line adds the “AcmeReports” part on the system’s breadcrumb (path) bar. The last line prints a standard html div block, containing all the html markup defined inside the referenced capture code. We will add more in our template right below, but see our 1-minute introduction to smarty to get a quick idea of how smarty works.
-
Reports on the total number of users, courses and certificates
-
Presents a list of users that have not used the system recently
-
Presents a list of users that have not received a certification for specific courses
-
Emails the list to the users’ supervisors
-
Allows saving and retrieving past reports
We will proceed with creating each one below
Req. 1: Reports on the total number of users, courses and certificates
Let’s create a triplet of functions that retrieve this information, inside our Model class, AcmeReports.php:
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\BaseModel; use Efront\Model\User; use Efront\Model\Course; use Efront\Model\UserCertificate; class AcmeReports extends BaseModel { const PLUGIN_NAME = 'AcmeReports'; public function getTotalUsers() { return User::countAll(); } public function getTotalCourses() { return Course::countAll(); } public function getTotalCertifications() { return UserCertificate::countAll(); } }
Now, let’s call these functions from our controller, AcmeReportsController.php:
<?php namespace Efront\Plugin\AcmeReports\Controller; [...] class AcmeReportsController extends BaseController { [...] public function index() { [...] $total_users = $this->_model->getTotalUsers(); $total_courses = $this->_model->getTotalCourses(); $total_certificates = $this->_model->getTotalCertifications(); $smarty->assign(array( "T_TOTAL_USERS" => $total_users, "T_TOTAL_COURSES" => $total_courses, "T_TOTAL_CERTIFICATES" => $total_certificates, )); } }
Finally, we edit our template file, AcmeReports.tpl, to display these numbers inside some pretty panels:
{eF_template_appendTitle title = $T_PLUGIN_TITLE link = $T_BASE_URL} {capture name = 't_code'} {eF_template_printPanel image = "users" header = "Total Users"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_USERS} {eF_template_printPanel image = "courses" header = "Total Courses"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_COURSES} {eF_template_printPanel image = "certificate" header = "Total Certificates"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_CERTIFICATES} <div class = "clearfix"></div> {/capture} {eF_template_printBlock data = $smarty.capture.t_code}
That’s it! Our plugin page now displays the totals we were requested to display:
Req. 2: Presents a list of users that have not used the system recently
Lists are best presented using Data Grids, like those found in the system under admin→users, admin→courses etc. To implement one, we are first going to create the table in the view file:
{eF_template_appendTitle title = $T_PLUGIN_TITLE link = $T_BASE_URL} {capture name = 't_code'} [...] <!--ajax:reportsTable--> <div class="table-responsive"> <table style = "width:100%;" class = "sortedTable table" data-sort = "last_login" size = "{$T_TABLE_SIZE}" id = "reportsTable" data-ajax = "1" data-rows = "{$smarty.const.G_DEFAULT_TABLE_SIZE}" url = "{$smarty.server.REQUEST_URI}"> <tr> <td class = "topTitle" name = "formatted_name">{"User"|ef_translate}</td> <td class = "topTitle" name = "last_login" data-order="desc">{"Last login"|ef_translate}</td> <td class = "topTitle noSort centerAlign">{"Operations"|ef_translate}</td> </tr> {foreach name = 'users_list' key = 'key' item = 'user' from = $T_DATA_SOURCE} <tr class = "{cycle values = "oddRowColor, evenRowColor"} {if !$user.active}text-danger{/if}" > <td>{$user.formatted_name}</td> <td>{$user.last_login}</td> <td class = "centerAlign nowrap"> <img src = 'assets/images/transparent.gif' class = 'ajaxHandle icon-trafficlight_{if $user.active == 1}green{else}red{/if} small ef-grid-toggle-active' data-url = "{eF_template_url url = ['ctg'=>'users','toggle'=>$user.id]}" alt = "{"Toggle"|ef_translate}" title = "{"Toggle"|ef_translate}"> </td> </tr> {foreachelse} <tr class = "defaultRowHeight oddRowColor"><td class = "emptyCategory" colspan = "100%">-</td></tr> {/foreach} </table> </div> <!--/ajax:reportsTable--> {/capture} {eF_template_printBlock data = $smarty.capture.t_code}
And the respective function in the controller:
<?php namespace Efront\Plugin\AcmeReports\Controller; [...] use Efront\Controller\GridController; use Efront\Model\User; class AcmeReportsController extends BaseController { [...] public function index() { [...] if (isset($_GET['ajax']) && $_GET['ajax'] == 'reportsTable') { $this->_listUsers(); } else { $total_users = $this->_model->getTotalUsers(); [...] } } protected function _listUsers() { try { $constraints = GridController::createConstraintsFromSortedTable(); $entries = User::getAll($constraints); $totalEntries = User::countAll($constraints); $grid = new GridController($entries, $totalEntries, $_GET['ajax'], true); $grid->show(); } catch (\Exception $e) { handleAjaxExceptions($e); } } }
You can now see a list of all users, sorted by the last_login field. However, this field appears as timestamp, rather than in a human-readable format. We’ll fix this in the _listUsers()
function of our controller:
protected function _listUsers() { try { $constraints = GridController::createConstraintsFromSortedTable(); $entries = User::getAll($constraints); $total_entries = User::countAll($constraints); foreach ($entries as $key=>$value) { $entries[$key]['last_login'] = formatTimestamp($entries[$key]['last_login'], 'time_nosec'); } $grid = new GridController($entries, $total_entries, $_GET['ajax'], true); $grid->show(); } catch (\Exception $e) { handleAjaxExceptions($e); } }
That’s it! Notice that we also added a “toggle active” handle in our list, that can be used to set a user as active/inactive. This handle does not have any implementation associated, because it calls the core system’s UserController
to perform the action. Here’s how our plugin page now looks, after adding the data grid:
On to the next requirement!
Req. 3: Present a list of users that have not completed a specific course
We first need to create a drop-down list of courses that have a certificate associated, so that the user can select one. In our controller, we do:
<?php namespace Efront\Plugin\AcmeReports\Controller; [...] use Efront\Model\Course; class AcmeReportsController extends BaseController { [...] public function index() { [...] $courses = Course::getPairs(array('condition'=>'archive=0 AND active=1'), array('id', 'formatted_name')); $smarty->assign("T_COURSES", $courses); } }
The code above retrieves all active, unarchived courses and assigns them to the template.
<div style = "max-width:400px;"> <select id = "ef-select-course" class = "form-control ef-select"> <option value="">{"Select a course to display certifications for"|eF_dtranslate:$T_PLUGIN_NAME}</option> {foreach $T_COURSES as $key=>$value} <option value="{$key}">{$value}</option> {/foreach} </select> </div>
ef-select
class to any <select>
element, will convert it to a searchable drop-down
<script> $('#ef-select-course').on('change', function(event) { if ($(this).val()) { var url = $.fn.st.getAjaxUrl('reportsTable').replace(/\/courses_ID\/[\w\d]*/, '')+'/courses_ID/'+$(this).val(); } else { var url = $.fn.st.getAjaxUrl('reportsTable').replace(/\/courses_ID\/[\w\d]*/, ''); } $.fn.st.setAjaxUrl('reportsTable', url); $.fn.st.redrawPage('reportsTable', true); }); </script>
This will change our Grid’s target URL, so that it also sends the selected course’s ID. We will need to handle this accordingly to our Controller function as well:
[...] use Efront\Model\Courses\CourseToUser; [...] public function index() { [...] if (isset($_GET['ajax']) && $_GET['ajax'] == 'reportsTable') { if (!empty($_GET['courses_ID'])) { $this->_listCourseUsers($_GET['courses_ID']); } else { $this->_listUsers(); } } else { [...] } } [...] protected function _listCourseUsers($course_id) { try { $this->checkId($course_id); $course = new Course(); $course->init($course_id); User::getCurrentUser()->canRead($course); $constraints = GridController::createConstraintsFromSortedTable(); $conditions = array('condition'=>'u.archive=0 AND u.active=1 and status !="'.CourseToUser::STATUS_COMPLETED.'"'); $entries = $course->getUsers($constraints+$conditions, array('u.*')); $total_entries = $course->countUsers($constraints+$conditions); foreach ($entries as $key=>$value) { $entries[$key]['last_login'] = formatTimestamp($entries[$key]['last_login'], 'time_nosec'); } $grid = new GridController($entries, $total_entries, $_GET['ajax'], true); $grid->show(); } catch (\Exception $e) { handleAjaxExceptions($e); } }
Here’s what our plugin page looks like now:
Req. 4: Email the list to a specified user
All grids contain a handy export button, that dumps its output as a CSV file (next to the filter box). Based on this functionality, we will create a button that delivers the report to the specified recipient, instead of prompting the user to download it. First, let’s create the button in our template file:
<a class = "btn btn-primary pull-right ef-report-handles" id = "ef-email-list" style = "display:none">{"Email list"|eF_dtranslate:$T_PLUGIN_NAME}</a> <div style = "display:none" id = "ef-select-recipient-div"> <form class="form-inline"> <div class="form-group"> <input type="text" name = "recipient" data-type = "users" class = "form-control ef-autocomplete" style = "width:400px" placeholder = "{"Start typing to find user"|eF_dtranslate:$T_PLUGIN_NAME}"/> </div> <input type = "button" class = "btn btn-primary" id = "ef-select-recipient" value = "{"Send"|ef_translate}"> </form> </div>
Notice how we hide it by default. This is because we want it to appear only when the user has selected a course, so we change the ef-select-course
handling and add the necessary scripting:
[...] <script> $('#ef-email-list').on('click', function(evt) { $.fn.efront('modal', { 'header':'Select recipient', 'id':'ef-select-recipient-div'}); }); $('#ef-select-recipient').on('click', function(evt) { var url = $.fn.st.getAjaxUrl('reportsTable', true)+'?email=reportsTable&columns=formatted_name:User,last_login:Last%20login'; $.fn.efront('ajax', url, { data: { recipients:$('input[name="recipient"]').val().replace(/users-/,'') } }, function(response) { $.fn.efront('modal', { close:true}); }); }); $('#ef-select-course').on('change', function(event) { if ($(this).val()) { var url = $.fn.st.getAjaxUrl('reportsTable').replace(/\/courses_ID\/[\w\d]*/, '')+'/courses_ID/'+$(this).val(); $('.ef-report-handles').fadeIn(); } else { var url = $.fn.st.getAjaxUrl('reportsTable').replace(/\/courses_ID\/[\w\d]*/, ''); $('.ef-report-handles').fadeOut(); } $.fn.st.setAjaxUrl('reportsTable', url); $.fn.st.redrawPage('reportsTable', true); }); </script> [...]
The code above will make a modal appear, where the user can select recipients for the email, and send an ajax request to our controller. We now have to implement the logic in our controller that handles this request:
[...] use Efront\Model\File; use Efront\Model\Configuration; use Efront\Model\MailQueue; [...] protected function _listCourseUsers($course_id) { try { [...] if (!empty($_GET['email'])) { $file = $grid->exportData(); $this->_sendEmail($file, $_GET['recipients']); $this->jsonResponse(); } else { $grid->show(); } } catch (\Exception $e) { handleAjaxExceptions($e); } } protected function _sendEmail(File $file, $recipients) { if (empty($recipients)) { throw new EfrontException("You haven't specified a recipient"); } $ids = explode(",", $recipients); $messages = array(); foreach ($ids as $id) { $this->checkId($id); $user = new User($id); $messages[] = array( 'recipient' => $user->email, 'title' => dtranslate("A new report has been emailed to you by %s",
AcmeReports::PLUGIN_NAME, Configuration::getValue(Configuration::CONFIGURATION_MAIN_URL)), 'body' => dtranslate("Hello %s, Please find attached the report exported on %s", AcmeReports::PLUGIN_NAME,
$user->formatted_name, formatTimestamp(time())), 'attachment' => $file->id, ); } if (!empty($messages)) { $mail_queue = new MailQueue(); $mail_queue->addToQueue($messages); } }
Sending an email is as simple as creating an array holding the email data (recipient email, title, body) and adding it to an MailQueue
instance. See Sending emails for more information.
Here’s how our plugin page looks now, after clicking on the “Email List” button:
Req. 5: Allows saving and retrieving past reports
We’re almost through with our plugin, the only thing remaining is to save reports to the database. We will employ our model for this, which will be converted to describe a database entry. But first, we need to set up our database table, using our AcmeReportsPlugin
class’ onInstall()
, onUninstall()
and onUpgrade()
methods:
<?php namespace Efront\Plugin\AcmeReports\Model; [...] use Efront\Model\Database; class AcmeReportsPlugin extends AbstractPlugin { const VERSION = '1.1'; public function installPlugin() { $sql = "CREATE TABLE if not exists plugin_acme_reports( id mediumint not null auto_increment primary key, timestamp int default 0, report longtext) ENGINE=InnoDB DEFAULT CHARSET=utf8;"; try { Database::getInstance()->execute($sql); } catch (\Exception $e) { $this->uninstallPlugin(); //so that any installed tables are removed and we're able to restart fresh } return $this; } public function uninstallPlugin() { $sql = "drop table if exists plugin_acme_reports"; Database::getInstance()->execute($sql); return $this; } public function upgradePlugin() { $queries = array(); if (version_compare('1.1', $this->plugin->version) == 1) { $queries[] = "CREATE TABLE if not exists plugin_acme_reports( id mediumint not null auto_increment primary key, timestamp int default 0, report longtext) ENGINE=InnoDB DEFAULT CHARSET=utf8;"; } foreach ($queries as $query) { Database::getInstance()->execute($query); } return $this; } [...]
The implementation above will ensure that:
-
A table called
plugin_acme_reports
will be created upon installation of the plugin -
The table will be deleted upon uninstallation
-
The table will be created for existing plugins (such as ours). To ensure that the
onUpgrade()
function runs, simply change the constantVERSION
in the class. See Upgrading for more information.
Simply refresh your page to allow the plugin’s onUpgrade()
to run.Now that our database table is in place, let’s extend our Model’s implementation:
<?php namespace Efront\Plugin\AcmeReports\Model; [...] class AcmeReports extends BaseModel { const PLUGIN_NAME = 'AcmeReports'; const DATABASE_TABLE = 'plugin_acme_reports'; protected $_fields = array( 'id' => 'id', 'timestamp' => 'timestamp', 'report' => 'wysiwig', ); public $id; public $timestamp; public $report; [...] }
That’s it! All we had to do is to define a DATABASE_TABLE
constant and create an array with the database table’s fields, as well as a public variable for each field. Being a BaseModel
class, our Model already contains all the necessary functions to handle create/update/read/delete operations (see Creating a new Model + Database table). We can now work on our controller to take advantage of this functionality. But first we must create the necessary button and functionality in the View component:
[...] <a class = "btn btn-primary pull-right ef-report-handles" id = "ef-save-list" style = "display:none;margin-left:10px;">{"Save list"|eF_dtranslate:$T_PLUGIN_NAME}</a> [...] <script> $('#ef-save-list').on('click', function(evt) { var url = $.fn.st.getAjaxUrl('reportsTable', true)+'?save=reportsTable&columns=formatted_name:User,last_login:Last%20login'; $.fn.efront('ajax', url, { data: { 'save':true } }, function(response) { bootbox.dialog({ message: '<h4>{"Report Saved!"|eF_dtranslate:$T_PLUGIN_NAME}</h4>', //title: 'Report saved', buttons: { cancel: { label: $.fn.efront('translate', "Close"),className: "btn-primary"}} }); }); }); [...]
This will send a request, similar to the previous, only this time specifying we need to save instead of emailing. Our controller implements this logic as follows:
[...] protected function _listCourseUsers($course_id) { try { [...] if (!empty($_GET['email'])) { $file = $grid->exportData(); $this->_sendEmail($file, $_GET['recipients']); $this->jsonResponse(); } else if (!empty($_GET['save'])) { $file = $grid->exportData(); $this->_model->setFields(array( 'timestamp'=>time(), 'report' => file_get_contents(G_ROOTPATH.$file->path), ))->save(); $this->jsonResponse(); } else { $grid->show(); } } catch (\Exception $e) { handleAjaxExceptions($e); } } [...]
That’s it! Our plugin now saves the report, along with a timestamp. The only thing left now, is to present the list of saved reports to the user. We will do so by implementing a second Grid, in a different tab. First, let’s consider our template. We will create a {capture}
section that will hold our new grid:
[...] {capture name = "t_saved_reports"} <!--ajax:savedReportsTable--> <div class="table-responsive"> <table style = "width:100%;" class = "sortedTable table" data-sort = "last_login" size = "{$T_TABLE_SIZE}" id = "savedReportsTable" data-ajax = "1" data-rows = "{$smarty.const.G_DEFAULT_TABLE_SIZE}" url = "{$smarty.server.REQUEST_URI}"> <tr> <td class = "topTitle" name = "timestamp">{"Timestamp"|ef_translate}</td> <td class = "topTitle noSort centerAlign">{"Operations"|ef_translate}</td> </tr> {foreach name = 'users_list' key = 'key' item = 'report' from = $T_DATA_SOURCE} <tr class = "{cycle values = "oddRowColor, evenRowColor"}" > <td>{$report.timestamp}</td> <td class = "centerAlign nowrap"> <a href = "{eF_template_url extend = $T_BASE_URL url = ['view'=>$report.id]}" target = "_new"> <img src = 'assets/images/transparent.gif' class = 'icon-search small' alt = "{"View"|ef_translate}" title = "{"View"|ef_translate}"> </a> <img src = 'assets/images/transparent.gif' class = 'ef-grid-delete ajaxHandle icon-error_delete small' data-url = "{eF_template_url extend=$T_BASE_URL url=['delete'=>$report.id]}" alt = "{"Delete"|ef_translate}" title = "{"Delete"|ef_translate}"/> </td> </tr> {foreachelse} <tr class = "defaultRowHeight oddRowColor"><td class = "emptyCategory" colspan = "100%">-</td></tr> {/foreach} </table> </div> <!--/ajax:savedReportsTable--> {/capture} [...]
We need this capture, to create tabs. We must add all the other code inside another,{capture}
and then create the tabs using {eF_template_printTabs}
. In the end it looks like this:
{eF_template_appendTitle title = $T_PLUGIN_TITLE link = $T_BASE_URL} {capture name = "t_users"} [...] {/capture} {capture name = "t_saved_reports"} [...] {/capture} {capture name = 't_code'} {eF_template_printPanel image = "users" header = "Total Users"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_USERS} {eF_template_printPanel image = "courses" header = "Total Courses"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_COURSES} {eF_template_printPanel image = "certificate" header = "Total Certificates"|eF_dtranslate:$T_PLUGIN_NAME body = $T_TOTAL_CERTIFICATES} <div class = "clearfix"></div> {eF_template_printTabs tabs = [['key' => 'users', 'title' => "Users"|ef_translate, 'data' => $smarty.capture.t_users], ['key' => 'reports', 'title' => "Reports"|ef_translate, 'data' => $smarty.capture.t_saved_reports]]} {/capture} {eF_template_printBlock data = $smarty.capture.t_code}
The code above prints 2 tabs, one for each grid. The saved reports grid is handled in our controller with a _listReports() function, similar to the previous grid:
[...] if (isset($_GET['ajax']) && $_GET['ajax'] == 'reportsTable') { if (!empty($_GET['courses_ID'])) { $this->_listCourseUsers($_GET['courses_ID']); } else { $this->_listUsers(); } } else if (isset($_GET['ajax']) && $_GET['ajax'] == 'savedReportsTable') { $this->_listReports(); [...] protected function _listReports() { try { $constraints = GridController::createConstraintsFromSortedTable(); $entries = AcmeReports::getAll($constraints); $total_entries = AcmeReports::countAll($constraints); foreach ($entries as $key=>$value) { $entries[$key]['timestamp'] = formatTimestamp($entries[$key]['timestamp'], 'time_nosec'); } $grid = new GridController($entries, $total_entries, $_GET['ajax'], true); $grid->show(); } catch (\Exception $e) { handleAjaxExceptions($e); } } [...]
But that’s not all! Our saved reports grid also contains a couple of handles: One for downloading a report and one for deleting it. Here’s the controller code that implements both of these:
[...] } else if (isset($_GET['ajax']) && $_GET['ajax'] == 'savedReportsTable') { $this->_listReports(); } else if (!empty($_GET['view'])) { $this->checkId($_GET['view']); $this->_model->init($_GET['view']); $path = G_TEMPDIR."report.csv"; file_put_contents($path, $this->_model->report); $file = new File($path); $file->sendFile(true); } else { $this->deleteHandler();
the deleteHandler()
function is part of the BaseController
so we don’t have to actually implement anything for the deletion. For viewing, we first dump the data to a temporary file, and then send it using File::sendFile(true)
, which takes care of all the required headers and procedures for downloading a file
Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-final
That’s it! Congratulations, your plugin is ready to ship to Acme!
Overriding templates
One common customization requirement is to change substantially the look and feel of a certain page. For example, someone might need to provide a totally different implementation for the navbar, with changes that cannot be achieved using CSS and Javascript (which can be altered using custom themes). In such a case, a plugin can be used, as follows:
-
Make sure you implement
AbstractPlugin::overridesTemplate()
inside yourPlugin
class (AcmeReportsPlugin
in our example):public function overridesTemplate() {return dirname(__DIR__).'/View/';}
-
Locate the template file you wish to override. In our example, that would be libraries/Efront/View/layout/navbar.tpl. Then create a file with the same name at the respective folder of your plugin. In our
AcmeReports
example, we would create the file www/plugins/AcmeReports/View/layout/navbar.tpl -
The system now will read your plugin’s file, instead of the default one. However, if you still want to reference the original file (for example, if we simply want to add some code), you can do this as follows:
{include file = "file:[base]layout/navbar.tpl"}<!-- Some custom HTML code here -->
-
A different option is to extend a plugin, rather than completely overriding. In this case, if the core file contains any named {block} sections, you can override these specifically. For example, the file lessons/lesson_dashboard.tpl, contains the following block for displaying the two columns in the dashboard:
{block name = "lesson_dashboard_layout"} <div class="col-md-6"> {$smarty.capture.left_block} </div> <div class="col-md-6"> {$smarty.capture.right_block} </div> {/block} [...]
You can now extend this particular piece of code in your plugin’s lesson/lesson_dashboard.tpl file. For example, the code below would add an additional column in the middle, making the lesson dashboard’s layout to have 3 columns, instead of 2:
{extends file = "file:[base]catalog/course.tpl"} {block name = 'lesson_dashboard_layout'} <div class="col-md-4"> {$smarty.capture.left_block} </div> <div class="col-md-4"> <!-- Some custom code here --> </div> <div class="col-md-4"> {$smarty.capture.right_block} </div> {/block}
Manipulating forms
Forms in general is a complicated issue, but in eFront steps have been taken to simplify them as much as possible. Forms are usually implemented as part of the Model class. See Creating forms for more information on how to create a form. Every form can be manipulated from within a plugin using the onCreateForm()
, onBeforeHandleForm()
and onAfterHandleForm()
functions. All of these functions accept as argument the form name and the Form
object
-
onCreateForm()
is called right after the form is populated with fields, but before any processing happens. This is suitable for adding or removing fields from the form. -
onBeforeHandleForm
is called right after the submit button is pressed, but before any actual processing takes place. This is suitable for completely overriding the default form handling, or for pre-processing POST variables -
onAfterHandleForm()
is called after all processing is finished. This is suitable for performing post-processing operations.
Internationalization
You may have noticed that in many examples, we use translate()
/dtranslate()
(PHP) and ef_translate
/eF_dtranslate
(template) for outputting strings that should appear localized. That’s because eFront uses Gettext to handle the translations of its messages. translate()
and ef_translate
will use the system’s existing translations, and are used for cases where a string already exists in the core. For strings that are specific to your plugin, follow a few simple steps as described below:
-
A message of your plugin which require translation, will properly be translated if you use 2 functions. The function
dtranslate(“myMessage”, “<plugin_name>”)
must be used when the message exists into a class and not a template inside your plugins scope. In the other case (template), you have to use the“myMessage”|eF_dtranslate:<plugin_name>
. Replace<plugin_name>
with the name of your plugin -
Visit the page /<plugin>/parse_language/1 (For example, http://eFront.example.com/AcmeReports/parse_language/1). This will create all the required language specific folders and .po files inside the plugin’s i18n folder and a php file with name translations.php.
-
Open each .po file with poedit. Poedit can be found here.
-
In Poedit menu click on Catalog->Update from Source Code to parse the plugin files and fill the list of strings that need translation.
-
Translate all string literals.
-
Press the “Save” button to save your work. Make sure you have write access to the folders created in step 1.
-
Restart your server, in case that the translations aren’t changed.
Sending emails
When creating a plugin, you don’t have to actually send an email. All you have to do is to pass the email data to the Mailer
, and your email will queued to be delivered upon the next iteration. Example:
<?php [...] $recipients = array('jdoe@example.com', 'professor@example.com', 'admin@example.com'); $subject = "Hello world"; $body = "Hello, this is an email to test emailing from a plugin"; $messages = array(); $mailer = new Mailer(); foreach ($recipients as $recipient) { $message = array( 'recipient' => $recipient, 'subject' => $subject, 'body' => $body, ); $mailer->sendMessage($message) }
Error handling
It is a good practice to have your code throw an exception whenever an error occurs. When catching exceptions, you should discriminate between exceptions that are thrown during the normal flow of your program, or during an ajax request, and handle them differently, using handelNormalFlowExceptions()
or handleAjaxExceptions()
respectively:
<?php [...] try { //some normal flow stuff here throw new \Exception("Testing exceptions"); } catch (\Exception $e) { handleNormalFlowExceptions($e); } if (!empty($_GET[‘ajax’])) { try { //some ajax flow stuff here throw new \Exception("Testing exceptions"); } catch (\Exception $e) { handleAjaxExceptions($e); } }
This will ensure that the error message will always display properly in the end user.
onEvent()
call, the exception will be suppressed, to prevent the operation from stopping. You should handle any exceptions that your plugin might generate during an onEvent()
call yourselfUpgrading
A plugin typically requires upgrade, when its associated database schema needs changing. The plugin upgrading process can be automated in eFront and is described as a step-by-step process:
Imagine that you have a plugin named “AcmeReports” which is in version 1.0 and needs to run an alter table
query.
-
Edit the plugin class file that extends AbstractPlugin and change the VERSION constant to a larger number, for example 1.1
-
Edit your upgradePlugin() function to include the SQL code that must be ran. For example, in order to add a field to a table:
[...] public function upgradePlugin() { if (version_compare('1.0', $this->plugin->version) == 1) { Database::getInstance()->execute("alter table plugin_acme_reports add timestamp int default null"); } return $this; } [...]
-
Once you run any page in your system (e.g. sign in as administrator) The system will detect that the plugin’s stored version number is different than the one specified in VERSION, and will execute the plugin’s upgradePlugin() function.
-
Each time a new change is required, increase the VERSION and add the proper lines inside upgradePlugin(). For example, for 1.2, the function will become:
[...] public function upgradePlugin() { if (version_compare('1.1', $this->plugin->version) == 1) { Database::getInstance()->execute("alter table plugin_acme_reports add timestamp int default null"); } if (version_compare('1.2', $this->plugin->version) == 1) { Database::getInstance()->execute("alter table plugin_acme_reports add comments text"); } return $this; } [...]
onInstall()
function, and add proper routines in onUninstall()
, if neededThe Demo plugin
eFront ships with a plugin called “Demo”, which serves as a testbed for all Plugin calls. It implements all possible calls found in AbstractPlugin while at the same time serving as a fully functional plugin, complete with its own database table and Grid. Feel free to download, explore and change it to learn more about plugins
Taking advantage of the REST API
eFront provides a REST API that allows 3rd-party systems to exchange data and perform operations, using the associated API key. A plugin may take advantage of the REST API’s mechanism of exchanging data, via the onApiCall()
method. See the documentation on the REST API and the Plugin API Reference for more information
User types and restrictions
There are 3 basic user types in efront, which determine the interface that displays to the user: Administrator, Instructor, and Learner. Based on these, an Administrator may create subtypes, with different permission levels. In addition, an Administrator-type user that is assigned to a branch, is called a “Supervisor” and has Admistrator rights only in his/her own branch context. In order to determine a user object’s type, the simplest way is to call one of these functions:
$user = new User(321); $user->isAdministrator(); //returns true if the $user type is based on the “administrator” basic type $user->isSuperisor(); //returns true if the $user type is based on the “administrator” AND is assigned to a branch $user->isProfessor(); //returns true if the $user type is based on the “professor” basic type $user->isStudent(); //returns true if the $user type is based on the “student” basic type
When editing a template file, these functions have their smarty counterparts:
{if $T_CURRENT_USER_IS_ADMINISTRATOR} {elseif $T_CURRENT_USER_IS_SUPERVISOR} {elseif $T_CURRENT_USER_IS_PROFESSOR} {elseif $T_CURRENT_USER_IS_STUDENT} {/if}
When inside a controller, if we need to restrict access to a specific basic type, we must implement the _requestPermissionFor()
function. For example, the following implementation limits the current plugin to administrator types only:
[...] class AcmeReportsController extends BaseController { protected function _requestPermissionFor() { return array(UserType::USER_TYPE_PERMISSION_PLUGINS, UserType::USER_TYPE_ADMINISTRATOR); } [...]
…and the following to Administrators and Instructors:
[...] class AcmeReportsController extends BaseController { protected function _requestPermissionFor() { return array(UserType::USER_TYPE_PERMISSION_PLUGINS, array(UserType::USER_TYPE_ADMINISTRATOR,UserType::USER_TYPE_PROFESSOR)); } [...]
… and the following, to any logged-in user, regardless of type:
[...] class AcmeReportsController extends BaseController { protected function _requestPermissionFor() { return array(UserType::USER_TYPE_PERMISSION_PLUGINS); } [...]
In order to determine whether a user can access or change an entity, one can utilize the usertype object:
//determine if a user can change a course, based on his/her user type $user = new User(123); $user_type = new UserType($user->user_types_ID); $access_level = $user_type->getAccessLevel(UserType::USER_TYPE_PERMISSION_COURSES); if ($access_level == UserType::USER_TYPE_LEVEL_WRITE) { //the $user can create/change a course } else if ($access_level == UserType::USER_TYPE_LEVEL_READ) { //the $user can only read a course, but not change or delete it, neither create a new one } else { //the $user has no access on course information whatsoever }
To simplify things, all controllers that extend BaseController already include the current user’s access_level for the entity specified in _requestPermissionFor()
(the first argument), so inside a controller it’s sufficient to do something like this:
//determine if a user can change a course, based on his/her user type $user = new User(123); if ($this->_access_level == UserType::USER_TYPE_LEVEL_WRITE) { //the $user can create/change a course } else if ($this->_access_level == UserType::USER_TYPE_LEVEL_READ) { //the $user can only read a course, but not change or delete it, neither create a new one } else { //the $user has no access on course information whatsoever }
USER_TYPE_LEVEL_NONE
) to 1 (USER_TYPE_LEVEL_READ
) and 10 (USER_TYPE_LEVEL_WRITE
). However, values between 1 and 10 are possible. Consult the UserType object for an overview of available access levels (or see admin→user types)When inside a template, a similar approach is possible, especially since all access levels are already available, as the
$T_USER_TYPE_ACCESS
variable. For example, if we were to decide whether to display an “edit” link to a user, we would do:
{if $T_USER_TYPE_ACCESS['\Efront\Model\UserType::USER_TYPE_PERMISSION_USERS'|constant] != '\Efront\Model\UserType::USER_TYPE_LEVEL_NONE'|constant} <a href = "{eF_template_url url=['ctg'=>'users','edit'=>$user.id]}" class = "editLink">{$user.formatted_name}</a> {else} {$user.formatted_name} {/if}
In order to ensure that a user has permissions to view/change an entity, one has to consider other parameters, besides its user type: For example, if a Instructor tries to access a course, is he actually enrolled to it? What about a supervisor trying to view information about a user, is that user part of his/her branch tree? For such cases, there is a pair of handy functions one can use, User::canRead()
and User::canChange()
$current_user = User::getCurrentUser(); //The currently logged-in user $course = new Course(321); //some course that the user is trying to access $user->canRead($course); //will throw an exception if the user should not access the course $user->canChange($course); //will throw an exception if the user should not update/delete the course
canRead()
and canWrite()
accept as an argument any object deriving from the BaseModel
class. However, they are expensive functions and should never be used in loops, but only as a last safety precaution.Prioritization
Plugins in eFront are executed in a “first installed-first served” fashion. This means that, when waiting for a call from the plugin API, your plugin might not be the first to handle it, so the output might have changed. In the future, we might add prioritization in plugins, so take care to not rely on this behaviour.
Maintaining core compatibility
Being an actively developed project, eFront will bring updates from time to time, that might break support for your plugin, one way or another. You should take special care to verify your plugin’s compatibility with current versions. In the future, the system will automatically disable plugins that do not explicitly state their compatibility with the current version of the LMS (the ‘compatibility’ entry inside the plugin’s plugin.ini file).
Maintaining template compatibility
If you implement a plugin that overrides or extends a template file, then it’s very likely that at some point, the original file will change in the core, after an upgrade. You should manually verify that your plugin’s versions of the template files incorporate any changes brought by a new eFront version
Coding standards
eFront does not impose any strict coding standards, but encourages the user of the PSR-2 guidelines, which ensure readability and tidiness.
Best practices / common pitfalls
-
Avoid directly executing queries when possible, especially using the
Database::execute()
method. BaseModel usually provides all the tools necessary for querying the database. -
Every database table should have a model class representing it, inheriting the BaseModel class
-
Make sure you account for Supervisors (branch administrators), when presenting data lists. These accounts should never have access to data generated from other, unrelated branches
-
Make sure you leave out archived users, courses and lessons when making calculations. Archived entities are equivalent to deleted; you can tell that an entity is archived, because it will have a timestamp for its “archive” field (normal entities have 0 in this field)
-
All entities are cached, when a caching engine is present. Thus changing an entity directly in the database (either using the Database class or directly from the command line) will not have an apparent effect, unless the cache is cleared (with a webserver restart or from admin→Maintenance→Cache)
Troubleshooting / debugging
Inevitably, you’ll run into all sorts of errors and abnormal situations. Here’s a list of the most common and their causes
debug()
at any point in your script to turn on full error reporting, and a dump of all queries executed, until the script ends or debug(false)
is called.-
Blank screen: This is caused by a fatal error at some very early stage of the system execution. If the web server’s error log file doesn’t say anything about the error, then you might have to dive into the core code itself: Edit the file libraries/Controller/MainController.php, locate the
setup()
function and change the line where it sayserror_reporting( E_ERROR );
toerror_reporting( E_ALL );ini_set(“display_errors”, true);define(“G_DEBUG”, 1);
-
Murphy’s law: The standard error page for unhandled errors states quotes Murphy. Usually this page also displays the error message itself, as well as the file and line where it occurred. If it’s not clear by the message, then try adding a
debug()
call early in your script to help you find out what’s wrong. -
Smarty error: The most common error in smarty templates, is when some Javascript codes contains an opening bracket immediately followed by a character, without a space, for example
{'foo':'bar'}
. Change this to{ 'foo':'bar'}
to prevent smarty from trying to parse it. Other errors, such as syntax errors, usually display with a clear message and the file and line number where they occur. -
Invalid CSRF Token: This is common when trying to open an ajax request in a new window. It happens because of an aggressive CSRF filter the system employs in all Ajax requests. You should only debug ajax calls via your browser’s console.
-
SyntaxError: Unexpected token <: This error is usually thrown from an ajax request that did not receive an JSON response, but rather an HTML response. Probable causes are:
-
You made an ajax request to a controller, but the controller did not provide a JSON response
-
The controller responds properly, but you forgot to exit after the response
-
An error occurred that was not handled with handleAjaxRequest()
-
jsonResponse()
function.Below you can find all functions that you can override inside your Plugin class, as found inside the AbstractPlugin
class
/** * This function is executed every time an event is fired. * @param Event $event The Event object */ public function onEvent(Event $event) { } /** * This function is executed right after a form is submitted, but before * any processing takes place. It may be used to manipulate submitted values. * You have to use the $form_name, which is unique for every for, to * discriminate between various forms * @param string $from_name The form name * @param Form $form The form object */ public function onBeforeHandleForm($form_name, Form $form) { } /** * This function is executed right before a form is submitted. * It may be used in order to add fields or manipulate existing ones * You have to use the $form_name, which is unique for every for, to * discriminate between various forms * @param string $from_name The form name * @param Form $form The form object */ public function onCreateForm($form_name, Form $form) { } /** * This function is executed after a form is submitted and its * values are processed. It may be used to perform any post-processing * tasks. You have to use the $form_name, which is unique for every for, to * discriminate between various forms * @param string $from_name The form name * @param Form $form The form object */ public function onAfterHandleForm($form_name, Form $form) { } /** * This function is called when the controller to be ran is decided, * or the default controller is about to take over. It can be used in two * ways: Either to execute some code when a specific controller (page) is * executed, or to provide a means to access a plugin-specific controller * @param string $ctg The requested controller in the browser, e.g. 'users' or 'lessons' * @return mixed null or a BaseController object */ public function onCtg($ctg) { } /** * This function is called by each controller, every time its index function is executed. * @param BaseController $controller */ public function onControllerIndex(BaseController $controller) { } /** * This function is executed every time an icon list is loaded. Each icon list * has a distinctive name, so the plugin must discriminate based on the $list_name * provided. The icons array is passed by reference, so the plugin may manipulate * the list or add icons. This is usually used to add a plugin icon, which provides * a link to the plugin page * @param string $list_name The icon list name * @param $options The current icon options list */ public function onLoadIconList($list_name, &$options) { } /** * This function is executed every time a lesson dashboard is loaded * @param Course $course The current course * @param Lesson $lesson The current lesson * @param LessonProgress $lesson_progress The progress object (optional) */ public function onLessonDashboard(Course $course, Lesson $lesson, LessonProgress $lesson_progress = null) { } /** * This function is executed every time a course dashboard is loaded * @param Course $course The current course * @param CourseToUser $ctu The user-to-course relationship object * @param array $entries The course's contents */ public function onCourseDashboard(Course $course, CourseToUser $ctu = null, $entries = []) { } /** * This function is executed each time the user sings in to his/her account and the list of his/her courses is displayed * @param array $entries The list of entries (categories, curriculums, courses, lessons) for the user */ public function onMyCoursesList(array &$entries) { } /** * This function is executed each time platform validates the password of a user * @param \Efront\Model\User $user * @param string $password The password that needs verification */ public function onVerifyPassword(\Efront\Model\User $user, $password) { } /** * This function is executed every time the system gets a lesson's options. * It can be used to augment or manipulate the options list, which is passed * by reference * @param Lesson $lesson The lesson used * @param array $lesson_settings The list of current lesson settings */ public function setLessonOptions(\Efront\Model\Lesson $lesson, array &$lesson_settings) { } /** * This function is called in the lesson dashboard, where the professor may change the settings in effect. * It is typically used to append an icon to the settings list, although it can also be used to change the list * itself * @param \Efront\Model\Lesson $lesson * @param array $lesson_settings The settings array, in setting=>label pairs * @param array $lesson_settings_icons The settings array, in setting=>icon pairs */ public function onLessonSettingsList(\Efront\Model\Lesson $lesson, array &$lesson_settings, array &$lesson_settings_icons) { } /** * This function is executed before any controller takes action */ public function onPageLoadStart() { } /** * This function is called before the calendar is displayed. It can be used * to manipulate calendar entries, or add new ones * @param array $calendar_entries The calendar's entries */ public function onLoadCalendar(array &$calendar_entries) { } /** * This function is called from the REST API, using a POST method, * and passes the POST payload to the plugin. It then returns the * plugin's return value to the caller. * @param string $method The method used (currently only POST supported) * @param mixed $data The data passed from the POST request * @return mixed Any response the plugin needs to send to the requester */ public function onApiCall($method, $data) { } /** * Implement this function with all the required queries and actions to * install the plugin */ abstract public function installPlugin(); /** * Implement this function with all the required queries and actions to * remove the plugin */ abstract public function uninstallPlugin(); /** * Implement this function with all the required queries and actions to * upgrade the plugin to a new version */ abstract public function upgradePlugin();
Events are fired in various parts of the script’s lifetime. Each event bears one or more objects as arguments, for example the user that caused the event, the entity it affects etc. Each event has its own associated class, and you can refer to it for a list of the available properties. Below is a list of the currently available events:
const EVENT_BRANCH_CREATED = 'branch_created'; const EVENT_BRANCH_UPDATED = 'branch_updated'; const EVENT_BRANCH_DELETED = 'branch_deleted'; const EVENT_JOB_CREATED = 'job_created'; const EVENT_JOB_UPDATED = 'job_updated'; const EVENT_JOB_DELETED = 'job_deleted'; const EVENT_USER_JOB_ADDED = 'user_job_added'; const EVENT_USER_JOB_REMOVED = 'user_job_removed'; const EVENT_EXTENDED_FIELD_DELETED = 'extended_field_deleted'; const EVENT_USER_TYPE_DELETED = 'user_type_deleted'; const EVENT_DISCUSSION_TOPIC_CREATED = 'discussion_topic_created'; const EVENT_DISCUSSION_TOPIC_UPDATED = 'discussion_topic_updated'; const EVENT_DISCUSSION_CREATED = 'discussion_created'; const EVENT_USER_CREATED = 'user_created'; const EVENT_USER_UPDATED = 'user_updated'; const EVENT_USER_ARCHIVED = 'user_archived'; const EVENT_USER_DELETED = 'user_deleted'; const EVENT_USER_SIGNIN = 'user_signin'; const EVENT_USER_SIGNOUT = 'user_signout'; const EVENT_USER_SIGNUP = 'user_self_signup'; const EVENT_USER_ENABLED = 'user_enabled'; const EVENT_USER_REQUESTED_ACTIVATION = 'user_requested_activation'; const EVENT_USER_REQUESTED_MAIL_ACTIVATION = 'user_requested_mail_activation'; const EVENT_USER_REQUESTED_PASSWORD = 'user_requested_password'; const EVENT_USER_RECEIVED_PERSONAL_MESSAGE = 'user_received_personal_email'; const EVENT_USER_COURSE_COMPLETED = 'user_course_completed'; const EVENT_USER_COURSE_FAILED = 'user_course_failed'; const EVENT_USER_CONTENT_COMPLETED = 'user_content_completed'; const EVENT_USER_CONTENT_FAILED = 'user_content_failed'; const EVENT_USER_LESSON_COMPLETED = 'user_lesson_completed'; const EVENT_USER_LESSON_FAILED = 'user_lesson_failed'; const EVENT_USER_COURSE_REMOVED = 'user_course_removed'; const EVENT_USER_COURSE_ADDED = 'user_course_added'; const EVENT_COURSE_BOOKING_CREATED = 'course_booking_created'; const EVENT_COURSE_BOOKING_CHANGED = 'course_booking_changed'; const EVENT_COURSE_BOOKING_CANCELLED = 'course_booking_cancelled'; const EVENT_EXPRESSED_INTEREST_USER = 'expressed_interest_user'; const EVENT_USER_CURRICULUM_ADDED = 'user_curriculum_added'; const EVENT_USER_CURRICULUM_REMOVED = 'user_curriculum_removed'; const EVENT_USER_CURRICULUM_COMPLETED = 'user_curriculum_completed'; const EVENT_USER_CURRICULUM_FAILED = 'user_curriculum_failed'; const EVENT_USER_CERTIFICATE_AWARDED = 'user_certificate_awarded'; const EVENT_USER_CERTIFICATE_EXPIRED = 'user_certificate_expired'; const EVENT_USER_CERTIFICATE_REVOKED = 'user_certificate_revoked';
Creating URLs
It is generally discouraged to hard-code URLs in eFront, but use the UrlHelperController
class for this task. Here are a few usage examples:
$url = UrlHelperController::url(array('ctg' => 'users', 'edit'=>123)); //will output /users/edit/123 $url = UrlHelperController::extendUrl($url, array('tab'=>'courses')); //will output /users/edit/123/tab/courses $url = UrlHelperController::redirect(array('ctg' => 'users', 'edit'=>123)); //will redirect the user to /users/edit/123
You can create a url/link in a template file, using the {eF_template_url}
smarty function:
{eF_template_url url = ['ctg' => 'users', 'edit'=>123]} {eF_template_url extend = $T_SOME_URL url = ['tab'=>'courses']}
And in javascript:
<script> $.fn.efront('url', {'ctg':'users','edit':123}) </script>
Creating a new Model + Database table
Every database table in eFront must be accompanied from a Class that represents it. For example, let’s consider a table called “plugin_logs” with the following definition:
CREATE TABLE `plugin_logs` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `time` INT(10) UNSIGNED DEFAULT NULL, `title` text, `content` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
For this table, we would create a class called Log
, with the following contents (we use the AcmeReports
namespace, from our earlier example):
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\BaseModel; class AcmeReports extends BaseModel { const DATABASE_TABLE = 'logs'; protected $_fields = array( 'id' => 'id', 'time' => 'timestamp', 'title' => '', 'content' => '', ); public $id; public $timestamp; public $title; public $content; }
Since our class inherits BaseMode
l, it comes with a complete set of functions for performing CRUD operations (Create/Read/Update/Delete). We only have to specify the database table name, and its fields. The protected $_fields
array is a key-value array, where keys are the field names and values are optional type-checking attributes. For example, the line 'id'⇒'id' specifies that, when updating, the value of the “id” element should conform to the restrictions for an “id” value (positive integer). Similarly, 'time' ⇒ 'timestamp' means that the value of the field ‘time’ should be a timestamp (positive integer, 10 digits). When left empty, the value is only checked against the “generic” type, that imposes scalar values (and throws an error for arrays, objects etc).
After creating the class, you can use it to perform CRUD operations:
$log = new Log(); //create an empty Log object $log->setFields(array( 'time'=>time(), 'title'=>'A sample log entry', 'content'=>'This is a sample log entry, for the sake of the example')); $log->save(); //Actually creates the database entry; $id = $log->id; $log2 = new Log($id); //Create a Log object and instantiate from the database $log2->setFields(array('title'=>'Changed title'))->save(); //Update the database entry $log2->delete(); //Delete the database entry
setFields()
and save()
functions to alter a database entry and the delete()
function to delete one. Otherwise, the built-in cache manager may not be updated, leading to unpredictable results.Creating forms
Forms are easy to create and display. The simplest scenario is when we create a form for a class that represents a database table. For our previous Log
example, we could add a form like in the example below:
<?php namespace Efront\Plugin\AcmeReports\Model; use Efront\Model\BaseModel; use Efront\Model\Form; class AcmeReports extends BaseModel { const DATABASE_TABLE = 'logs'; protected $_fields = array( 'id' => 'id', 'time' => 'timestamp', 'title' => '', 'content' => '', ); public $id; public $timestamp; public $title; public $content; public function form($url) { $form = new Form("log_form", "post", $url, "", null, true); try { $form->addElement('text', 'title', translate("Title"), 'class = "form-control"'); $form->addElement('textarea', 'content', translate("Content"), 'class = "form-control ef-editor"'); $form->addElement('submit', 'submit', translate("Submit"), 'class = "btn btn-primary"'); $form->setDefaults($this->getFields()); if ($form->isSubmitted() && $form->validate()) { try { $values = $form->exportValues(); $values['body'] = $form->handleInlineImages($values['body']); $fields = array( 'title' => $values['title'], 'content' => $values['content'] ); $this->setFields($fields)->save(); $form->success = true; } catch (\Exception $e) { handleNormalFlowExceptions($e); } } } catch (\Exception $e) { handleNormalFlowExceptions($e); } return $form; } }
In order to output the form, in your controller you must call the form and assign it to the template:
$log = new Log(); $form = $log->form(UrlHelperController::url(array('ctg'=>'AcmeReports','add'=>1))); $smarty->assign("T_FORM", $form->toArray());
And display it in your template:
{eF_template_printForm form=$T_FORM}
That’s it! The fields will display in the order they were defined.printForm
calls TemplateController::printForm()
which supports very elaborate structures, consult that function’s source for more information.
BaseController
, then calling parent::index()
in your controller will automatically call your model’s form()
function, if the URL contains the /add/1 or /edit/<id> parameters. It will also assign the form to your template, inside the $T_FORM
variable
Javascript functions
eFront comes with a number of high-level javascript functions to simplify common tasks, through the $.fn.efront
extension to jquery
-
Display a modal (popup) window, based on Bootstrap’s modal:
<script> $.fn.efront('modal', { 'header':'My modal title', 'body':'Modal content'}); //Display a modal with this title and content </script>
-
Display a confirmation box, based on bootboxjs:
<script> $.fn.efront('confirm', { 'title':'Confirm', 'body':'Are you sure?', 'success': { 'class_name':'btn-danger', 'label':'Yes', 'callback': function(result) { /*do something if Yes is clicked */ } }, 'fail': { 'callback': function(result) { /*do something if No is clicked */ } } }); </script>
-
Perform an ajax request, with built-in error handling, based on
jquery’s $.ajax()
call:<script> $.fn.efront('ajax', location.toString(), { data:{ 'ajax':1}}, function(response, textStatus, jqXHR) { /*executes on success*/ }, function(response, textStatus, jqXHR) { /*executes on failure*/ } ); </script>
1-minute introduction to smarty
So, it seems that for creating a plugin you need to learn yet another language? Fear not, smarty is painfully easy to use Assigning a variable from PHP to Smarty…
<?php $variable = "John Doe"; $smarty->assign(“T_VAR”, $variable); ?>
…and accesing it in the template file:
Hello mr {$T_VAR}, how are you today?
Conditionals:
<div class = "world"> {if $some_var_value} Goodmorning world! {else} Goodnight world! {/if} </div>
Loops:
<ul> {foreach $array_variable as $value} <li>$value</li> {/foreach} </li>
Variables:
{$simple_var = “John doe”} {capture name = “much_code”} John Doe’s CV: blah blah blah {/capture} Here you can see {$simple_var}’s CV: {$smarty.capture.much_code}
Javascript: Always leave a space after opening brackets, to keep smarty from parsing it as smarty code:
<script>var obj = {name:’test’,value:’test’}</script> //will throw a smarty error <script>var obj = { name:’test’,value:’test’}</script> //Correct!
eFront specific functions:
{eF_template_printBlock data = $data} //Prints $data wrapped inside a standard block {eF_template_printForm form = $form} //Prints $form, where $form is a standard eFront $form array
Had enough yet? Access the Smarty3 manual for the complete documentation