Pines Development

Chapter 4. System Services

Table of Contents

4.1. System Services
4.1.1. Configurator
4.1.2. Editor
4.1.3. Entity Manager
4.1.4. Icons
4.1.5. Log Manager
4.1.6. Template
4.1.7. Uploader
4.1.8. User Manager
4.2. Creating a Service

System services provide many features, and allow these features to be implemented in many different ways. System services are not provided by Pines Core. They are, however, well defined and must conform to certain guidelines to ensure that any component designed to use them will work with any implementation.

4.1. System Services

4.1.1. Configurator

The configurator service allows the user and other components to edit/save configuration and enable/disable components. In Pines, a component is disabled by prepending a period (.) to its directory's name. This is the same way the directory is hidden on Linux file systems.

The configurator itself has the following methods:

  • disable_component

    Disables a component. This is accomplished by prepending a dot to its directory's name. Accepts the name of the component as the only argument.

  • enable_component

    Enables a component. This is accomplished by removing the dot from the beginning of its directory's name. Accepts the name of the component as the only argument.

  • list_components

    Creates and attaches a module which lists configurable components. It also returns the created module.

Enabling and Disabling Components

// Enable the component com_good.
$pines->configurator->enable_component('com_good');

// Disable the component com_bad.
$pines->configurator->disable_component('com_bad');

4.1.1.1. Configurator Component

The configurator also provides a class for retrieving and manipulating a component's configuration. This class is the configurator_component class. It provides the static method factory, which accepts the component's name as the only argument and returns a new instance of configurator_component for the provided component. The class constructor also accepts this argument, but using the factory method is recommended.

The component configurator class has these properties:

  • defaults

    The configuration defaults.

  • config

    The current configuration.

  • config_keys

    The current configuration in an array with key => values.

  • info

    The info object of the component.

  • name

    The component.

The component configurator class provides these methods:

  • get_full_config_array

    Get a full config array. (With defaults replaced.)

  • is_configurable

    Check if a component is configurable.

  • is_disabled

    Check if a component is disabled.

  • print_form

    Print a form to edit the configuration.

  • print_view

    Print a view of the configuration.

  • save_config

    Write the configuration to the config file.

  • set_config

    Set the current config by providing an array of key => values. Any value not provided will be set back to default.

Changing a Component's Configuration
// Load the component's configuration.
$conf = configurator_component::factory('com_example');
// Set the new configuration.
$conf->set_config(array(
    'host' => $host,
    'user' => $user,
    'password' => $password,
    'database' => $database
));
// Save the new configuration to disk.
if (!$conf->save_config())
    pines_error('Couldn\'t save config.');

When the configuration is saved, the configurator creates a config.php file in the component's directory (if it does not already exist), and saves a script to the file, which returns the configuration values as an array of associative arrays. Each associative array contains an entry called "name" and an entry called "value". The config service uses this file when building the component's configuration, replacing values from defaults.php with the values from this file.

Example config.php File
<?php
defined('P_RUN') or die('Direct access prohibited');
return array (
  0 => 
  array (
    'name' => 'notice_text',
    'value' => 'Notice: you have exceeded the limits.',
  ),
  1 =>
  array (
    'name' => 'limits',
    'value' => 
    array (
      0 => 9.33,
      1 => 13.6,
    ),
  ),
);
?>

4.1.2. Editor

The editor service provides a more friendly HTML editor for the user. When editing some sort of content, which is to be displayed in a page or email, a textarea is usually not the best solution. The editor service provides a method which allows the system administrator to choose between multiple HTML editors to provide for the system's users.

Once the editor is loaded, it will transform any textareas with the class "peditor", "peditor-simple", or "peditor-email" into HTML editors. If it provides a simplified editor, textareas with the class "peditor-simple" will use this editor instead. If it provides an email editor, textareas with the class "peditor-email" will use this editor instead. Email editors will format content for emails. This includes full URLs for links and images, no stylesheets, etc.

The editor has a method, load, which loads the editor. This method should be called from a view, to ensure it is only called when outputting HTML. The editor also has a method, add_css, which takes one argument, the URL of a stylesheet to use for styling WYSIWYG content.

Using the Editor Service in a View

<?php
/**
 * Prints editors.
 *
 * @license http://www.gnu.org/licenses/agpl-3.0.html
 * @author Hunter Perrin <hunter@sciactive.com>
 * @copyright SciActive.com
 * @link http://sciactive.com/
 */
/* @var $pines pines *//* @var $this module */
defined('P_RUN') or die('Direct access prohibited');
$this->title = 'Editors';
// Load the editor.
$pines->editor->load();
// Load a custom stylesheet.
$pines->editor->add_css($pines->config->location.'components/com_example/includes/custom.css');
?>
<div>Regular Editor</div>
<div><textarea rows="3" cols="35" class="peditor"></textarea></div>
<br />
<div>Simple Editor</div>
<div><textarea rows="3" cols="35" class="peditor-simple"></textarea></div>
<br />
<div>Email Editor</div>
<div><textarea rows="3" cols="35" class="peditor-email"></textarea></div>

4.1.3. Entity Manager

Go brew some coffee or drink an energy drink, cause this is without a doubt, the most complex and important part of Pines! Understanding the entity manager lets you build complex relationships between data as easily as setting the value of a property.

4.1.3.1. Introduction

