Pines Development

3.3. Core Services

3.3.1. Config (config class)
3.3.2. Depend (depend class)
3.3.3. Hook (hook class)
3.3.4. Info (info class)
3.3.5. Menu (menu class)
3.3.6. Page (page class)

3.3.1. Config (config class)

The config service loads configuration for both Pines and any components/templates. The configuration for Pines is always loaded, and components' configuration is loaded the first time it is accessed, during each script run. Config will start by loading the defaults.php file (if it exists) in the component's directory. The return value from this file is an array which is used to load configuration variables. Each array entry should be an associative array containing the following entries:

Table 3.3. Config Array Entries

KeyRequiredDescription
nameYesThe configuration option's name. This is used to access it from code.
cnameYesA common name. This is displayed to the user when setting the option.
descriptionYesA description of the configuration option.
valueYesThe default value of the configuration option.
optionsNoAn array of the possible values of the option.
peruserNoA boolean determining whether the option can be set on a per user basis.


[Tip]Tip

Calling a function in a defaults.php file will use extra resources when the configuration variable for the component is first accessed on each request, even if the configuration has been set to a non-default value. To speed up your component, avoid calling functions in defaults.php.

Config will then load the config.php file (if it exists) in the component's directory. This file is generated by the configurator service and stores user specified configuration. The file is structured the same, except that each entry only contains "name" and "value".

The configuration object is built using these files, and placed into a property (named after the component) on the first attempted access of the property. Pines' system configuration options are placed in the config object itself.

When the user changes a config option, they may only change the option to a value of the same type as the default value. For example, if the option's default value is an integer, the value can only be changed to another integer.

Accessing Configuration for Pines and Components

// Access Pines' "system_name" configuration option:
echo $pines->config->system_name;

// Access a component's "website_name" configuration option:
echo $pines->config->com_example->website_name;

// Access a template's "tagline" configuration option:
echo $pines->config->tpl_green->tagline;
[Caution]Caution

Configuration options are not sanitized. When you are inserting them into HTML, they should be escaped with htmlspecialchars().

3.3.2. Depend (depend class)

The depend service manages and runs dependency checkers. It includes several dependency checkers by default.

Table 3.4. Built In Dependency Checkers

NameDescription
abilityCheck that the current user has certain abilities.
actionCheck the currently running or originally requested action.
classCheck whether classes exist.
clientipCheck the client's IP address.
componentCheck whether components/templates are installed, enabled, and are certain versions.
extensionCheck whether PHP extensions are installed, enabled, and are certain versions.
functionCheck whether functions exist.
hostCheck the requested server hostname. Uses the hostname provided by the client in the HTTP request.
optionCheck the currently running or originally requested component.
phpCheck PHP's version.
pinesCheck Pine's version.
requestCheck the requested component and action. (Combination of "option" and "action".)
serviceCheck the available system services.


To check a dependency, use the check() method. It will return true if the value passes the check, or false if it does not. To retrieve a checker's help array, with information about the checker, use the help() method.

Using the Dependency Checker

if (!$pines->depend->check('component', 'com_example>=2.0|com_sample')) {
    pines_notice('This feature is only available when you have com_example 2.0 or higher, or com_sample.');
    return;
}
if (!$pines->depend->check('extension', 'mysql|mysqli')) {
    pines_notice('It appears the MySQL extension is not installed. Please install it to continue.');
    return;
}
$help_array = $pines->depend->help('extension');

Adding a new dependency checker is easy. Add a new entry to the checkers variable, using the name of the checker as the key, and a callback to the checker function as the value. The callback function should return true if the check passes and false if it does not. The first argument to a checker is a value to check. However, the second argument, if set to true, means the callback function should return a help array.

This should usually be done using an init script. That will ensure the checker is available by the time an action is run.

Adding a New Checker

