Home » Portfolio » Software » dliCore

dliCore

This is a copy of the README that comes with dliHelper and dliCore

CLI tool

In /bin you will find a script dliHelper.php that can help you to get started even faster. Note that this tool is just started and is subject to change 🙂

Usage: dliHelper.php –method=”methodName” [args]

–method

  • help
  • createPlugin
    –internalName: Used for namespace, class name etc, i.e. myThing
    –pluginName: Public name of the plugin
    –description: Description of plugin
    –author: Name of author
    –supportEmailAddress: Mail address to send support inquires to
  • packagePlugin
    –pluginName: Plugin to package
    –location: Folder to put package in
  • generateClassMap
    –pluginName: Plugin to generate class map for

Create new plugin

To create a new plugin named Foo, just type

./dliHelper.php --method="createPlugin" --pluginName="myFoo" --description="Foo is a plugin built using dliCore" --author="Daniel Liljeberg" --supportEmailAddress="[email protected]"

You will now have a new plugin in you oc-contents/plugins folder named myFoo. It will contain a few folders and files. Your plugin will live under the namespace myFoo, or whatever name you gave your plugin.

index.php is created for you and handles registering your plugin with the dliCore PluginManager. This does not have to be touched except for editing Osclass specific plugin information found in the information block at the top.

Your actual plugin is found in myFooPlugin.php and is an instance of a Plugin class called myFooPlugin.

The plugin will also be configured to allow users to click on the question mark saying “Problems with this plugin? Ask for support.” in the plugins list in the admin area. That will take them to a form where they can request support which will be sent to the mail you provided as the –supportEmailAddress argument.

You can choose to remove this by altering the “Support URI” string in the index.php file information block.

Also, the plugin’s “configure” link will point to the indexAction of the AdminController. The view associated with the controller can be found under views/Admin.

To change which controller the configure link points to you alter the $_configAction variable of your plugin. If no configuration should be visible you can delete this variable and you are then also free to delete the
views/Admin folder and the Controllers/AdminController.php file.

Packaging a plugin

Packaging a plugin helps you get it ready for distribution. It’s completely optional to use but it aids you with several things.

./dliHelper.php --method="packagePlugin" --pluginName="fooBar" --location="/opt/osclass-production/oc-content/plugins"

The above command is used to package the plugin fooBar located in osclassroot/oc-contens/plugins/fooBar and deploy it to
“/opt/osclass-production/oc-content/plugins”

During packaging several things will be checked for you.

  • If your plugin has dependencies to other dliCore based plugins you will be given the option to package these to.
    This helps make sure you don’t rely on functionality in a dependency that has not been released.
  • If your plugin has dependencies to other dliCore based plugins their versions will be checked and if they differ
    from your dependency you will be notified and given the option to update your dependency.
  • If your plugin is under SVN control and local modifications exist you will be notified and given the option to
    “diff” the modifications
    “commit” the modifications
    “ignore” the modifications

Once everything is in a clean state and all checks out your plugin is deployed to your given folder. This can be a folder where you just collect the plugins to zip and upload or another clean OsClass installation used to
test your plugin from a clean state. If your plugin was under SVN control a “checkout” will be made to the destination and the .svn folder removed. If your plugin is not under SVN control a normal copy will occur.

After this is done you will be given the option to generate a class map file for you plugin. This speeds up auto loading and is recommended for production environments.

Lastly, a file containing information regarding the plugin such as dependencies on other plugins etc is created to assist you when writing information about your plugin.

Note that you should have the following php extensions enabled in order to use this tool

  • php-mysqli
  • php-simplexml
  • php-tokenizer
  • php-pdo_sqlite
  • php-zlib
  • php-json
  • php-phar
  • php-iconv
  • php-mbstring

The packager presents the option to minify js and css files so you can work with unminified version during development and have the the packager minify them for your release. Optionally you can also have the packager go through a lot of different file types using Leanify and try to compress them. These include documents, base64 encoded image data in html files, images etc.

APK file (.apk)

    It is based on ZIP.
    Note that modifying files inside APK will break digital signature.
    To install it, you'll have to sign it again.
    If you don't want to modify any files inside APK, use -d 1 option.


Comic book archive (.cbt, .cbz)

    cbt is based on tar. cbz is based on ZIP.


Microsoft Office document 2007-2013 (.docx, .xlsx, .pptx)

    It is based on XML and ZIP.
    Office document 1997-2003 (.doc, .xls, .ppt) is not supported.