The entity manager service provides database abstraction for Pines. Data in Pines is based on objects called entities. An entity is an object which can hold any type of data available in PHP (except resources), including other entities. All entity classes (and therefore all entities) inherit the entity class.

Components can freely access the database if they wish, however it is strongly discouraged, as this prevents portability and extensibility. Using entities to store data has several benefits for both developers and site operators.

  • Site operators can choose which database backend to use, based on their own needs.

  • Moving from one database to another, backing up and restoring a database, and mirroring databases are much easier through an entity manager.

  • Data storage is much easier and faster to code.

  • Data querying is much easier and faster to code.

  • A component can store data in another component's entities and extend its functionality.

  • The user manager provides access control for all entities automatically.

Entities are not strictly structured, so data of any type can be added and saved just by assigning a variable on the entity and calling save. This makes data manipulation in Pines very easy.

All entities are given a globally unique identifier (GUID), which is an integer (but may not be in the future). No entities in the same Pines installation will ever have the same GUID. The entity manager also provides UIDs, which can be used to number certain types of entities (or practically anything else). UIDs can be used to provide a more visibly pleasing identifier for entities.

Entities are organized with tags. For example, if a component, com_blog, wanted to store posts using an entity, it may add the tags "com_blog" and "post" to all new instances. This allows components to select and differentiate their entities from other components' entities.

An entity's class is usually named by appending to their component's name. In the previous example, the class would probably be called com_blog_post. When selecting entities with the entity manager, the class used to retrieve them must be specified, or the entity class will be used. If an entity is referenced in another entity's variable, the class is saved along with the GUID. When this variable is accessed later, the entity manager will retrieve the referenced entity using the saved class.

[Caution]Caution

When changing an entity's class name, any entities referencing it must be resaved after setting the reference again using the new class name.

4.1.3.2. Creating Entities