function com_example__check_something($value, $help = false) {
    if ($help) {
        $return = array();
        $return['cname'] = 'Barbecue Checker';
        $return['description'] = <<<'EOF'
Check that something is a barbecue.
EOF;
        $return['syntax'] = <<<'EOF'
Any value will do, but to pass, it must be barbecue.
EOF;
        $return['examples'] = <<<'EOF'
stick in the mud
:   Check if "stick in the mud" == "barbecue".
EOF;
        $return['simple_parse'] = false;
        return $return;
    }
    return ($value == 'barbecue');
}

$pines->depend->checkers['something'] = 'com_example__check_something';

// Call the new checker like this:
// $pines->depend->check('something', 'a value'); // Would return false.
[Caution]Caution

Since unprivileged users are often allowed to run custom dependency checkers, it is important to not allow disclosure of sensitive data using a checker. For example, an unprotected file checker could be used to find files on the host machine.

[Caution]Caution

Be careful that a checker does not recursively call itself, which could result in an infinite loop.

All the built in dependency checkers use simple_parse() to understand simple logic. The syntax is simple, including and "&", or "|", not "!", and grouping using parentheses "()". Here's an example of how to use simple_parse() in the example checker.

Using the Simple Parser

function com_example__check_something($value, $help = false) {
    global $pines;
    if ($help) {
        $return = array();
        $return['cname'] = 'Barbecue Checker';
        $return['description'] = <<<'EOF'
Check that something is a barbecue.
EOF;
        $return['syntax'] = <<<'EOF'
Any value will do, but to pass, it must be barbecue.
EOF;
        $return['examples'] = <<<'EOF'
stick in the mud|hot dogs
:   Check if "stick in the mud" or "hot dogs" == "barbecue".
EOF;
        $return['simple_parse'] = true;
        return $return;
    }
    if (
            strpos($value, '&') !== false ||
            strpos($value, '|') !== false ||
            strpos($value, '!') !== false ||
            strpos($value, '(') !== false ||
            strpos($value, ')') !== false
        )
        return $pines->depend->simple_parse($value, 'com_example__check_something');
    return ($value == 'barbecue');
}

$pines->depend->checkers['something'] = 'com_example__check_something';

// Call the new checker like this:
// $pines->depend->check('something', 'a value|barbecue'); // Would return true.

The checker's help array must contain the following values.

Table 3.5. Dependency Checker Help Array Values

NameDescription
cnameA common name to present to users.
descriptionA description of the checker. This is formatted with Markdown.
syntaxAn explanation of the checker's syntax. This is formatted with Markdown.
examplesExamples of values that can be checked with the checker. This is formatted with Markdown.
simple_parseA boolean. Whether this checker uses the simple_parse feature.


If you'd like to include your checker in your component's class, it is better to consider the resources used by including a callback directly to your component's method. Component classes are loaded as soon as the object is first accessed. Instead of using a callback to the method, you can use a shortcut function to save Pines from loading your component's class during each script run.

Using a Shortcut Function to a Method

// Using an extra function to call the real checker keeps the com_example
// class from being loaded on each script run.
function com_example__check_something($value) {
    global $pines;
    return $pines->com_example->something($value);
}

$pines->depend->checkers['something'] = 'com_example__check_something';
[Note]Note

Many things use the dependency checker, including menu entries, the package manager, conditional groups, and conditional configuration just to name a few.

3.3.3. Hook (hook class)

The hook service provides method hooking for the entire $pines object and most classes. Method hooking allows a component to intercept function calls to alter arguments, alter return values, and/or change the actual function being called.

How Hook Works

Hook uses a complex technique to override an object and allow all its public methods to be hooked. When hook_object() is called and passed an object, it begins by using PHP's reflection API to analyze the object and build a new object. The class used to build the new object is the hook_override__NAMEHERE_ class. Hook will create a new class based on this class by replacing "_NAMEHERE_" with the name of the class. Each method on the original object is recreated in the new class. The replacement methods, when called will run the callbacks associated with that hook and the original method on the original object. Once the new class is complete, it is loaded, and an object is created using it. The new object stores the original object, and replaces the variable holding that object. The new object now resides in place of the original object.

