Developing Symbols

While the symbols provided by Magic Data are extensive and growing all the time, they can never be complete. Magic Data is an addon for developers and those wanting specific symbol functionality can easily add symbols specific to their requirement.

Further symbols can be added as root level models or in packages. Each symbol should be a model within the root level or package folder /models/magic_data/symbols/. The name of the file should map to the name of the model class, as per the usual concrete5 naming conventions.

Within the symbol file, the symbol class should extend the class MagicDataSymbolPluginBase or in some cases extend another symbol class.

All of this is easier with an example. Suppose we want to create a symbol AND that will return the logical and of two values.

Copy any existing symbol file into the file /models/magic_data/symbols/and.php. A good file to copy for this may be or.php as that is another logical operation. Edit the file and change the class to be:

Loader::library('magic_data/symbol_plugin_base', 'jl_magic_data');
class MagicDataSymbolsAnd extends MagicDataSymbolPluginBase{ 

We then need the actual symbol and description. As a convention, symbols should be uppercase and, because they are in many ways a simple programming macro language, should not the wrapped in a t() function:

public function  getSymbol(){
    return 'AND'; 
} 

public function getDescription(){ 
    return t('Logical and.');
}

We then need to write some help text for the new symbol, perhaps with a small example:

public function getHelp(){
    return t('If the preceding symbols evaluate to a non-false value and the following item also evaluates to a non-false value, returns the preceding value. Otherwise returns null.');
}

The actual processing of the symbol is made in the function processSymbol(). This receives two parameters, the result of any preceding symbol evaluation and a stack of all following symbols. This stack is passed as a reference &$params, so any changes made to it will be propagated to any following symbol evaluation.

public function processSymbol ($previous_result, &$params){
   $next_param = $this->maybe_following($params);
   $a = $previous_result && $previous_result !== '0' && $previous_result !== 'false' && $previous_result !== 'NULL';
   $b = $next_param && $next_param !== '0' && $next_param !== 'false' && $next_param !== 'NULL';
   if($a && $b) {
     return $previous_result;
   } else {
     return null;
   }
}

Note how $next_param is removed from the stack of following params using maybe_following($params). For a simple parameter, you could just pop it from the stack, but maybe_following($params) will process any following parenthesis, allowing following parameters to be the result of further symbol evaluation.

Not shown in our example, maybe_following($params) also accepts a second parameter that is a condition. When a condition is passed, the parameter is only extracted from the stack if it matches the condition, so facilitating optional parameters such as the optional quantity that many list symbols accept. The various conditions are commented in the source of jl_magic_data/libraries/magic_data/symbol_plugin_base.php.

$a and $b are both evaluated with a consideration for textual equivalents of false. That is not a requirement of Magic Data, but it is the kind of behaviour I like to put into this sort of code to make it more forgiving to those using it. A matter of personal style.

For a true return, I have decided to return the previous result rather than just a Boolean true value. Again, that is not a requirement of Magic Data, just something I decided to do to make it more useful (although if I thought this was a really useful symbol, I would have already included it in the package!)

Again a matter of personal coding style, I like to wrap a class definition inside a test to make sure that class is not already defined and loaded by another package. So putting it all together with the standard concrete5 execute or die line, the complete code for our new symbol now looks like:

defined('C5_EXECUTE') or die("Access Denied.");

if (!class_exists('MagicDataSymbolsAnd')){
Loader::library('magic_data/symbol_plugin_base', 'jl_magic_data');
class MagicDataSymbolsAnd extends MagicDataSymbolPluginBase{

    public function getSymbol(){
        return 'AND';
    }

    public function getDescription(){
        return t('Logical and.');
    }
    public function getHelp(){
       return t('If the preceding symbols evaluate to a non-false value and the following item also evaluates to a non-false value, returns the preceding value. Otherwise returns null.');
    }

    public function processSymbol ($previous_result, &$params){
        $next_param = $this->maybe_following($params);
        $a = $previous_result && $previous_result !== '0' && $previous_result !== 'false' && $previous_result !== 'NULL';
        $b = $next_param && $next_param !== '0' && $next_param !== 'false' && $next_param !== 'NULL';
        if($a && $b) {
            return $previous_result;
        } else {
            return null;
        }
    }

}
}

Visit the Symbols dashboard page and Magic Data will re-scan and find your new AND symbol. If you placed it in a sub-folder of root/models/, you may also need to temporarily turn off the overrides cache to avoid a php error until the re-scan has been made.

While on the Symbols page, run a few tests of your new symbol using the Symbol Tester.

PAGE OWNER EMAIL AS_LINK AND true

PAGE OWNER EMAIL AS_LINK AND false

PAGE OWNER EMAIL AS_LINK AND ( PAGE OWNER ATTRIBUTE a_defined_attr_handle )

PAGE OWNER EMAIL AS_LINK AND ( PAGE OWNER ATTRIBUTE something_unused )

Its not needed for our AND symbol, but if your symbol changes the context or sets a context, you may also want to declare:

public function get_new_context(){
    return 'ContextName';
}

Most of the time you can leave this method out and the previous context will then simply continue. Context is only really important when retrieving an attribute.

Again not needed in our example, if your symbol requires a particular package to be installed (other than its own package or Magic Data), you may also want to declare:

public function plugin_not_available(){
    //test for required packages and return true if requirements are not met
}

If plugin_not_available() returns true, the symbol will not be listed by the plugin manager and will be completely ignored.

The base class for symbols, MagicDataSymbolPluginBase, also provides some utility methods to help when creating new symbols. The most important of these are:

get_cID() - Should always be used to get the current collection (page) id. If you fail to use this method, the symbol will not work properly in the Token symbol evaluation tester.

get_uID() - Should always be used to get the user id. If you fail to use this method, the symbol will not work properly in the Token symbol evaluation tester.

get_current_context() - Returns the current context.

confirm_context($context) - Returns true or false and will report a warning if the context does not match.

report_error($msg), report_warning($msg) and report_note($msg) - Will add messages to the Symbol evaluation trace. The convention here is to pass the class name into the message using get_class($this).

maybe_following($params, $condition) will pop the next parameter from the stack, processing any following parenthesis first, so allowing compound data in parameters. The optional $condition allows for conditional parameters.

See jl_magic_data/libraries/magic_data/symbol_plugin_base.php for details of the methods available.

Last updated: over a year ago