Data URI (.html .htm .js .css)

    Looks for data:image/*;base64 and leanify base64 encoded embedded image.


Design Web Format (.dwf, dwfx)

    It is based on ZIP.


EPUB file (.epub)

    It is based on ZIP.


FictionBook (.fb2, .fb2.zip)

    It is based on XML.
    Leanify embedded images.


GFT file (.gft)

    It's an image container format found in Tencent QQ.
    Leanify the image inside.


gzip file (.gz, .tgz)

    Leanify file inside and recompress deflate stream.
    Remove all optional section: FEXTRA, FNAME, FCOMMENT, FHCRC.


Icon file (.ico)

    Convert 256x256 BMP to PNG.
    Leanify PNG inside, if any.


Java archive (.jar)

    It is based on ZIP.


JPEG image (.jpeg, .jpg, .jpe, .jif, .jfif, .jfi, .thm)

    Remove all application markers (e.g. Exif (use --keep-exif to keep it), 
    ICC profile, XMP) and comments.
    Optimize with mozjpeg.


Lua object file (.lua, .luac)

    Remove all debugging information:

    Source name
    Line defined and last line defined
    Source line position list
    Local list
    Upvalue list


OpenDocument (.odt, .ods, .odp, .odb, .odg, .odf)

    It is based on XML and ZIP.


PE file (.exe, .dll, .ocx, .scr, .cpl)

    Leanify embedded resource.
    Remove Relocation Table in executable file.
    Remove undocumented Rich Header.
    Overlap PE Header and DOS Header.


PNG image (.png, .apng)

    Remove all ancillary chunks except for:

    tRNS: transparent information
    fdAT, fcTL, acTL: These chunks are used by APNG
    npTc: Android 9Patch images (*.9.png)
    Optimize with ZopfliPNG.


RDB archive (.rdb)

    It is an archive format found in Tencent QQ.
    Leanify all files inside.


Flash file (.swf)

    Leanify embedded images.
    Recompress it with LZMA.
    Remove Metadata Tag.


SVG image (.svg, .svgz)

    It is based on XML.
    Remove metadata.
    Shrink spaces in attributes.
    Remove empty attributes.
    Remove empty text element and container element.


tar archive (.tar)

    Leanify all files inside.


XML document (.xml, .xsl, .xslt)

    Remove all comments, unnecessary spaces, tabs, line breaks.


XPInstall (.xpi)

    It is based on ZIP.
    Note that modifying files inside xpi will break digital signature. 
    To install it, you'll have to sign it again.


XPS document (.xps, .oxps)

    It is based on XML and ZIP.


ZIP archive (.zip)

    Leanify all files inside and recompress deflate stream using Zopfli.
    Use STORE method if DEFLATE makes file larger.
    Remove extra field in Local file header.
    Remove Data descriptor structure, write those information to Local file header.
    Remove extra field and file comment in Central directory file header.
    Remove comment in End of central directory record.

To have the packager handle pdf conversions it’s recommended to be running Windows and having Microsoft Word installed since that creates the best conversions. On Linux Libre Office is used if found otherwise Pdf Gearbox. But I have noticed that layout and missing fonts often are an issue on Linux.

dliCore Quick Reference

Here are some quick references to components and ideas used in dliCore to help you get your plugins done faster.

Hooks

dliCore plugins support all hooks in Osclass and third party plugins. To have your plugin handle the “header” hook add the following function
to your myFooPlugin class.

public function headerHook() {
    // Code to run
}

In the same way you can add a hook to “user_register_form” by adding the following function.

public function userRegisterFormHook() {
    // Code to run
}

Note that you do not have to do anything more than to create the function to make it work. dliCore will handle the wiring to make sure your function is called when the hook is run.

DocBlock Hook definitions

Alternative ways of defining hooks using a DocBlock for a function in your Plugin also exists to provide flexibility. You can make your function hook the header hook by adding

/**
 * My function description
 *
 * @hook        header
 */

You can set the priority of the hook

/**
 * My function description
 *
 * @hook        header
 * @priority    5
 */

You can run your function under multiple hooks

/**
 * My function description
 *
 * @hook        header
 * @hook        footer
 * @priority    5
 */

You can make each hook have its own priority

/**
 * My function description
 *
 * @hook        header:6
 * @hook        footer:2
 */

You can also write this on a single line by using comma to separate the hooks

/**
 * My function description
 *
 * @hook        header:6, footer:2
 */

You can set restrictions so that your hook only run under certain situations. Restrictions are the Rewrite objects location or location-section, or a dliCore based Plugins route like PluginName-Controller-Action. Since the homepage contains an empty location and section a special case name of ‘index’ is used to refer to that page.