To create an entity, call the factory static method of an entity's class. The entity class takes a list of tags as arguments. However, if an author chooses, the factory method of an inheriting class can take any other arguments. It will usually take only one argument, a GUID. It would return the entity with the GUID or, if it didn't exist or no GUID was provided, a new entity. The factory method is used instead of the new keyword to provide entities a chance to set up hooking on their objects. To check that an entity is a new entity (it hasn't been saved), check that the GUID has not been set.

Creating an Entity
// Using the entity class.
$new_entity = entity::factory();

// Using a custom class.
$blog_post = com_blog_post::factory();

// Check that the entity is new.
if (isset($new_entity->guid))
    pines_notice('This entity is not a new entity!');

Much like blogs in many blogging systems, entities are organized using tags. Entity tags are not supposed to be added by the user, as this could allow them to create or manipulate users, groups, configurations, etc. It may seem inviting to use the entity tags to store user provided data, but don't! Though not strictly necessary, it is highly recommended to give every entity your component creates a tag identical to your component's name, such as 'com_xmlparser'. You don't want to accidentally get another component's entities.

Be very cautious when saving an entity in another entity's variable. If the referenced entity is newly created and does not have a GUID, the entity manager will not be able to retrieve it later. Always save the referenced entity first.

Saving a Referenced Entity the Wrong Way
$entity = entity::factory();
$entity->foo = entity::factory();

$entity->save(); // foo hasn't been saved yet!
$entity->foo->bar = 'It works!';
$entity->foo->save();

$guid = $entity->guid;
unset($entity);
$entity = $pines->entity_manager->get_entity($guid);

isset($entity->foo->guid); // False
echo $entity->foo->bar; // Outputs nothing.
Saving a Referenced Entity the Right Way
$entity = entity::factory();
$entity->foo = entity::factory();

$entity->foo->bar = 'It works!';
$entity->foo->save(); // Saving the referenced entity first! :)
$entity->save(); // now foo has been saved.

$guid = $entity->guid;
unset($entity);
$entity = $pines->entity_manager->get_entity($guid);

isset($entity->foo->guid); // True
echo $entity->foo->bar; // Outputs 'It works!'.
[Caution]Caution

Since the reference entity's class name is stored in the reference on the entity's first save and used to retrieve the reference entity using the same class, if you change the class name in an update, you need to reassign the reference entity and save to storage.

When an entity is loaded, it does not request its referenced entities from the entity manager. This is done the first time the variable/array is accessed. The referenced entity is then stored in a cache, so if it is altered elsewhere, then accessed again through the variable, the changes will not be there. Therefore, you should take great care when accessing entities from multiple variables. If you might be using a referenced entity again later in the code execution (after some other processing occurs), it's recommended to call clear_cache.

4.1.3.3. Entities

As mentioned before, entities are organized using tags. To add, remove, and check tags, the methods add_tag, remove_tag, and has_tag are used, respectively. Each takes any number of tags or an array of tags as arguments.

Manipulating Entity Tags
$entity = entity::factory();

$entity->add_tag('com_foobar', 'foo', 'bar');
$entity->has_tag('foo'); // True

$entity->remove_tag('foo', 'bar');
$entity->has_tag('foo'); // False

To clear the cache of referenced entities, so that the next time one is accessed it will be pulled from the database, use the clear_cache method.

Clearing the Reference Entity Cache
$entity = entity::factory();
$entity->foo = entity::factory();
$entity->foo->bar = 'Old value.';
$entity->foo->save();
$entity->save();

$entity = $pines->entity_manager->get_entity($entity->guid);

$inst_of_foo = $pines->entity_manager->get_entity($entity->foo->guid);
$inst_of_foo->bar = 'New value.';
$inst_of_foo->save();

echo $entity->foo->bar; // Outputs 'Old value.'
$entity->clear_cache();
echo $entity->foo->bar; // Outputs 'New value.'

Much like clearing the entity cache, you may need to refresh the entity's own data. Use the refresh method for this.

Refreshing an Entity's Data
$entity = entity::factory();
$entity->foo = 'Old value.';
$entity->save();

$inst_of_ent = $pines->entity_manager->get_entity($entity->guid);
$inst_of_ent->foo = 'New value.';
$inst_of_ent->save();

echo $entity->foo; // Outputs 'Old value.'
$entity->refresh();
echo $entity->foo; // Outputs 'New value.'

As you've already seen in the examples, to save an entity, use the save method. Likewise, to delete the entity, use the delete method. You can also call the save_entity, delete_entity, and delete_entity_by_id methods of the entity manager itself. Entities use these methods, which allows hooking of all entity saves and deletes.

Different Ways of Saving and Deleting Entities
$entity = entity::factory();

// Save the entity.
$entity->save();
// or
$pines->entity_manager->save_entity($entity);

// Delete the entity.
$entity->delete();
// or
$pines->entity_manager->delete_entity($entity);
// or
$pines->entity_manager->delete_entity_by_id($entity->guid);

Several methods are provided by entities to find and compare them with other data. Entities cannot simply be checked using the == operator. It will almost always fail because of things like entity caching and sleeping references. Instead, you can use the following entity methods.

  • is

    Perform a less strict comparison of two entities. To return true, the entity and the object passed must meet the following criteria:

    They must be entities.
    They must have equal GUIDs. (Or both can have no GUID.)
    If they have no GUIDs, their data must be equal.
  • equals

    Perform a more strict comparison of two entities. To return true, the entity and the object passed must meet the following criteria:

    They must be entities.
    They must have equal GUIDs. (Or both can have no GUID.)
    They must be instances of the same class.
    Their data must be equal.
  • in_array

    Check whether the entity is in an array. Takes two arguments, the array and a boolean $strict. If $strict is false, the function uses is to compare, and if it's true, the function uses equals.

  • array_search

    Search an array for the entity and return the corresponding key. Takes two arguments, the array and a boolean $strict. If $strict is false, the function uses is to compare, and if it's true, the function uses equals. This method may return 0, which evaluates to false, so you should use in_array if you are only checking whether the entity is in the array.

Comparing Entities
$entity = entity::factory();

$entity->is($entity); // True


$array = array('foo' => null, 'bar' => $entity);

echo $entity->array_search($array); // Outputs 'bar'

The entity manager provides three methods to sort entities. Each one uses a different method of sorting.

  • hsort

    Sort an array of entities hierarchically by a specified property's value.

  • psort

    Sort an array of entities by parent and a specified property's value. If they have the same parent, their own value is used.

  • sort

    Sort an array of entities by a specified property's value.

Sorting Entities
$cats = $pines->entity_manager->get_entities(
        array('class' => com_foobar_category),
        array('&',
            'tag' => array('com_foobar', 'category')
        )
    );

$case_sensitive = false;
$reverse = false;

// Sort categories hierarchically by their name.
$pines->entity_manager->hsort($cats, 'name', 'parent', $case_sensitive, $reverse);

// Sort categories by their parent's name.
$pines->entity_manager->psort($cats, 'name', 'parent', $case_sensitive, $reverse);

// Sort categories by their name.
$pines->entity_manager->sort($cats, 'name', $case_sensitive, $reverse);

4.1.3.4. Entity Querying

The real power behind the entity manager is the entity querying system. After all, what's the use of having complex data and relationships if you can't find it?

The simplest way to get an entity is usually its factory static method. Almost all entities provided by SciActive maintained components use a factory that takes a GUID as an argument. However, the author of an entity's class can use whatever they like as arguments. For example, the user class takes either a GUID or a username. Usually the method will return a brand new entity if the queried entity is not found.

[Tip]Tip

If you're using a user provided value as the GUID, casting it to an int will ensure it is evaluated as a GUID. Also, non-numeric values will be evaluated as 0, resulting in a new entity, since no entity has GUID 0. Then you can determine if it was found by checking that guid is set.

Getting Entities Using the Factory Method
// Most entities use a GUID.
$baz = com_foobar_baz::factory((int) $_REQUEST['id']);

if (!isset($baz->guid)) {
    pines_notice('The specified baz cannot be found!');
    return;
}

// When selecting a user, you can use a GUID or a username.
$cron_user = user::factory('cron');

if (!isset($cron_user->guid)) {
    pines_notice('Can\'t find the cron user! Have you created one yet?');
    return;
}

// If the script made it to this point, both $baz and $cron_user were found!
pines_notice('Hooray! I found the baz you wanted and the cron user!');

The really powerful way of querying entities is the entity manager's get_entities and get_entity methods.

The first argument to these functions is an options array. It is an associative array of key value pairs signifying options. The options array can be empty, in which case the entity class will be used for returned entities. The following is a list of options which all entity managers support.

Table 4.1. Entity Querying Options

OptionTypeDefaultDescription
classstring"entity"The class used to create each entity. It must have a factory static method that returns a new instance.
limitintnullThe limit of entities to be returned. Not needed when using get_entity, as it always returns only one.
offsetint0The offset from the oldest (or newest if reversed) matching entity to start retrieving.
reverseboolfalseIf true, entities will be retrieved from newest to oldest. Therefore, offset will be from the newest entity.
sortstring"guid"How to sort the entities. Accepts "guid", "cdate", and "mdate".
skip_acboolfalseIf true, the user manager will not filter returned entities according to access controls.


[Note]Note

When using skip_ac, any referenced entities, when accessed, will also be retrieved using skip_ac.

A component or entity manager implementation is free to add extra options, but it should consider prepending the component's name to the name of the option. For example, if your component, com_entityversions, wants to add the option "date", it should use "com_entityversions_date".

Every argument following the options array is a selector. A selector is an associative array of criteria. An entity must pass each selector's criteria to be returned. The first member of the array (the value at index 0) is the type of selector, which determines how the criteria are matched against an entity's data.

Table 4.2. Entity Selector Types

TypeNameDescription
&AndAll values in the selector must be true.
|OrAt least one value in the selector must be true.
!&Not AndAll values in the selector must be false.
!|Not OrAt least one value in the selector must be false.


The following members of the array are the criteria of the selector. They must be in the form $selector['name'] = $value, or $selector['name'] = array($value1, $value2,...). They can be negated by prepending a bang (!) to the name, such as $selector['!tag'] = 'user'.

Table 4.3. Entity Selector Criteria

NameValueConditionExample SelectorMatching Entity
guidA GUID.True if the entity's GUID is equal.array('&', 'guid' => 12)$entity->guid = 12;
tagA tag.True if the entity has the tag.array('&', 'tag' => 'com_foobar')$entity->add_tag('com_foobar');
issetA name.True if the named variable exists and is not null.array('&', 'isset' => 'foo')$entity->foo = 0;
dataAn array with a name, then value.True if the named variable is defined and equal.array('&', 'data' => array('foo', false))$entity->foo = 0;
strictAn array with a name, then value.True if the named variable is defined and identical.array('&', 'strict' => array('foo', 0))$entity->foo = 0;
arrayAn array with a name, then value.True if the named variable is an array containing the value. Uses in_array().array('&', 'array' => array('foo', 'bar'))$entity->foo = array('bar', 'baz');
matchAn array with a name, then regular expression.True if the named variable matches. Uses preg_match().array('&', 'match' => array('foo', '/bar/'))$entity->foo = 'foobarbaz';
gtAn array with a name, then value.True if the named variable is greater than the value.array('&', 'gt' => array('foo', 5))$entity->foo = 6;
gteAn array with a name, then value.True if the named variable is greater than or equal to the value.array('&', 'gte' => array('foo', 6))$entity->foo = 6;
ltAn array with a name, then value.True if the named variable is less than the value.array('&', 'lt' => array('foo', 7))$entity->foo = 6;
lteAn array with a name, then value.True if the named variable is less than or equal to the value.array('&', 'lte' => array('foo', 6))$entity->foo = 6;
refAn array with a name, then either a entity, or a GUID.True if the named variable is the entity or an array containing the entity.array('&', 'ref' => array('foo', 12))$entity->foo = com_foobar_baz::factory(12);


So putting it all together, you can specify any of the options, and any number of selectors to find the exact entities you want.

Getting Entities by Querying
// Get all entities using the entity class.
$entities = $pines->entity_manager->get_entities();

// Get only the first entity.
$entity = $pines->entity_manager->get_entity();

// Get the last entity.
$entity = $pines->entity_manager->get_entity(array('reverse' => true));

// Get all baz entities, using the com_foobar_baz class.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz')
        )
    );