When a variable is requested from the object, the new object will pass the request directly to the original object. When the object is requested as a string, invoked, or cloned it will also request it from the original object. When the object is cloned, it will hook the clone as well.

[Caution]Caution

Calls from within a method of the original class to another method of the class will not be intercepted. This includes all calls to private methods. Static methods cannot be hooked either.

[Caution]Caution

Functions which return or check the class name of the object will use the class name of the new object, and could therefore return unwanted results. For example, a hooked user object would return false with is_a($user, 'user'). You can check for the class hook_override_user or check hook_override just to see if it is hooked.

How to Use Hooks

To hook an object, so calls to its methods can be intercepted, use hook_object(). The prefix will be used when setting up callbacks for the hooks.

Hooking an Object
$widget = new com_example_widget;
$pines->hook->hook_object($widget, 'com_example_widget->', false);
[Note]Note

$pines, along with all objects within it (except the hook, depend, config, and info services) are automatically hooked. Most components are designed to automatically hook their classes using the factory() static method.

To set up a callback, use add_callback(). A callback is called either before a method runs or after. The callback is passed an array of arguments or return value which it can freely manipulate. If the callback runs before the method and sets the arguments array to false (or causes an error), the method will not be run. Callbacks before a method are passed the arguments given when the method was called, while callbacks after a method are passed the return value (in an array) of that method.

The callback can receive up to 5 arguments, in this order:

  1. &$arguments

    An array of either arguments or a return value.

  2. $name

    The name of the hook.

  3. &$object

    The object on which the hook caught a method call.

  4. &$function

    A callback for the method call which was caught. Altering this will cause a different function/method to run.

  5. &$data

    An array in which callbacks can store data to communicate with later callbacks.

A hook is the name of whatever method it should catch. A hook can also have an arbitrary name, but be wary that it may already exist and it may result in your callback being falsely called. In order to reduce the chance of this, always use a plus sign (+) and your component's name to begin arbitrary hook names. E.g. "+com_games_player_bonus".

If the hook is called explicitly, callbacks defined to run before the hook will run immediately followed by callbacks defined to run after.

A negative $order value means the callback will be run before the method, while a positive value means it will be run after. The smaller the order number, the sooner the callback will be run. You can think of the order value as a timeline of callbacks, zero (0) being the actual method being hooked.

Additional identical callbacks can be added in order to have a callback called multiple times for one hook.

The hook "all" is a pseudo hook which will run regardless of what was actually caught. Callbacks attached to the "all" hook will run before callbacks attached to the actual hook.

Adding a Callback to a Hook
function com_example__callback() {
    // Do something.
}

// Run the callback before something() is called on com_example.
// Note the single quotes. Double quotes would be parsed by PHP and not work.
$pines->hook->add_callback('$pines->com_example->something', -10, 'com_example__callback');

// Run the callback before delete() is called on com_example_widget.
$pines->hook->add_callback('com_example_widget->delete', -1, 'com_example__callback');

// Run the callback after every method call is caught.
$pines->hook->add_callback('all', 10, 'com_example__callback');

To manually run callbacks (without an originating function call), use run_callbacks().

Manually Run Callbacks
$pines->hook->run_callbacks('+com_games_player_bonus');

3.3.4. Info (info class)

The info service provides information about Pines and installed components/templates. When Info is loaded, the info.php file in the system folder is retrieved. This file is used to fill variables in the info object. Similarly, when a component's variable is accessed, Info will look for an info.php file in the component's directory. It will use this file to create an object for that component. The template variable will contain the info of the current template. The info file should return an associative array with the following entries:

Table 3.6. Info Array Entries