/**
 * My function description
 *
 * @hook        header
 * @restriction login
 * @priority    5
 */

You can add multiple restrictions either as separate tags or by using comma to separate them

/**
 * My function description
 *
 * @hook        header
 * @restriction login, item, FooBar-User-edit
 * @priority    5
 */

As you can see you can create quite elaborate hook rules for you functions.
You can also do all of this in a single tag. To make your function run in the header hook on the login and register-register pages with a priority of 8 and under the footer hook at the item page with a priority of 6 you can write

/**
 * My function description
 *
 * @restrictedHook  header : login, register-register : 8
 *                  footer : item : 6
 */

Each entry should be on a new line. Note that if this is used, any @hook or @restriction tags are ignored. You can also combine things like using the fooBarHook naming convention along with setting the priority in the DocBlock.

If your function is static your Plugin is not instantiated by the hook being called. This can be used for instance if you have some small script or style that should be included on every page load but the entire Plugin is not needed for every page.

/**
 * My function description
 *
 * @hook  header
 */
public static function enqueuStyles() {
    osc_enqueue_style('foobar', $this->getPluginUrl() . 'assets/css/foobar.css');
}

Registering Plugins own hooks

Optionally, you can register hooks your own plugin will call to have them for instance automatically written to the info file during Plugin packaging of the dliDevTools Helper.

To register a hook your Plugin will provide you add the following in your Plugins _init function

$this->_registerHook(new Hook('AfterMessageSent', 'Called after a message has been sent', ['recipientId' => 'id of receiving user']));

To trigger the hook within your plugin you can then use

$this->runHook('AfterMessageSent', 10);

If your Plugin is called myFoo the hook that is actually called will be

myfoo_after_message_sent

In the info file the above hook would add the following entry

myfoo_after_message_sent

    Arguments:
                recipientId
                id of receiving user

This helps users of your plugin identify which hooks you provide and how they function.

Controllers

Controllers are used to separate your logic from your presentation.
Controllers are placed under the Controllers folder and should end in Controller.php. Action functions in your controller will be wired to respond to specific routes and are where you write your logic for a specific action. Their name should end in Action. Let’s say you have a user profile part in your plugin myFoo. If you create the following function

class UserController extends WebSecBaseController
{
    public function profileAction() {

    }
}

this will auto map the function to respond to the route “myFoo-User-profile”. By creating a link using

<a href="<?php echo osc_route_url('myFoo-User-profile')?>">User Profile</a>

you would have a link that would automatically trigger the profileAction function in your UserController. You do not have to manually create new routes etc. dliCore handle all the wiring for you.

For a given plugin you can get a registered route by calling

$fooPlugin->getRoute('User', 'profile');

The returned route then presents a function getName() which would return “myFoo-USer-profile”. Besides this the route also presents additional functions such as getUrl, getAjaxUrl etc.

There are three base controllers you can extend your Controllers from

BaseController
    Can be run by anyone. Should be used for public pages.

WebSecController
    Can only be run by logged in users. Should be used functionality that requires a user to be logged in.

AdminSecBaseController
    Can only be run by logged in administrators. Should be used for handling the administrative back end.

To make sure your Plugin knows about your controllers you specify them in a protected member variable of your Plugin class called _controllers