// Get the five newest baz entities.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz, 'reverse' => true, 'limit' => 5),
        array('&',
            'tag' => array('com_foobar', 'baz')
        )
    );

// Get baz entities with names.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz'),
            'isset' => 'name'
        )
    );

// Get baz entities without names.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz'),
            '!isset' => 'name'
        )
    );

// Get baz entities with either first names or last names.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz')
        ),
        array('|',
            'isset' => array('first_name', 'last_name')
        )
    );

// Get baz entities created since yesterday.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz'),
            'gt' => array('p_cdate', strtotime('-1 day'))
        )
    );

// Get baz entities with names, who either make not greater than 8 dollars pay or are under 22.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz'),
            'isset' => 'name'
        ),
        array('!|', // at least one must be false
            'gte' => array('age', 22),
            'gt' => array('pay', 8)
        )
    );

// Get baz entities named Clark, James, Chris, Christopher, Jake, or Jacob.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz),
        array('&',
            'tag' => array('com_foobar', 'baz')
        ),
        array('|',
            'strict' => array(
                array('name', 'Clark'),
                array('name', 'James')
            ),
            'match' => array(
                array('name', '/Chris(topher)?/'),
                array('name', '/Ja(ke|cob)/')
            )
        )
    );

// Get all baz entities, regardless of whether the logged in user has access to them.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz, 'skip_ac' => true),
        array('&',
            'tag' => array('com_foobar', 'baz')
        )
    );

4.1.3.5. Exporting and Importing Entities

All entity managers provide a method for exporting and importing entities. This makes it very easy to backup, restore, duplicate, and move an entire database of entities, even with different database backends.