KeyRequiredDescription
nameYesThe component's name. This is the name displayed to the user.
authorYesThe component's author.
versionYesThe component's version.
licenseNoThe URL or name of the license under which the component is released.
websiteNoThe URL of the component's website.
servicesNoAn array of the names of services the component provides.
short_descriptionNoA short description (one line) of the component.
descriptionNoA description of the component.
dependNoAn associative array of dependencies which the component requires. The keys are the names of the dependency checker to use, and the values are the values to check.
conflictNoAn associative array of dependencies which the component conflicts with. The keys are the names of the dependency checker to use, and the values are the values to check.
recommendNoAn associative array of dependencies which the component recommends. The keys are the names of the dependency checker to use, and the values are the values to check.
abilitiesNoAn array of abilities used by the component. Each array has three entries, a name, a title, and a description, in that order.


Example Info File

defined('P_RUN') or die('Direct access prohibited');

return array(
    'name' => 'System Configurator',
    'author' => 'SciActive',
    'version' => '1.0.0',
    'license' => 'http://www.gnu.org/licenses/agpl-3.0.html',
    'website' => 'http://www.sciactive.com',
    'services' => array('configurator'),
    'short_description' => 'Manages system configuration',
    'description' => 'Allows you to edit your system\'s configuration and the configuration of any installed components.',
    'depend' => array(
        'pines' => '<2',
        'component' => 'com_jquery&com_ptags&com_pgrid&com_pform'
    ),
    'recommend' => array(
        'service' => 'entity_manager&user_manager'
    ),
    'abilities' => array(
        array('edit', 'Edit Configuration', 'Let the user change (and see) configuration settings.'),
        array('editperuser', 'Edit Per User Configuration', 'Let the user change (and see) per user/group configuration settings.'),
        array('view', 'View Configuration', 'Let the user see current configuration settings.'),
        array('viewperuser', 'View Per User Configuration', 'Let the user see current per user/group configuration settings.')
    ),
);

Accessing Info for Pines and Components

// Access Pines' "license" info entry:
echo $pines->info->license;

// Access a component's "description" info entry:
echo $pines->info->com_example->description;

// Access a template's "version" info entry:
echo $pines->info->tpl_green->version;

3.3.5. Menu (menu class)

The menu service builds and renders menus. In Pines, menus are built using arrays, and these arrays are usually stored in JSON files. Menu will begin by loading the menu.json file in the system directory. This will set up the main menu and its submenus. It will then load any menu.json files found in all components' directories. The JSON structure of a menu file is an array of objects (because JSON doesn't support associative arrays) which are each placed into the menu_arrays variable in the menu object. Each entry is either a top level menu, or a menu entry. Top level menus require a position in which to place them, as well as several other values:

Table 3.7. Top Level Menu Array Entries

KeyRequiredDescription
pathYesThe name of the menu's path.
textYesThe text to title the menu's module.
positionYesThe page position in which to place the menu.
sortNoBoolean value of whether the menu should be sorted alphabetically.
dependNoAn associative array of dependencies required to show the menu.


Regular menu entries have these values:

Table 3.8. Regular Menu Array Entries

KeyRequiredDescription
pathYesThe path of this menu entry. Paths are hierarchical like file paths, using forward slashes as separators.
textYesThe text to show in the menu entry.
sortNoBoolean value of whether the menu should be sorted alphabetically.
hrefNoA URL, or an array of arguments to pass to pines_url(), which the entry will link to.
targetNoThe HTML target of the link. Defaults to "_self".
onclickNoJavaScript which will run when the entry is clicked.
dependNoAn associative array of dependencies required to show the entry.


An extra dependency called "children" can be used on menu entries in order to show the menu only if it has visible children.

To add a menu entry using code, you can insert it into the menu_arrays variable.

Adding a Menu Entry Through Code

// Using pines_url().
$pines->menu->menu_arrays[] = array(
    'path' => 'main_menu/example',
    'text' => 'Example',
    'href' => array('com_example', 'something', array('id' => $some_id)) // These are used as arguments for pines_url().
);