class FooBarPlugin extends Plugin
{
    protected $_controllers = [ 'FooBar\Controllers\UserController',
                                'FooBar\Controllers\AuthenticatedUserController',
                                'FooBar\Controllers\AdminController'];

This enables the PluginManager to know about and register routes to Controllers without having to instantiate the Plugin.

Views

Views are where you write your presentation and should basically contain your HTML code that goes with any given Action in your Controller. Your view files should be placed under views/{ControllerName}/{ActionName}.php So your view file fo the profileAction of your UserController would reside in

views/User/profile.php

Support for theme specific views exist. If you would like to have a specific view file for the FooBar theme you would place your view file in

themes/FooBar/views/User/profile.php

If a special view file for the active theme is found that will be used. Otherwise the default view located under views/User/profile.php
will be used as a fallback.

Fragments

Fragments are like a special type of view one might say. I could have used this approach for the entire view model, but since Osclass has it’s own way of rendering custom files I didn’t want to break that and instead jack into that for the views. But what if you don’t have a Controller? Perhaps you don’t want to or perhaps you wan’t some piece of HTML to be rendered as part of some hook being run.

This is where Fragments come into play. They allow you to take a file to be rendered and pass data to it.

Let’s say you have a function to add some form element to the item form when a new item is added using the “item_form” hook.

We add the following to our Plugin class.

public function itemFormHook($catId) {
    if(!$this->isActiveForCategory($catId)) {
        return;
    }
    $fragment = new Fragment($this, 'itemForm.php');
    $fragment->checked = false;
    $fragment->render();
}

This will now run when the item_form hook is run and will create a Fragment. The fragment will use the file itemForm.php The contents of itemForm.php could look like this.

<div class="box myFoo_item_form">
    <h2><?php _e("My important option", "myFoo")?></h2>
    <div class="control-group">
        <div class="controls">
            <input id="myImportantOptionStatus" type="checkbox" name="myImportantOptionStatus" <?php echo ($this->checked ? 'checked' : '')?>> <label for="myImportantOptionStatus">THis is my important label</label>
        </div>
    </div>
</div>

As you can see it’s pretty much just HTML code. There is one check to see if $fragment->checked is true or false. If it’s true then we make sure that the checkbox is checked. This allows us to use the same fragment when editing
an item. Fragments can easily be used inside view files etc.

When you create a Fragment you tell it what base path it should use. You can give either a path or an instance of a Plugin. The Fragment will look for files in the base path+"/fragments" folder. To create a Fragment inside your Plugin that should look for its files in the fragments folder of the plugin you just pass it a reference to $this like we did in the example
above.

Support for theme specific fragments exist. If you would like to have a specific fragment file for the FooBar theme you would place
your fragment file in

themes/FooBar/fragments/itemForm.php

If a special fragment file for the active theme is found that will be used. Otherwise the default fragment located under fragments/itemForm.php will be used as a fallback.

Tables

Most plugins saves stuff to tables in the database. To create a table for your plugin you create a class that extends dliLib\Db\AbstractTable. As a convention you can place the class in a sub folder named Tables. This is not a requirement, but I’m considering adding functionality to auto load tables in the same manner as Controllers and will most likely look for them in this folder if that is the case.

Let’s say our plugin stores some extended profile information about a user that we would like to store in the database. To create a table to handle this the class could look like this.

namespace myFoo\Tables;

use dliLib\Db\AbstractTable;

class ExtendedUserProfileTable extends AbstractTable {
    protected $_tableName = 't_myfoo_extended_user_profiles';

    protected function _init() {
        $this->_struct = "CREATE TABLE IF NOT EXISTS /*TABLE_NAME*/ (
                            fk_i_user_id INT UNSIGNED NOT NULL,
                            's_favourite_movie' VARCHAR(128) DEFAULT NULL
                            PRIMARY KEY (fk_i_user_id),
                            FOREIGN KEY (fk_i_user_id) REFERENCES /*TABLE_PREFIX*/t_user (pk_i_id) ON DELETE CASCADE
                        ) ENGINE=InnoDB DEFAULT CHARACTER SET 'UTF8' COLLATE 'UTF8_GENERAL_CI';";
    }
}

We set the table name, excluding the OC prefix, in the $_tableName member variable. Then we set the $_struct member variable to hold the create table struct. This should be done in the _init() function so that the new key word /*TABLE_NAME*/ is known.

To have the plugin create and drop the table when installed and uninstalled, all you have to do is to register the Table with the Plugin.

If we assume you save the table under the Tables folder in the Plugin directory you do the following in the Plugin’s _init() function.

protected function _init() {
    // Register Tables
    $this->_registerTable('myFoo\Tables\ExtendedUserProfileTable', 'ExtendedProfileTable');
}

The first parameter points to the class of your Table, including the namespace for it. The second parameter is a friendly name that you give your Table.

Now, when you plugin is installed the Table will be created and when your Plugin is uninstalled the Table will be removed.

Using this functionality is optional in dliCore and you can create your own DAO if wanted.

Models

To work against your Tables you also have the option to create Database Models. They are essentially objects that you can work with and save etc without manually writing the data of it to the database.

To create a model for our ExtendedUserProfileTable above we would write the following class extending dliLib\Model\DbModel.

namespace myFoo\Models\Google;

use dliLib\Model\DbModel;
class ExtendedUserProfile extends DbModel
{
// Tell the model which table it is connected to
protected static $_dbTableClass = 'myFoo\Tables\ExtendedUserProfileTable';

// Member variables to represent each column name. Note that prefixes such as fk_, s_, i_ etc are removed and
// the column name converted to camelCase with a prefixing _.
protected $_userId = null;
protected $_favouriteMovie = null;
protected $_favouriteQuote = null;

/**
* @return the user id
*/
public function getUserId()
{
    return $this->_userId;
}

/**
* @param int $userId
*/
public function setUserId($userId)
{
    $this->_userId = (int)$userId;
}

/**
* @return the favourite movie
*/
public function getFavouriteMovie()
{
    return $this->_favouriteMovie;
}

/**
* @param string $favouriteMovie
*/
public function setFavouriteMovie($favouriteMovie)
{
    $this->_favouriteMovie = $favouriteMovie;
}
}