The entity manager has the following methods for exporting and importing entities:

  • export

    Export entities to a file.

  • export_print

    Export entities to the client as a downloadable file.

  • import

    Import entities from a file.

Exporting and Importing Entities with the Entity Manager
@set_time_limit(3600); // Make sure the script doesn't terminate before the entities are exported/imported.

// Export entities to a file.
$pines->entity_manager->export($filename);

// Export entities to the client.
$pines->entity_manager->export_print();

// Import entities from a file.
$pines->entity_manager->import($filename);

The format of an entity export file is simple, and readable by all entity managers. The file extension, PEX, stands for Pines Entity eXchange. Here is an example of an exported entity file.

Example Entity Export File (PEX)
# Pines Entity Export
# com_pgentity version 1.0.0
# sciactive.com
#
# Generation Time: Tue, 05 Jul 2011 09:56:14 -0700
# Pines Version: 0.94.0beta

#
# UIDs
#

<com_sales_sale>[1]
#
# Entities
#

{1}[com_user,user,enabled]
    p_cdate="d:1309884676.464271068572998046875;"
    p_mdate="d:1309884676.464293003082275390625;"
    abilities="a:2:{i:0;s:10:\"system\/all\";i:1;s:14:\"com_user\/login\";}"
    groups="a:0:{}"
    inherit_abilities="b:1;"
    address_type="s:2:\"us\";"
    addresses="a:0:{}"
    attributes="a:0:{}"
    username="s:5:\"admin\";"
    salt="s:32:\"7d5bc9dc81c200444e53d1d10ecc420a\";"
    password="s:32:\"f066801b0b70491f397ad37b51b7e278\";"
    name_first="s:6:\"Hunter\";"
    name_middle="s:0:\"\";"
    name_last="s:6:\"Perrin\";"
    name="s:13:\"Hunter Perrin\";"
    email="s:20:\"hunter@sciactive.com\";"
    phone="s:0:\"\";"
    fax="s:0:\"\";"
    timezone="s:0:\"\";"
    address_1="s:0:\"\";"
    address_2="s:0:\"\";"
    city="s:0:\"\";"
    state="s:0:\"\";"
    zip="s:0:\"\";"
    address_international="s:0:\"\";"

4.1.3.6. UIDs

UIDs provide an easier way for users to identify entities. UIDs are just sequential numbers and can be used for anything you like, not just entities. As opposed to a GUID, which is a unique ID for all entities, a UID is only unique for its own sequence. This allows them to stay small in a database filled with other entities.

UIDs can be named anything. A good naming convention, in order to avoid conflicts, is to use your component's name, a slash, then a descriptive name of the sequence being identified for non-entity sequences, and the name of the entity's class for entity sequences. E.g. "com_example/widget_hits" or "com_hrm_employee".

The entity manager has the following methods for handling UIDs:

  • delete_uid

    Delete a unique ID from the system.

  • get_uid

    Get the current value of a unique ID.

  • new_uid

    Increment or create a unique ID and return the new value.

  • rename_uid

    Rename a unique ID.

  • set_uid

    Set the value of a unique ID.

Using UIDs
// Create a new entity.
$entity = com_foobar_baz::factory();

// Now give it a more friendly ID than GUID.
$entity->id = $pines->entity_manager->new_uid('com_foobar_baz');
$entity->save();
[Caution]Caution

If a UID is incremented for an entity, and the entity cannot be saved, there is no safe, and therefore, no recommended way to decrement the UID back to its previous value.

4.1.4. Icons

The icon service provides a set of icons available to use through CSS classes. Pines Icons come in two sizes, 16x16 and 32x32. To use an icon, you add the class of the icon, such as picon-document-edit, to an HTML element. This will use the icon's 16x16 size. To use the 32x32 size, add another class, picon-32.

The icon service has one method, load, which loads the icon CSS. This method should be called from a view, to ensure it is only called when outputting HTML.

Using the Icon Service

<?php
/* @var $pines pines *//* @var $this module */
defined('P_RUN') or die('Direct access prohibited');
// Load the icons CSS.
$pines->icons->load();
?>
<div style="float: left; width: 32px; height: 32px; margin: 1em;" class="picon-32 picon-document-sign"></div>
<p>Your order can't be shipped until your contract is signed.</p>

<button type="button" class="btn">
    <span class="picon-dialog-ok" style="background-repeat: no-repeat; padding-left: 18px;">&nbsp;</span>Go
</button>

4.1.5. Log Manager

The log manager service provides an easy and simple logging feature for Pines. The default log manager can use the hook system to log all calls to an object under $pines. The log manager has no restriction on the backend it can use, so log managers can be made for flat files, MySQL, PostgreSQL, MSSQL, Oracle, etc.

The log manager has one method, log, which takes two arguments, the log entry and the level of the entry. The level can be one of the following, ordered in increasing severity.

  • debug

    Provide information that could be useful while debugging.

  • info

    Provide information that might be useful for a user. This is the default.

  • notice

    Provide notice of an event or action that has occurred that the user may want to know.

  • warning

    Warn the user of something that may be wrong or bad, but not necessarily an error.

  • error

    Provide notice of an error that has occurred.

  • fatal

    Provide notice of an error, from which the script cannot recover.

Making an Entry in the Log

$user = user::factory();
$user->username = 'bob';
$user->password('changeme');