// Using a URL. This is placed under the above entry. It only appears when the user is logged in. It opens in a new window.
$pines->menu->menu_arrays[] = array(
    'path' => 'main_menu/example/google',
    'text' => 'Google',
    'href' => 'http://google.com',
    'target' => '_blank',
    'depend' => array(
        'ability' => ''
    )
);

The system init script i80menus.php is responsible for loading the JSON files, by calling add_json_file() for the system menu file, then each component's menu file. This means that menu entries are not loaded until after all init/kill scripts and requested actions have run. This means that if you need to alter a component's menu entries (as opposed to just adding submenus and entries), you must hook Menu's add_json_file() or render() method.

Once all menu entries have been loaded, render() will remove entries when their dependencies are not met. It will convert the entries into multi-dimensional arrays for each menu using their path values. It then passes these arrays to the menu() method of the current template and places the return value into a module in the menu's requested position.

Example menu.json File

[
    {
        "path": "main_menu/other/example",
        "text": "Example",
        "depend": {
            "children": true
        }
    },
        {
            "path": "main_menu/other/example/widgets",
            "text": "Widgets",
            "href": ["com_example","widget/list"],
            "depend": {
                "ability": "com_example/listwidgets"
            }
        },
        {
            "path": "main_menu/other/example/newwidget",
            "text": "New Widget",
            "href": ["com_example","widget/edit"],
            "depend": {
                "ability": "com_example/newwidget"
            }
        }
]

3.3.6. Page (page class)

The page service manages modules, notices, and errors, and renders content to be output to the client by the template. Modules, which are objects that use views to generate content, are attached to Page in specific positions. A template's info object will have a list of positions the template supports. Page uses PHP's output buffering system to retrieve the output from modules and templates. Page also controls the title of the page.

Page has several methods for manipulating the page title.

  • title

    Append text to the title.

  • title_set

    Replace the title.

  • title_pre

    Prepend text to the title.

  • get_title

    Retrieve the title.

Page has several methods for displaying notices and errors to the user. Page only manages and organizes them, as it is ultimately the responsibility of the template to display the messages to the user.

  • notice

    Add a notice to be displayed to the user.

  • get_notice

    Get the array of notices.

  • error

    Add an error to be displayed to the user.

  • get_error

    Get the array of errors.

Modules are segments of the page, which must be attached to the page in order to have their view content rendered and sent to the client. When a module is attached to Page, a position must be specified. Though module attachments are usually handled by the module when it is created, you can manually attach a module to Page. The second argument of attach_module() is the position in which to place the module. You can also manually detach the module. If the module is attached to the same position twice, it must likewise be removed twice.

Manually Attaching and Detaching a Module

$module = new module;

// Attach the module.
$pines->page->attach_module($module, 'content');

// Detach the module.
$pines->page->detach_module($module, 'content');

The template will need to place the module content in the appropriate place on the page. To do this, templates use render_modules(), providing a position. If needed, the template can also provide a model, which will be used to format the content as necessary.

Rendering the Modules in a Position Using a Model

<div id="left">
    <?php echo $pines->page->render_modules('left', 'module_sidebar'); ?>
</div>

Under special circumstances, you may need to only output certain data to the client. For example, when you are returning JSON data to an Ajax request. To do this, set the override variable to true, then pass the content to override_doc().

Overriding the Page

// Turn override on.
$pines->page->override = true;

// Output JSON data.
$pines->page->override_doc(json_encode($var));

// If you need to retrieve the data:
$data = $pines->page->get_override_doc();

The page is rendered when the system init script i90render.php calls render(). If the page is overridden, the override document is returned. Otherwise, each module is rendered individually, then the template's template.php file is run. Because each module is rendered before the page, a module can cause another module to be attached to the page. However, that module will ultimately be rendered during the page's rendering. Therefore, if that module then causes another module to be attached in a position that has already been rendered, the third module will not be output to the user.

[Note]Note

In order to allow it to be hooked, render() only returns the page output. The init script then echoes it to the user.