As you can see we just supply simple getter and setter functions for our internal variables. In some instances setting a variable could involve more logic and bounds checking for instance. dliLib\Model\DbModel handles the wiring for you and your model is now ready to be used.

// Get current users ExtendedUserProfile
// find() works on the primary key which in this case was the foreign key to the user.
$extendedUserProfile = ExtendedUserProfile::find(osc_current_logged_user_id());

// Make sure this user actually had an extended profile
if($extendedUserProfile) {
    echo "Favourite Movie is " . $extendedUserProfile->getFavouriteMovie() . "<br/>";

    // Update and save data in the model
    $extendedUserProfile->setFavouriteMovie("Robin Hood");
    $extendedUserProfile->save();
}

To delete a model you call the static function delete and pass the instance of the model to delete. After the model has been deleted the model object will be null.

ExtendedUserProfile::delete($extendedUserProfile);

// $extendedUserProfile is now null

Using this functionality, like almost every single mechanism, is optional in dliCore and you can create your own DAO if wanted.

Automatic caching of model objects

dliCore offers a sophisticated automatic caching mechanism (if enabled by the site operator) when models are used. This system automatically keeps track of changes in an object to invalidate the cached object and also invalidates any cache of multiple models that the object was part of.

For instance, say you select a given object with

$foo = FooModel::find(1);

This fetches the FooModel with id 1 into $foo.

Then you might also retreive this object as part of a collection like

$foos = FooModel::findAll();

Now, the instance of $foo with id = 1 within $foo and $foos will be the same. And if you run

$foo->setName('Bar');
$foo->save();

The cache containing the individual FooModel object as well as the collection created when finding all are both invalidated
since an object that is part of the caches has changed.

Avoiding caching of certain properties of an object

Sometimes you might not want every property of an object to be cached. One such time can be when you fetch a collection of other model objects that might “belong” to the main object. But you would not like these objects to be cached inside the main object but instead cached as their own entities to avoid out of sync data between the main object containing the “child” objects and the actual child objects caches. Since the data cached in the main object is cached when the main object is cached and as part of that object. Changing the child object does not invalidate the main objects cache and thus the main object will contain out of sync data.

To avoid this you could easily not store the objects in the main object at all but instead fetch them when needed. For example in FooModel we could have a function

public function getChildObjects() {
    return BarModel::_fetchAll(BarModel::getDbTable()->selectConditions()->where('fk_i_parent_id', $this->_id));
}

Now, the objects are retrieved using the model system, cached and invalidated as normal and will be “correct”. Because of the way models are handled and cached it will also not actually be fetched each time from the database but instead already fetched data will be returned if it exists.

But sometimes you might want to keep an array of the child objects within the parent object. Thankfully dliCore provides a way to achieve this and not run into the above mentioned caching inconsistency issue.

Let’s say you have the following functions in you FooModel

// After load we load all the child objects abd place them into an array within the parent
protected function _afterLoad() {
    $this->_children = BarModel::_fetchAll(BarModel::getDbTable()->selectConditions()->where('fk_i_parent_id', $this->_id));
}

// Before we delete the parent we delete all the children of this parent
protected function _beforeDelete() {
    BarModel::deleteWhere(['fk_i_pranet_id' => $this->_id]);
}

// After we save the parent we set a field in the child referensing this parent and save all the children
protected function _afterSave() {
    // Save all the recipients
    foreach($this->_children as $child) {
        $child->setParent($this);
    }
    BarModel::saveBatch($this->_children);
}

Now, to avoid this array we are working with to be part of a FooModel object that is cached we simply add the following tag to the array in the class.

    /**
     * @var FooModel[]
     * @nocache
     */
    protected $_children;

This @nocache tag will tell the CacheSystem to ignore this specific member of the class when caching the data. So all of the data of the FooModel will be cached, except the children we hold in $_children. They will be cached by their own model when loaded in the BarModel::_fetchAll call instead.

The next time our FooModel is fetched from cache _afterLoad will be called, the BarModel children fetched, which actually
means they will be fetched from cache and then they will once again exist within our FooModel object now fetched from the
cache.

Emails

Creating emails to send in Osclass usually involves adding them to the pages table in the database etc so that they are available for translation. This to is simplified using dliCore. Emails work much like Tables. You create a class that extends dliLib\Email\AbstractEmail. Let’s say we wanted to be able to notify a user if someone thought his/her selected favorite move was boring.