$pines->log_manager->log('Trying to save a new user.'); // The log level defaults to info.
if ($user->save())
    $pines->log_manager->log("Saved user {$user->username}.", 'notice');
else
    $pines->log_manager->log("Couldn't save user {$user->username}.", 'error');

4.1.6. Template

The template service is a special service, which does not have to declare itself as the template service. Instead, Pines reads the configuration and determines the template. The template builds the layout and design of the page and inserts the content from any modules attached to the page. It also has the responsibility of building the output of the menus. Templates will be discussed in much greater depth in Templates in Part II.

The template only needs two methods.

  • menu

    Formats a menu so it can be printed into content.

  • url

    Creates a URL, utilizing the Pines URL rewriting setup. Has the following arguments:

    1. The name of the component to be requested by the URL.

    2. The name of the action to be requested by the URL.

    3. An associative array of parameters to be used as query data in the URL.

    4. Boolean determining if the URL should be a full URL. Otherwise, it will be a URL relative to the server of the Pines installation.

Formatting a URL Using the Template Service

// A URL suitable for a link on a page in Pines.
$link = $pines->template->url('com_example', 'widget/edit', array('id' => $widget->guid));
// or you can use the shortcut function:
$link = pines_url('com_example', 'widget/edit', array('id' => $widget->guid));

// A URL suitable for a link in an external page or an email.
$link = $pines->template->url('com_example', 'widget/edit', array('id' => $widget->guid), true);

// The URL of the home page of the Pines installation.
$link = pines_url();

Templates also have some properties which define how output is constructed/viewed.

  • editor_css

    The location of a stylesheet to be used by WYSIWYG editors.

  • format

    The format of the template. This format is used by Pines to find the view when a module is being rendered.

The template format uses dashes to separate its parts, and describes the language, format, version, etc. of the view. The following are some example formats and a description of their language and what they're for.

  • html-desktop-5

    HTML 5 for desktops.

  • html-mobile-4-01

    HTML 4.01 for mobile screens.

  • html-desktop-xhtml-1-0-strict

    XHTML 1.0 Strict for desktops.

  • xml-rss-2-0

    RSS 2.0.

For more information on templates, you can see Templates in Part II.

4.1.7. Uploader

The uploader service provides a convenient way for the user to upload and manage files on the server. The intent of the uploader service is to allow files to be uploaded and selected using an Ajax enabled interface for ease of use.

Once the uploader is loaded, it will transform any text inputs with the class "puploader" into Ajax file uploaders/selectors. If you would like the user to be able to select folders, add the "puploader-folders" class. If you would like the user to be able to select multiple files, add the "puploader-multiple" class. Add the class "puploader-temp" to emulate a single file upload. Only one file will be allowed, and it will be placed into a temporary directory. Note that the check method is not needed when you are using a temp file. To use the uploader in a view, call the load method. This method should be called from a view, to ensure it is only called when outputting HTML.

Using the Uploader Service in a View

<?php
/**
 * Prints an uploader.
 *
 * @license http://www.gnu.org/licenses/agpl-3.0.html
 * @author Hunter Perrin <hunter@sciactive.com>
 * @copyright SciActive.com
 * @link http://sciactive.com/
 */
/* @var $pines pines *//* @var $this module */
defined('P_RUN') or die('Direct access prohibited');
$this->title = 'Uploader';
// Load the uploader.
$pines->uploader->load();
?>
<div>Choose or Upload a File on the Server</div>
<div><input class="puploader" name="upload_file" type="text" value="" /></div>
<div>Upload a New File</div>
<div><input class="puploader puploader-temp" name="temp_file" type="text" value="" /></div>

The value of the field when the form is submitted will be the relative URL of the selected file. When selecting multiple files, they will be separated by two forward slashes "//". After receiving the path of the file, it should always be checked using the check method of the uploader service (unless it is a temp upload). This will verify that the user didn't maliciously alter the URL. It can then be translated to a real file path using the real method, and back to a URL again with the url method. The url method takes an extra argument, which determines whether a full URL should be returned instead of a relative URL.

Verifying and Using a URL Returned by the Uploader

$file_url = $_REQUEST['upload_file'];

// The first step is always checking the file URL.
if (!$pines->uploader->check($file_url)) {
    pines_notice('Invalid file path submitted!');
    return;
}

// Now get the real file path.
$file_path = $pines->uploader->real($file_url);

// And the full URL of the file.
$file_url_full = $pines->uploader->url($file_path, true);
[Warning]Warning

Using a submitted URL without first checking it could allow a user to bypass safety features and exploit a server, potentially giving them access to confidential information.

When you accept a temp upload, use the temp method to get the real file path to the uploaded file.

Using a Temp File Returned by the Uploader

$file = $_REQUEST['temp_file'];

// Get the path to the temporary location of the file.
$file_path = $pines->uploader->temp($file);
if (!$file_path) {
    pines_notice('Invalid file path submitted!');
    return;
}

// Now you can move the file into a permanent location with rename().

4.1.8. User Manager

The user manager service is one of Pines' most powerful features. It has many features and responsibilities, designed to separate and protect data. Fully understanding the user manager will allow you to create complex systems which function safely in a multi-user environment.

