Site Tools

 
 
 

how_to_create_a_plugin

Plugin Guide

Quick start

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:

  1. Make sure you have a working eFrontPro installation, and you have access to its files (with permission to change them). See Installation for instructions on how to install eFrontPro.
  2. Visit your eFrontPro system, sign in as an administrator and click on Plugins.
  3. Click on the Create plugin button. Enter a name1), 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 eFrontPro's 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.
  4. The plugin has been created inside the server that hosts eFrontPro. You can find its files at the same location where eFrontPro 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.

Introduction

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 eFrontPro 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 eFrontPro installation for development purposes, contact us.

Creating your first plugin

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 eFrontPro 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

Heads up! 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)

Heads up! 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)

The file plugin.ini is used by the system to initialize the plugin. In our case, it should contain the following:

[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 eFrontPro version
compatibility = 4.1.0    ;maximum compatible eFrontPro version
author = John Doe
description = This is a fully-functional plugin that is created as part of eFrontPro's 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 eFrontPro 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 eFrontPro that this plugin has been tested on. You can see your current eFrontPro 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() {
	}
}

Heads up! You always have to implement the 3 abstract functions shown above, even if they’re left empty

That’s it! All we have to do now is to install the plugin into our eFrontPro system (if it’s not already installed). This can be done with 2 ways:

  • Go to Admin → Plugins → Install new plugin, and click to install your plugin, which you must have compressed as a zip file.

Note that you have to compress the files inside the plugin folder and not the folder itself

  • Or copy the plugin files directly to the www/plugins/ folder on the eFrontPro 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 eFrontPro 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');

Heads up! Although you can directly update data in the database table, it is strongly discouraged to do so, because eFrontPro utilizes an automatic caching mechanism, and directly changing the database will not update cached entries. Always use $obj→setFields() to change an object’s properties and $obj→delete() to delete an entry.

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:

Heads up! 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://efrontpro.example.com/ and we request http://efrontpro.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.

Heads up! If you use a name of a controller that already exists, the system will never reach it, since plugins are checked after all controllers have failed to initialize.

Now that we have a target for our link, let’s create the controller itself, which must extend the 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.

Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-initial

We are now ready to get down on implementing the actual logic of our plugin. As we described earlier, Acme wants 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 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:

Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-step1

On to the next requirement.

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:

Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-step2

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.

Heads up! Users, courses and lessons may be “archived”, meaning that they have a value set for the “archive” field. This signifies that these entries are set as “deleted” and moved to the archive. Special care should be taken, so that such entries are never taken into account for calculations.

We must create now a drop-down box in our template like this:

 <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>

Heads up! Giving the ef-select class to any <select> element, will convert it to a searchable drop-down

The tricky part now is to have this drop-down box interact with the grid, so that it displays only users that possess the selected course but have not completed it. We will employ some javascript and jquery magic for this:

<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:

Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-step3

On to our next task: Create an export and email it to the specified user

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", 
                    Configuration::getValue(Configuration::CONFIGURATION_MAIN_URL), AcmeReports::PLUGIN_NAME), 
                'body' => dtranslate("Hello %s, Please find attached the report exported on %s", 
                    $user->formatted_name, 
                    formatTimestamp(time()), AcmeReports::PLUGIN_NAME),
                '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 a MailQueue instance. See Sending emails for more information.

Here’s how our plugin page looks now, after clicking on the “Email list” button:

Heads up! The plugin code, as it is created until now, can be found in Github, as AcmeReports-step4

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 VERSION constant 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:

  1. Make sure you implement AbstractPlugin::overridesTemplate() inside your Plugin class (AcmeReportsPlugin in our example):
     public function overridesTemplate() {return dirname(__DIR__).'/View/';}
  2. 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
  3. 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 -->
  4. 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 eFrontPro 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.

Heads up! Make sure your plugin only handles the correct form, by checking its name, otherwise it will process all forms!

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 eFrontPro 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:

  1. 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
  2. Visit the page /<plugin>/parse_language/1 (For example, http://efrontpro.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.
  3. Open each .po file with poedit. Poedit can be found here.
  4. Click on the “Update” button to parse the plugin files and fill the list of strings that need translation.
  5. Translate all string literals.
  6. Press the “Save” button to save your work. Make sure you have write access to the folders created in step 1.
  7. Restart your server, in case that the translations aren’t changed.

Heads up! Make sure that the file “translations.php” is a valid PHP file. If it’s not, check your template files again. All translations must be enclosed in double quotes, not single.

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)
}

Heads up! The above snippet will send the message directly. If you prefer to add it to the send queue (suitable for batch emails, to avoid the delay), then you should simply create and save a QueuedEmail object.

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.

Heads up! If your plugin throws an exception while handling an event, using the 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 yourself

Upgrading

A plugin typically requires upgrade, when its associated database schema needs changing. The plugin upgrading process can be automated in eFrontPro 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.

  1. Edit the plugin class file that extends AbstractPlugin and change the VERSION constant to a larger number, for example 1.1
  2. 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;
          }
    [...] 
  3. 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.
  4. 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;
          }
    [...]

Heads up! When changing the version, make sure you change it inside your plugin’s plugin.ini file. In addition, don’t forget to make the same database changes inside your onInstall() function, and add proper routines in onUninstall(), if needed

More plugin examples

The Demo plugin

eFrontPro 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

eFrontPro 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

Considerations

User types and restrictions

There are 3 basic user types in efront, which determine the interface that displays to the user: Administrator, professor and student. 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 professors:

[...]
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
}

Heads up! Permission levels vary from 0 (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 professor 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

Heads up! 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 eFrontPro 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, eFrontPro 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 eFrontPro version

Coding standards

eFrontPro 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

Heads up! Call 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 says error_reporting( E_ERROR ); to error_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()

Heads up! Streamline your ajax responses, by calling the controller’s jsonResponse() function.

Plugin API Reference

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();

Event Reference

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';

Core API Notes

Creating URLs

It is generally discouraged to hard-code URLs in eFrontPro, 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 eFrontPro 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 BaseModel, 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).

Heads up! If you don’t specify a type check, when saving an object the passed value will be XSS-filtered. If you want to keep any HTML code present in the value, then use the ‘wysiwig’ value for the field.

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

Heads up! Always use the 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.

Heads up! If you use a controller that inherits from 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

eFrontPro comes with a number of high-level javascript functions to simplify common tasks, through the $.fn.efront extension to jquery

  1. 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>
  2. 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>
  3. 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!

eFrontPro 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 efrontpro $form array

Had enough yet? Access the Smarty3 manual for the complete documentation

1) Spaces and special or international characters are not allowed for the plugin name