<?php
namespace myFoo\Emails;

use dliLib\Email\AbstractEmail;
class BoringFavouriteMovie extends AbstractEmail
{
    /**
     * Should return the internal Osclass description of the email.
     * This helps you identify it when editing and translating emails.
     *
     * @return string
     */
    protected function _getDescription()
    {
        return 'myFoo - Boring Movie';
    }

    /**
     * Should return the default title/subject of the email
     *
     * @return string
     */
    protected function _getDefaultTitle()
    {
        return '{WEB_TITLE} - Your selected favourite movie is boring';
    }

    /**
     * Should return the default content/body of the email
     *
     * @return string
     */
    protected function _getDefaultContent()
    {
        return "<p>Hi {USER_NAME}!</p>\r\n<p> </p>\r\n<p>A fellow user has indicated that your selected favourite movie {MOVIE_TITLE} is very boring. Consider watching better movies.</p>\r\n<p>This is an automatic email, Please do not respond to this email.</p>\r\n<p> </p>\r\n<p>Thanks</p>\r\n<p>{WEB_TITLE}</p>";
    }
}

Note that you can add any number of dynamic tags here enclosed in {} such as {MOVIE_TITLE}. Then we register the email with the plugin we add it to the Plugins _init() function.

protected function _init() {
    // Register tables
    $this->_registerTable('myFoo\Tables\ExtendedUserProfileTable', 'ExtendedProfileTable');

    // Register emails
    $this->_registerEmail('myFoo\Emails\BoringFavouriteMovie', 'BoringFavouriteMovie');
}

The email will now be added to the database when you install your plugin and removed once your plugin is uninstalled. It can be edited like normal in the Osclass admin under Settings/Email templates.

To send a BoringFavouriteMovie mail you create an instance of it and pass an array with values for the dynamic tags.

$email = new BoringFavouriteMovie(['{MOVIE_TITLE}' => $boringMovieName]);
$email->addTo($userEmailAddress, $userName);
$email->send();

Dependencies

Having plugins dependent on other plugins can be a hassle to handle. dliCore itself becomes a dependency for any plugin built using it. Luckily dliCore has support for handling dependencies of Plugins as well.

First of all, the index.php that is generated by the CLI tool dliHelper.php in the bin folder sets up functions that make sure dliCore exists and explains to any user trying to install the plugin that they need dliCore and where they can download it if they don’t have it.

If your plugin depends on a particular version of dliCore or any other Osclass plugin you can let the system know that to.

In your Plugin’s _init() function you could add

protected function _init() {
    // We depend on having at least version 0.1.5 of dliCore
    $this->_registerDependency(new PluginDependency('dliCore', '0.1.5'));

    // We require Osclass to be at least version 2.3.0
    $this->_registerDependency(new OsclassDependency('2.3.0'));
}

There are several possible dependency types to register for your Plugin. Examples include

MysqlDependency
OsclassDependency
PhpDependency
PluginDependency

With them you can set min and max versions of given dependencies that your Plugin requires. During installation the Plugin will check all of the registered dependencies and report if anyone is not fulfilled. Some dependency types have extra features, like for instance the PluginDependency where you can set a download url to present to the user so they get a link to where they can download the missing dependency etc.

Optional Dependencies

Some functionality of your plugin might depend on some external dependency, but your plugin does not require it to function. Let’s for instance say that you have a plugin that will have to act differently depending on if you have version 1 or 2 of a plugin called Foo installed.

To accomplish this you

$this->_registerDependency(
        (new PluginDependency('Foo', '1.0.0'))->
        setAsOptional('Without PHP you will not be able to run any PHP code'))
    );

Processes

The dliLib\Shell\Process component allows you to execute processes. You can select to execute a Process and wait for it to finish

$process = new Process('ping', ['-n' => 5, 'www.google.com']);
$process->executeAndWait();
echo $process->getStdOut();

or you can select to run the Process asynchronously to have the Process run in the background. This is good for long running Processes.

$process = new Process('ping', ['-n' => 5, 'www.google.com']);
$uid = $process->execute();

When running a Process asynchronously you can set a timeout that will kill the Process if it has been running for longer than this.

// Kill Process if it has been running for longer than 120 seconds
$process->setTimeOut(120);

When running Processes asynchronously you can even re-attach to it in later requests to check it’s status and get output.

$process = ProcessManager::getInstance()->getProcessByUid($uid);

// Check if process is still running
if($process->isRunning()) {
    // Do something
}

// Get output
echo $process->getStdOut();