The user manager has several methods to help access and organize user and group data.

  • fill_session

    Fill the $_SESSION['user'] variable with the logged in user's data. It also sets the default timezone used by date functions to the user's timezone.

  • get_groups

    Get the system's groups. It takes one argument, which, when true, will cause it to return both disabled and enabled groups, rather than just enabled groups.

  • get_users

    Get the system's users. It takes one argument, which, when true, will cause it to return both disabled and enabled users, rather than just enabled users.

  • group_sort

    Sort an array of groups hierarchically. An additional property of the groups can be used to sort them under their parents. It takes up to four arguments:

    1. The array of groups.

    2. The name of the property to sort groups by. Null for no additional sorting.

    3. Boolean, determines whether to sort case sensitively.

    4. Boolean, determines whether to reverse the sort order.

  • login

    Logs the given user into the system. Takes only one argument, the user.

  • logout

    Logs the current user out of the system.

  • print_login

    Creates and attaches a module which lets the user log in. Takes up to two arguments, the position of the new module, and a URL to which the user will be redirected after logging in.

  • punt_user

    Kick the user out of the current page. This method completely terminates execution of the script when it is called. Code after this method is called will not run. Though not required, user managers will usually give the user an opportunity to log in and return to the page, if they are not currently logged in. This method takes up to two arguments, a message to display to the user, and the URL they should use to retry after logging in. If no message is given, a generic message will be displayed. To prevent a message, use an empty string.

Two of the methods of the user manager, gatekeeper and punt_user, have shortcut functions to make calling them easier. Unlike most shortcut functions in Pines, which have "pines_" prepended to their names, these two functions have identical names as their method counterparts.

Access control is one of the features heavily considered since the beginning of Pines' development. As such, the user manager has some unique and powerful systems of access control.

4.1.8.1. Access Control Systems

This section discusses the different types of access control that the user manager service provides. A component is not required to utilize these systems, but they will certainly make designing a safe, functional, multi-user component much easier. When developing Pines, SciActive always considers good access control very important. If you plan on contributing code to SciActive maintained components or Pines Core, please take the time to learn and understand these access control systems very well.

4.1.8.1.1. Abilities

Abilities define what a user is allowed to do. Abilities can be given to both users and groups. When a user is in a group or groups that have abilities and the user is set to inherit abilities, the user manager will act as though the user has the sum of his own abilities and the abilities of all his groups.

The user manager has to be told what abilities a component checks for. This is done in the component's info.php file. The info array has an entry called "abilities", which is an array of arrays. Each array in the abilities array contains three values, the name of the ability, the title or common name, and the description of the ability.

Example Abilities Entry From an Info File
'abilities' => array(
    array('view', 'View Log', 'Let the user view the Pines log.'),
    array('clear', 'Clear Log', 'Let the user clear (delete) the pines log.')
),

Once a user has been granted abilities, the gatekeeper method can be used to check them. To check an ability, append a slash and the name of the ability to the name of the component, and call gatekeeper. If no user is given as the second argument, the currently logged in user will be used. There is one special ability called "system/all" which has the effect of granting the user all abilities.

Checking Users' Abilities and Using punt_user
$user = user::factory('hunter');
// Check that the user has the ability.
if (!$pines->user_manager->gatekeeper('com_logger/view', $user)) {
    pines_notice('Hunter can\'t view the Pines log.');
    return;
}

// Use shortcuts and check the current user. Punt them if they don't have the ability.
if (!gatekeeper('com_logger/view'))
    punt_user(null, pines_url('com_logger', 'view'));

// Checking two abilities. Custom punt user message.
if (!gatekeeper('com_example/viewwidgets') && !gatekeeper('com_example/viewownwidgets'))
    punt_user('You need the ability to see widgets to access this page.');

// Use depend to check multiple abilities. Same as above.
if (!pines_depend('ability', 'com_example/viewwidgets|com_example/viewownwidgets'))
    punt_user('You need the ability to see widgets to access this page.');
4.1.8.1.2. Entity Access Controls

Entity access controls determine what a user can do, if anything, with an entity. When a component retrieves entities from the entity manager, the user manager filters out any entities the user does not have read access to before the entities are returned from the method. Likewise, when a component tries to save or delete an entity, the user manager will only allow it if the user has appropriate permission.

Every entity which is created while a user is logged in will be owned by that user, and belong to that user's primary group. If an entity should belong to another user, it must be saved, assigned to the other user, then resaved. An entity belongs to a user when the user is placed in the entity's user variable. Likewise, an entity belongs to a group when the group is placed in the entity's group variable.

Access controls are stored in a variable called ac on every entity. This variable is an object with the following three variables.

Table 4.4. Entity Access Control Variables

ValueDefault ValueDescription
user3Controls the entity's owner's access.
group3Controls the access of all users in both the entity's group and all ancestor groups.
other0Controls all other users' access.


Each of these three variables can be one of the following numeric values.

Table 4.5. Entity Access Control Values

ValueAccess Level
0No access to the entity is granted.
1Access is granted to read the entity.
2Access is granted to read and edit the entity.
3Access is granted to read, edit, and delete the entity.


[Caution]Caution

If your component allows the user to edit an entity's user, group, or ac variable, the access control mechanism could probably be easily circumvented.

The user manager's check_permissions method can be used to determine whether the current user has certain access to an entity. It takes two arguments, the entity and the type of access to check. The type of access is given as an integer, and returns true if the user has at least that level of access. Different situations use different checks to determine the user's access.