To facilitate this, Process information, stdout and stderr are stored in the filesystem. This could also have been achieved by some shared memory space or similar. But since that can be turned off, and often is in shared hosting for instance, this approach was selected to makes sure it works for all users without having to have access to and change configurations. It is easy to select to store this in a ram disk for instance to avoid writing to disk. The ProcessManager is responsible for cleaning this up after a Process has completed.

Jobs

Jobs are an extension of Processes. Think of them as cron jobs. Tasks that are recurring at specific intervals. You could keep an eye on this yourself and on every page load check if some task should be carried out or not.
The downside is that if no page loads are done, then you check will never run. Also, running the checks on every page load could affect the perceived performance of your site.

Before, you solved this by adding an entry to a script in your crontab or Windows Task Scheduler. The downside to that is that it requires the end user of your Plugin to have access to and be able to configure those parts of the system. For some people it’s just hard while for others, in some shared hosting environments for instance, it’s actually impossible
since they might not have access to their crontab.

Jobs solves all of these issues by adding a Cron-like manager system to Osclass itself. It will not check your Jobs every page load but instead start an instance of the JobManager in its own Process and have that check and execute your Jobs for you. This results in Jobs that can execute in parallel with the normal operations of your site without affecting the performance of your site in the same way checking and running tasks when a user
loaded a page would.

This also has the benefit that you no longer need to ask your users to edit the crontab to enable your Plugin to run required recurring work. You can just have your Plugin register a Job when it’s installed.

Let’s say we have a Plugin that generates a sitemap used by search engines. To do this it has to run a task to generate the sitemap once every hour. Before, the user would install your Plugin and then to have the sitemap be generated you could do one of two things. Either ask the user to enter a given command in the crontab

0 * * * * php /var/html/osclass/oc-content/plugins/mySitemapPlugin/generateSitemap.php

This would require the user to have access to their crontab or figure out how to schedule this task in their Windows Task Shceduler. It would also require a second step from the users after installing your Plugin before they could actually start to use it. Not very user friendly.

The second solution would be to check if it was time to generate the sitemap on every page load. If it was, the function that generated the sitemap would be run. This would result in some page loads taking a long time since it would have to wait for the sitemaps generation to finish before returning the content to the users browser.

With dliCore and the JobManager you could just add the following to your installHook() function of your Plugin.

$job = new ShellCommandJob('php -f ' . $this->getPluginPath().'generateSitemap.php');
$job->setName('Sitemap Generation');
$job->setSchedule('@hourly');
$this->_registerJob($job);

Now your generateSitemap.php script will be run once per hour. If your plugin is uninstalled the Job will be unregistered automatically.

Note here that we execute php and pass in our script using a ShellCommandJob. This works, but ShellCommandJob was mostly
intended to run other command line commands. If we want to run php code we could instead use a Closure. This is supported through the ClosureJob.

$fn = function() {
    // Code to generate sitemap
};

$job = new ClosureJob($fn);
$job->setName('Sitemap Generation');
$job->setSchedule('@hourly');
$this->_registerJob($job);

Here we create an anonymous function and register that with the job. This function will now be called once every hour. To run scripts you could also register it as a route and execute that route. Let’s assume you have a SitemapController that is registered with you Plugin and it contains a function generateSitemapAction.

class SitemapController extends BaseController
{
    ...

    public function generateSitemapAction() {
        // Code to generate sitemap
    }

    ...
}

You would now have a route available to your generateSitemapAction. So now we could register a Job to execute this given route.

$job = new RouteJob($this->getName().'-Sitemap-generateSitemap');
$job->setName('Sitemap Generation');
$job->setSchedule('@hourly');
$this->_registerJob($job);

Plugin storage

Sometimes your plugin might need to store data. This could be data that should be private or public data like a generated xml file. To allow for easy handling of such storage dliCore offers several functions to get paths to location to store temporary, private and public data. If there is a desire to change the default values of these locations that can be done in the Admin interface under dliCore/Settings.

All files and folders in the plugins data directories are removed when the plugin is uninstalled.

Plugin storage functions

getTempDir($path)
Returns the path to the Plugins temp directory and an optional path within the temp dir. If the path does not exist it will be created.
Temporary files are not to be considered persistent and will be removed at the discretion of the server. Most often at reboot.
All files and folders within the Plugins temp dir will be deleted when the plugin is uninstalled!

getPrivateStorageDir($path)
Returns the path to the Plugins private storage directory and an optional path within the temp dir. If the path does not exist it will be created.
All files and folders within the Plugins private storage directory will be deleted when the plugin is uninstalled! This folder should point to a folder that the client canb’t access. So for instance, the web browser should not be able to access this folder. If possible this should be configured to point to a directory outside of the public html folder.

getPublicStorageDir($path)
Returns the path to the Plugins public storage directory and an optional path within the temp dir. If the path does not exist it will be created.
All files and folders within the Plugins public storage directory will be deleted when the plugin is uninstalled! Plugins should use the public storage directory to store files that should be publicly accessible for instance dynamic data, xml files, images etc.

getPublicStorageUrl($path)
Returns url to existing public storage directory

Plugin storage related Hooks

When reconfigured under Settings in dliCore Admin hooks are fired to give plugin developers the option to react to the change. The developer will not have to manage moving files, but for instance, Processes that are locking files in that folder might have to be terminated before the move etc.

dlicore_pre_plugins_temp_base_directory_changed

Description:
Called before moving existing plugin files when the base directory for temporary files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path
dlicore_pre_plugins_private_base_directory_changed

Description:
Called before moving existing plugin files when the base directory for plugins non public files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path
dlicore_pre_plugins_public_base_directory_changed

Description:
Called before moving existing plugin files when the base directory for plugins public files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path
dlicore_post_plugins_temp_base_directory_changed

Description:
Called after moving existing plugin files when the base directory for temporary files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path
dlicore_post_plugins_private_base_directory_changed

Description:
Called after moving existing plugin files when the base directory for plugins non public files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path
dlicore_post_plugins_public_base_directory_changed

Description:
Called after moving existing plugin files when the base directory for plugins public files are changed

Arguments:

    oldPath
     String containing old path

    newPath
     String containing new path

Additional Components

There are many more components available in dliCore such as a Caching Manager, Forms and Html element wrappers, Html Widgets, FileManager, menu builders etc.

But the ones mentioned above are the most fundamental. I’m adding new, refining and re-writing existing components all the time.

Ideas and questions can be directed to me.

Docker Development Image

To quickly enable local development of plugins I have created a Docker image with PHP and MariaDB.

Note that it is NOT intended for production use, only local development. It allows you to test against multiple versions of PHP including

Version | Protocol | Port
===========================
5.3.29 | http | 8053
5.3.29 | https | 8153
5.4.44 | http | 8054
5.4.44 | https | 8154
5.5.38 | http | 8055
5.5.38 | https | 8155
5.6.30 | http | 8056
5.6.30 | https | 8156
7.0.17 | http | 8070
7.0.17 | https | 8170
7.1.3 | http | 8071
7.1.3 | https | 8171

All versions have X-debug installed for debugging and PHP 5.6 also comes with Z-Ray configured so you can profile your application.
If running windows you can place the bellow script into a file like rundevenv.bat and place it in the root of your project.
DEV_CLIENT_IP and DEV_CLIENT_PORT should be the LAN ip of your computer and the port your IDE is listening to debug connections
on.

@ECHO OFF
SET DEV_CLIENT_IP=192.168.0.111
SET DEV_CLIENT_PORT=9000

PowerShell.exe -Command "& docker run --rm -t -i -v $PWD\:/var/www:rw -v $PWD/dbdata:/var/lib/mysql -v $PWD/etc/mysql:/etc/mysql -v $PWD/etc/apache2:/etc/apache2 -e DEV_CLIENT_IP="%DEV_CLIENT_IP%" -e DEV_CLIENT_PORT=%DEV_CLIENT_PORT% -e MYSQL_ROOT_PASSWORD="root" -p 8051:8051 -p 8052:8052 -p 8053:8053 -p 8054:8054 -p 8055:8055 -p 8056:8056 -p 8070:8070 -p 8071:8071 -p 8151:8151 -p 8152:8152 -p 8153:8153 -p 8154:8154 -p 8155:8155 -p 8156:8156 -p 8170:8170 -p 8171:8171 -p 3306:3306 inquam/phpdev"
PAUSE

When you run this script an apache web server will start with the different PHP versions on the specified ports, MariaDB will also run. /etc/mysql and /etc/apache2 will be created in your project root to allow you to edit config files and the database files will be stored in a folder named dbdata under your project root.

Note that you must have Docker installed before running this command.
To shut down the image just press ctrl-c.

Edit your config.php to include these directives in order to work with this environment

/** MySQL hostname */
define('DB_HOST', 'localhost');

/** Database Table prefix */
define('DB_TABLE_PREFIX', 'oc_');

define('REL_WEB_URL', '/');

define('WEB_PATH', (isset($_SERVER['HTTPS']) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https' ? "https" : "http"). '://localhost:' . (!empty($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '8056') . '/');