Table 4.6. Entity Access Checking Situations

SituationCheck Performed/Value Returned
No user is logged in.Always true, should be managed with abilities.
The entity has no user and no group.Always true.
The user has the "system/all" ability.Always true.
The entity is the user.Always true.
The entity is the user's primary group.Always true.
The entity is a user or group.Always true.
Its user variable is the user. (It is owned by the user.)Check user AC.
Its group variable is the user's primary group.Check group AC.
Its group variable is one of the user's secondary groups.Check group AC.
Its group variable is a descendant of one of the user's groups.Check group AC.
None of the above.Check other AC.


A component can change the access control variable to allow or prevent access.

Changing the Access Controls of an Entity
// Create a new baz entity.
$entity = com_foobar_baz::factory();
$entity->foo = 'bar';

// Allow all users to see it, but only the current user to edit or delete it.
$entity->ac = (object) array(
    'user' => 3,
    'group' => 1,
    'other' => 1
);

$entity->save();

It is the responsibility of the user manager to prevent your component from accessing an entity in a way the user is not permitted. Therefore, you don't need to add anything to your code. Entities that the current user does not have proper access to won't be returned when you query the entity manager. When you try to access a referenced entity, and the user manager blocks it due to access controls, it will appear as if the variable is null. If you wish to bypass the entity access control feature, you can use the "skip_ac" option in your entity queries.

Using "skip_ac" to Access Entities
// Get all baz entities, regardless of whether the logged in user has access to them.
$entities = $pines->entity_manager->get_entities(
        array('class' => com_foobar_baz, 'skip_ac' => true),
        array('&',
            'tag' => array('com_foobar', 'baz')
        )
    );
[Note]Note

When you request entities using the "skip_ac" option, any referenced entities will also be retrieved using the "skip_ac" option.

4.1.8.2. User

The user class is very similar to the entity class. Their interfaces both extend data_object_interface, so they share many of the same methods. Here is a list of the methods they share.

  • array_search

  • delete

  • equals

  • in_array

  • is

  • refresh

  • save

Although the user's information doesn't have to be stored in an entity, users must act like entities in the following ways.

  • Users must have unique GUIDs, just like entities.

  • The user class must provide a factory method.

  • When requesting entities, users must be returned if they match as well.

  • Users must be able to store data, including entity references, just like entities.

  • Users must work with the "ref" selector in the entity manager.

What this means is that even though users are not necessarily entities, they can be treated as if they were, except for tags. It is up to the user manager implementation whether to use entities to store users. The SciActive maintained user manager uses entities to handle users and groups.

Users have several useful methods:

  • add_group

    Add the user to a (secondary) group. Takes one argument, the group. Returns true if the user is already in the group, the resulting array of groups if the user was not.

  • check_password

    Check the given password against the user's. Takes one argument, the password in question. Returns true if the passwords match, otherwise false.

  • del_group

    Remove the user from a (secondary) group. Takes one argument, the group. Returns true if the user wasn't in the group, the resulting array of groups if the user was.

  • disable

    Disable the user.

  • enable

    Enable the user.

  • get_timezone

    Returns the user's timezone. Takes a boolean argument (defaults to false), whether to return an object of the DateTimeZone class, instead of an identifier string.

  • in_group

    Check whether the user is in a (primary or secondary) group. Takes one argument, the group or the group's GUID.

  • is_descendant

    Check whether the user is in a (primary or secondary) group that is a descendant of the provided group. Takes one argument, the group or the group's GUID.

  • password

    Change the user's password. Takes one argument, the new password.

  • print_form

    Print a form to edit the user. Returns the form's module.

4.1.8.3. Group

The group class is also very similar to the entity class. Its interface also extends data_object_interface. It shares the same methods with entities that the user class shares. It also must act like entities in the same way users do. Again, this does not include support of tags.

Groups are hierarchical, and all children of a group have the group in their parent variable. This makes traversal up the hierarchy very easy. However, traversal down the hierarchy is not as easy. It requires the use of the get_children or get_descendants methods.

Groups can also have a logo. This allows an easy visual representation of a user's position in the group hierarchy. This feature is especially useful for systems which use sports teams, subsidiary companies, store locations, countries, guilds, etc. If the current group doesn't have an associated logo, the user manager should traverse up the hierarchy until it finds a group with a logo, or return the default logo if it can't find one.

Groups have several useful methods:

  • disable

    Disable the group.

  • enable

    Enable the group.

  • is_descendant

    Check whether the group is a descendant of the provided group. Takes one argument, the group or the group's GUID.

  • get_children

    Gets an array of the group's child groups.

  • get_descendants

    Gets an array of the group's descendant groups. Takes a boolean argument (defaults to false), whether to include itself in the returned array.

  • get_level

    Get the number of parents the group has. If the group is a top level group, this will return 0. If it is a child of a top level group, this will return 1. If it is a grandchild of a top level group, this will return 2, and so on.

  • get_logo

    Find the location of the group's current logo image. Takes a boolean argument (defaults to false), whether to return a full URL, instead of a relative one. Returns the URL of the logo image.

  • get_users

    Gets an array of users in the group. Some user managers may return only enabled users, so don't rely on this returning all users. Takes a boolean argument (defaults to false), whether to include users in all descendant groups too.

  • print_form

    Print a form to edit the group. Returns the form's module.