With the release of Joomla! 1.7.0 in July 2011, the Joomla platform officially became its own project, separate from the Joomla content management system (CMS). This allows the platform to be developed independently of the Joomla CMS and makes it easier for the platform to be deployed on its own to develop and power a wide variety of web applications.
The Joomla platform is a large topic and deserves its own book. This chapter is intended to give an overview of the platform and some simple examples of its use.
Joomla version 1.5 was designed so that classes that performed fundamental functions were placed in a folder called libraries/joomla
. Although used in various places in the CMS, these classes perform functions that are common to most web applications and not specific to a CMS application. Examples include authenticating users, translating text into other languages, filtering input and output, and working with the server’s file system.
This initial separation of these programs went a long way toward allowing the Joomla platform to be used separately from the CMS. However, there were still a number of places where programs in the platform folder depended on programs in other places in the CMS to function correctly.
With the release of the platform as a separate project, these dependencies have been eliminated and now it is easy to install and use the platform without installing the Joomla CMS. The platform has its own code repository at the website https://github.com/joomla/joomla-platform. It can be downloaded and used on its own.
There are two overriding reasons for officially separating out the platform project from the CMS project.
The first reason is to make the project more attractive to developers. The Joomla CMS is a mature product with millions of users. Although there is exciting work yet to do to improve the CMS, it has been around since 2005 and it was created as a fork of the Mambo project, which was started in 2000. So the Joomla CMS is at a stage where most changes are incremental. Moreover, the entire CMS industry is at a relatively mature stage at this time, at least compared to many other types of web applications.
By contrast, the Joomla platform presents a unique opportunity for developers. It is a new project where developers can participate in working on exciting new functionality without the constraints of existing code. There are many opportunities to create new code and functionality from scratch.
On the other hand, because the platform is already used in millions of Joomla websites, it is well tested and has a lot of credibility. New functionality in the platform will, in many cases, be made available to the entire Joomla CMS user base over time, as the CMS takes advantage of new platform features. This means that developers who add important features to the platform will potentially see those features get used by millions of people.
The second important reason for creating the platform project is to make it very easy for developers to use the platform for non-CMS applications. Content management is an important type of web application, but it is only one of many possible application types. Today, the web is used for many things beyond creating websites for sharing information, and these include many of the most innovative web applications.
Most web applications need basic functionality, such as user authentication, file system interaction, filtering, and others. When a team is developing a new web application, instead of creating this functionality for themselves, they can use the Joomla platform to get a head start on the project. By deploying a set of programs that already includes methods for many common requirements, they can focus on the parts of the project that are unique and not spend time “reinventing the wheel.”
Today, many types of applications are being performed on the web. Just some of the examples are as follows:
• E-commerce programs designed to act as the “back-room” processing for any website that includes a shopping feature
• Web-based enterprise resource planning (ERP) systems, designed to provide financial accounting and related functions to various types of businesses and organizations
• Applications that allow websites to be modified by running a program at a server’s command-line console.
• User forums and bulletin-board systems.
The Joomla platform can be used as the foundation for developing almost any type of web application. One way to think about it is this: if your application will need to do any of the functionality built into the platform, then the platform can be a great starting point for your application.
The best way to understand the platform is to see some examples. Note that there are a number of example applications available from the Joomla platform code repository here: https://github.com/joomla/joomla-platform-examples. We will start with two very simple examples, one that uses the browser and one that uses the system command-line interface (CLI). CLI programs are run from the command console, not from the browser.
The first thing we need to do is download the platform files and set up our folder structure. One simple way to do this is as follows:
1. Create a folder to hold the project files. We’ll call our folder platform-test
. If you want to test the platform with browser applications as well as CLI applications, put this folder under your web server’s DocumentRoot folder (for example, under the htdocs
folder).
2. Download the platform. One easy way to do this is to navigate to https://github.com/joomla/joomla-platform and press the Tags button. This will give you a list of the official release versions (for example, 11.4.zip). Alternatively, you can download the latest repository version by pressing Downloads → Download as zip.
3. Unpack the archive in the platform-test
folder. The top-level folder in the archive is the same as the archive name, in the format joomla-joomla-platform-<version>-<changeset number> (for example, joomla-joomla-platform-11.4-0-g4329ba0.zip
). After unpacking, rename this folder to joomla-platform
. At this point, platform-test
should have a subfolder called joomla-platform
, which should have subfolders called build
, docs
, libraries
, media
, and tests
. Note that you only need the libraries
and media
folders to run the platform. The other folders are for development.
4. Download the examples. To do this, navigate to https://github.com/joomla/joomla-platform-examples, press Download → Download as zip. This will download a file called something like joomla-joomla-platform-examples-544306f.zip
.
5. Unpack this archive in a temporary folder and copy the cli
and web
folders to the platform-test
folder. Also copy the bootstrap.dist.php
file to the platform-test
folder.
6. At this point, platform-test
should have three subfolders: cli
, joomla-platform
, and web
. You should also have a file called bootstrap.dist.php
.
7. Copy the bootstrap.dist.php
file to bootstrap.php
. Edit this file to contain the following line of code:
require dirname(__FILE__).'/joomla-platform/libraries/import.php';
This code tells the platform where to find the import script. It uses the PHP dirname()
command to get the directory name of the current file (bootstrap.php
) and then uses that to create the full path name of the import script. In our setup, the bootstrap.php
file is in the same folder as the joomla-platform
folder.
At this point, you should be able to run all the example programs. To test this, try out the Hello World example, as follows:
• Open a command-line session in your computer and change to the directory platform-test/cli/101-hello-world
.
• Make sure your PHP program is executable from the command line. If needed, add it to the execution path.
• At the command prompt enter the command php run.php
• You should see “Hello World!” output to the console.
If this doesn’t work, check the bootstrap.php
file and the require statement that loads the bootstrap file.
If the platform-test
folder is under your web server’s DocumentRoot folder, you should also be able to run the web applications. For example, to run the detect-client example web application, enter the following URL in your browser:
<path to the platform-test folder>/web/detect-client/index.php
For example, if your platform-test
is a subfolder of htdocs
on your local machine, the URL would be
http://localhost/platform-test/web/detect-client/index.php
You should see something like the following output in your browser:
Welcome to the Joomla! Platform's JWeb class.
* User-agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23 ( .NET CLR 3.5.30729; .NET4.0C)
* Is a mobile device? No
* Platform: 1
* Engine: 13
* Browser: 18 (3.6.23)
Again, if you get an error, check that the bootstrap.php
file is correct.
Next, let’s look at the code for the hello world application (in the file platform-test/joomla-platform/cli/101-hello-world/run.php
). The first part of the code is as follows:
// We are a valid Joomla! entry point.
// This is required to load the Joomla! Platform import.php file.
define('_JEXEC', 1);
// Setup the base path related constant.
// This is one of the few, mandatory constants needed for the Joomla! Platform.
define('JPATH_BASE', dirname(__FILE__));
// Bootstrap the application.
require dirname(dirname(dirname(__FILE__))).'/bootstrap.php';
// Import the JCli class from the platform.
jimport('joomla.application.cli'),
Here we define the _JEXEC
constant. This tells subsequent program files that we are inside a Joomla application. This constant is required for the platform files to execute.
The next thing we do is define the JPATH_BASE
constant. This constant needs to be defined, although its use is up to the application. It will normally point to the root folder of the application. In this case, we aren’t using it, so we define it to just point to the current folder.
The next command includes the bootstrap.php
file we edited earlier. In this case, we have that file in the root folder of our structure (platform_test
), which is three folders up from the current path (platform_test/cli/101-hello-world
). So we use three dirname()
commands to get to this folder. Note that we can put the bootstrap.php
file anywhere we like. This is just the structure that the example programs use.
The bootstrap.php
file loads the libraries/import.php
file, which in turn creates a number of platform constants and loads some basic platform classes.
The last command imports the cli.php
file from the libraries/joomla/application folder
. This is the class that all platform CLI applications will normally extend.
The rest of the code for our Hello World example is as follows:
class HelloWorld extends JCli
{
/**
* Execute the application.
*
* The 'execute' method is the entry point for a command line application.
*
* @return void
*
* @since 11.3
*/
public function execute()
{
// Send a string to standard output.
$this->out('Hello world!'),
}
}
// Instantiate the application object, passing the class name to JCli::getInstance
// and use chaining to execute the application.
JCli::getInstance('HelloWorld')->execute();
Here we create our class, which extends the JCli
class. We have one method called execute()
. In this case, it uses the out()
method of the JCli
class to output the message to the console. Then we close the method declaration and the class declaration.
The last line of the file uses method chaining to do two things. First, it gets a new JCli
object using the getInstance()
method. Then it calls the execute()
method of that object. The end result is that “Hello world!” is shown on the console. Note that this last line is outside the class declaration and is executed immediately.
This application, web/101-hello-www/index.php
, outputs a simple text message in the browser. The first part of the code is as follows:
// We are a valid Joomla! entry point.
define('_JEXEC', 1);
// Setup the base path related constant.
define('JPATH_BASE', dirname(__FILE__));
// Increase error reporting to that any errors are displayed.
// Note, you would not use these settings in production.
error_reporting(E_ALL);
ini_set('display_errors', true);
// Bootstrap the application.
require dirname(dirname(dirname(__FILE__))).'/bootstrap.php';
// Import the JWeb class from the platform.
jimport('joomla.application.web'),
As before, we define our constants. The next two lines set up maximum error reporting. This is helpful during development to allow you to see any code that violates PHP strict standards. Also, note that the platform is designed to run for PHP versions 5.3 and higher.
The next two lines require the bootstrap.php
file from our project root folder and then import the file libraries/application/web.php
file. This defines the JWeb
class.
The rest of the index.php
file is as follows:
class HelloWww extends JWeb
{
/**
* Overrides the parent doExecute method to run the web application.
*
* This method should include your custom code that runs the application.
*
* @return void
*
* @since 11.3
*/
protected function doExecute()
{
// This application will just output a simple HTML document.
// Use the setBody method to set the output.
// JWeb will take care of all the headers and such for you.
$this->setBody('<html>
<head>
<title>Hello WWW</title>
</head>
<body style="font-family:verdana;">
<p>Hello WWW!</p>
</body>
</html>'
);
}
}
// Instantiate the application object, passing the class name to JWeb::getInstance
// and use chaining to execute the application.
JWeb::getInstance('Hellowww')->execute();
This creates the HelloWww
class, extending the JWeb
class. Most web applications using the platform will extend the JWeb
class.
Then we create a protected method called doExecute()
. This overrides the doExecute()
method of the JWeb
class. Recall in the CLI Hello World example we created an execute()
method. We can use either method to execute code inside the class. However, there is an important difference. If you look at the code for the execute()
method of the JWeb
class, you will see that it triggers events before and after the doExecute()
method is called. Then it renders the document and triggers some more events. Finally, it sends the response to the client, again with before and after events triggered. Because these are steps we normally want done when we are working with HTML documents, the preferred approach is to override the doExecute()
method and let the JWeb execute()
method do the rest of this work.
The code in the doExecute()
method uses the setBody()
method of JWeb
to add our HTML to show the hello message. JWeb
includes various methods for working with the document header and body.
The last line of our file is similar to the prior example. We get an instance of the object and then run its execute()
method. The result is that the message displays in an HTML document in our browser.
Now that we understand the basics of using the platform CLI, let’s look at a realistic example using the Joompro Subscriptions component we created earlier in the book. Recall that this component creates subscriptions that expire after a certain number of days. In a real-life situation, we would want to have a way to monitor and manage these subscriptions.
In this example, we will create a CLI application that does two things. First, it checks the database for subscriptions that will expire in the next five days and sends the subscriber a reminder e-mail. Second, if a subscription has expired, it deletes the subscription row from the mapping table and removes the user from the subscription’s group.
We will assume that the application will be run unattended on a schedule (for example, using the Linux cron
command). For this reason, we will record the results of the program in a log file for the system administrator to review (instead of printing information out in the console).
For this example, we will use a somewhat different project structure from those the earlier examples used. We will put our program files in a folder called src
under the joomla-platform
folder. The src folder will hold our program files and log files.
To set this up, create a new folder called platform-test/joomla-platform/src
. Then under the src
folder create a folder called logs
to hold the log files.
In the src
folder, we will have three files and one folder, as follows:
• monitor.php
: The program to run from the command line
• subscriptionmonitor.php
: The program that does the subscription checking
• configuration.php
: The file that holds the site-specific configuration data
• logs
: The folder where our log files will be created
The first file we need to create is our configuration file. This has the exact same structure as the Joomla CMS configuration file (and in fact you can copy the configuration.php file from the root folder of a Joomla installation). In this example, we only require the fields needed to connect to the CMS database and send an e-mail, but we have included the other fields as well.
The code for configuration.php
is as follows:
<?php
// Prevent direct access to this file outside of a calling application.
defined('_JEXEC') or die;
class JConfig
{
public $dbtype = 'mysqli';
public $host = '127.0.0.1';
public $user = 'test';
public $password = 'password';
public $db = 'book_170';
public $dbprefix = 'jos_';
public $ftp_host = '127.0.0.1';
public $ftp_port = '21';
public $ftp_user = '';
public $ftp_pass = '';
public $ftp_root = '';
public $ftp_enable = 0;
public $tmp_path = '/tmp';
public $log_path = '/var/logs';
public $mailer = 'smtp';
public $mailfrom = '[email protected]';
public $fromname = 'Joomla! Programming';
public $sendmail = '/usr/sbin/sendmail';
public $smtpauth = '1';
public $smtpuser = '<gmail user name>';
public $smtppass = '<gmail password>';
public $smtphost = 'smtp.gmail.com';
public $smtpsecure = 'ssl';
public $smtpport = '465';
public $debug = 0;
public $caching = '0';
public $cachetime = '900';
public $language = 'en-GB';
public $secret = null;
public $editor = 'none';
public $offset = 0;
public $lifetime = 15;
}
You will need to change the settings to match your environment. These will include $user
(database user name), $password
(database password), $db
(database name), and $dbprefix
(database table prefix) and possibly the e-mail settings (depending on your system). You can run the example code here without e-mail if needed. The e-mail settings shown here are for a Google Gmail account.
This file is the entry point for our application. It is the command that will be run from the command line. The first part of the code is as follows:
<?php
// Declare that we are a valid Joomla! entry point if not declared.
if (!defined('_JEXEC'))
{
define('_JEXEC', 1);
}
// Setup the application base path constant if not already set.
if (!defined('JPATH_BASE'))
{
define('JPATH_BASE', dirname(__FILE__));
}
// Import the Joomla! Platform.
require dirname(dirname(__FILE__)) . '/libraries/import.php';
// Import library dependencies.
jimport('joomla.log.log'),
require JPATH_BASE . '/subscriptionmonitor.php';
Here we make sure that the _JEXEC
and JPATH_BASE
constants are defined. Then we require the libraries/import.php
file. Note that we don’t use a bootstrap.php
file in this example. Instead, we just require the import file. Note also that we adjust the command to find the import.php
file relative to the location of the current file (monitor.php
).
Once we have the import.php
file loaded, we can import the library classes we need for this program. In this case, it’s just the JLog
class. Then we require the subscriptionmonitor.php
program.
The next part of the file is as follows:
// Load the configuration.php file
$config = JFactory::getConfig(JPATH_BASE.'/configuration.php'),
// Create the log file
// Get the date so that we can roll the logs over a time interval.
$date = JFactory::getDate()->format('Y-m-d'),
// Add the logger.
JLog::addLogger(
// Pass an array of configuration options.
// Note that the default logger is 'formatted_text' - logging to a file.
array(
// Set the name of the log file.
'text_file' => 'monitor-.'.$date.'.php',
// Set the path for log files.
'text_file_path' => __DIR__.'/logs/'
)
);
Here we set the configuration using the configuration.php
file we created. Note that we don’t use the $config
object. However, executing this method creates the JConfig
object (JFactory::$config
), which is used in the JFactory::getDate()
and JFactory::getMailer()
methods we use later on. By setting this at the start of our program, we know it will be set with the desired configuration.php
file. If, for example, we call JFactory::getDate()
before creating JConfig
, it will load configuration.php
from the default location (JPATH_PLATFORM
), which is not what we want.
Next we create the log file using the JLog
class. We make the current date part of the file name. That way, we get a new file every day. We place this file in the logs folder we created earlier.
The next part of the file is as follows:
// Wrap the execution in a try statement to catch any exceptions thrown anywhere in the script.
try
{
// Instantiate and execute the application.
JCli::getInstance('SubscriptionMonitor')->execute();
}
catch (Exception $e)
{
// An exception has been caught, add the message to the log.
JLog::add($e->getMessage(), JLog::ERROR);
exit($e->getCode());
}
Here we create a try/catch block and call the execute()
method of the SubscriptionMonitor
class inside the try block. As we will see, several methods in the SubscriptionMonitor
class throw exceptions. By calling the execute()
method inside the try block, we ensure that any exceptions thrown anywhere in the method are caught and handled.
In the catch block, we add the exception message to the log. Keep in mind that this program will normally be run automatically by the server, so logging errors in a log file is preferable to showing them in the console.
We could have put all the code for checking the subscriptions in this one file instead of creating a second file. The approach used here has an important advantage: It makes it very easy to add more monitoring functions. Say, for example, we also wanted to monitor the stop publishing date for articles. All we would do is create the new article monitoring class and add the line to execute it in the try block of monitor.php
. This one monitor.php
file could run all our monitoring functions for the entire site and could be run by the server as one simple cron job.
This file, subscriptionmonitor.php
, actually does the work of checking and updating the database and sending the e-mails. The first part of this file is as follows:
jimport('joomla.application.cli'),
jimport('joomla.database.database'),
/**
* A Joomla! Platform application to monitor subscription status and take action when necessary.
*
* @package Subscriptions
* @since 1.0
*/
class SubscriptionMonitor extends JCli
{
/**
* @var JDatabase The application database connection object.
* @since 1.0
*/
protected $db;
This imports the JCli
and JDatabase
classes and then declares the SubscriptionMonitor
class as a subclass of JCli
. Finally, we add a protected member to hold the JDatabase
object for the class.
The next part of the code is the class constructor, as follows:
public function __construct(JInputCli $input = null, JRegistry $config = null, JDispatcher $dispatcher = null, JDatabase $db = null)
{
// Call the parent constructor for basic setup of the object.
parent::__construct($input, $config, $dispatcher);
// If a database connection object is given use it.
if ($db instanceof JDatabase)
{
$this->db = $db;
}
// Create the database connection based on the application logic.
else
{
$this->loadDatabase();
}
}
The constructor has four arguments in its method signature: $input
, $config
, $dispatcher
, and $db
. We don’t use these arguments in this example, but they are there in case we later want to reuse this class in a different context. For example, the first argument allows you to pass a JInputCli
object to the constructor. This object simulates command-line arguments. The other arguments allow you to call the constructor with your own configuration file, dispatcher class, and database class.
Note that there is something different about the method signature from what we have seen before. Here we indicate the class name before each variable (for example, JInputCli $input
). This is known in PHP as type hinting. It tells the PHP interpreter (and someone reading the code) that this argument must be an object of that type. Otherwise, you will get a fatal error when executing the method. Note that type hinting is only available in PHP 5.3 and later.
The constructor code calls the parent constructor and then checks whether the $db
field has been set. If not, it calls the loadDatabase()
method to set this field.
The next method in this class is doExecute()
. Recall that this method is run by the execute()
method of the JCli
class. The code for this is as follows:
protected function doExecute()
{
// Log start of program
JLog::add('Subscription monitoring started'),
// Get a list of the ended subscriptions and remove them.
$ended = $this->getEndedSubscriptions();
foreach ($ended as $sub)
{
$this->doEndSubscription($sub);
}
// Get a list of the ending subscriptions and send a notification.
$ending = $this->getEndingSubscriptions();
foreach ($ending as $sub)
{
$this->doSendSubscriptionEndingNotification($sub);
}
// Log end of program
JLog::add('Subscription monitoring ended'),
}
This is the method that actually controls the program flow and does the work. We start by adding a log entry indicating the program has started. Then we call the getEndedSubscriptions()
method to get a list of subscriptions that have expired. Then we loop through each of these and call the doEndSubscription()
to actually process each expired subscription.
There is an interesting point about using foreach
loops in this context. It is quite possible in normal use that there could be no expiring subscriptions. In this case, $ended
will be an empty array. In that case, the foreach
loop is simply skipped, and there are no errors or notices.
After we process the expired subscriptions, we call the getEndingSubscriptions()
to get a list of subscriptions that are close to expiration. Again, we process the list in a foreach
loop, calling the doSendSubscriptionEndingNotification()
method for each one in the list. The last thing we do is add a log entry indicating that we have finished.
The next method in the class is getEndedSubscriptions()
. This method queries the database to get a list of expired subscriptions. The code is as follows:
protected function getEndedSubscriptions()
{
// Get a date object for now.
$now = JFactory::getDate($this->get('execution.datetime'));
// Get the query builder class from the database.
$query = $this->db->getQuery(true);
// Set up a query to select subscriptions that have end dates before now.
$query->select('*')
->from($this->db->qn('#__joompro_sub_mapping'))
->where($this->db->qn('end_date') . ' < ' . $this->db->q($now->toSQL()))
->where($this->db->qn('end_date') . ' > ' . $this->db->q('0000-00-00 00:00:00'));
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Get all the returned rows from the query as an array of objects.
$rows = $this->db->loadObjectList();
return $rows;
}
Here we get the current date and time and then build a query that selects all rows in the mapping table that have an end_date
column that is less than the current date and time. Notice that we use the code
($this->get('execution.datetime')
to get the current time. This calls the get()
method of the JCli
class. That method has the following code:
public function get($key, $default = null)
{
return $this->config->get($key, $default);
}
Here we are calling the get()
method of the config field of the class. Where is the execution.datetime being set? The answer is in the contructor of the JCli
class. There we see the following line of code:
$this->set('execution.datetime', gmdate('Y-m-d H:i:s'));
The set()
method adds the results of the gmdate()
method to the config field of the class. The gmdate()
method returns the current time for the Greenwich Mean Time (GMT) time zone. Why do we save this value in the config field instead of just calling gmdate()
each time we need the current time? The answer is that the gmdate()
method will return a different value each time it is called. By calling it once in the constructor, we have one time value to use in both of our queries, so they will always give a consistent result.
Note that we also check that the end date is greater than a zero date. Recall that in the Joomla CMS we use the convention of a zero date indicating an indefinite expiration date. Adding this would allow us to use the same convention for subscriptions.
Here we use method chaining to build the query. The entire query is built with one command, using the results of one query method as the object to call the next method. Alternatively, we could have used separate lines of code to call the select()
, from()
, and the two where()
methods.
After we build the query, we pass it to the database object and return the results using loadObjectList()
. Recall that this method returns an array of objects.
There is one last interesting thing to notice about this code. In several places, we use the JDatabase qn()
method. However, if we look at the JDatabase
class (libraries/joomla/database/database.php
), we don’t see any method called qn()
. The JDatabase
class doesn’t extend another class, so it isn’t coming from a parent class. Where is qn()
defined?
The answer is in the __call()
method of JDatabase
. This method is one of several “magic” methods in PHP. It is called any time we call a method that doesn’t exist in the current class. One common use for the __call()
method is to create aliases for commonly used methods. If we look at this method, we see the following code:
switch ($method)
{
case 'q':
return $this->quote ($args[0], isset($args[1]) ? $args[1] : true);
break;
case 'nq':
case 'qn':
return $this->quoteName ($args[0]);
break;
}
Here we test for methods called q
, nq
, and qn
and in turn calling either the quote()
method or the quoteName()
method. So the qn()
method is just an alias for the quoteName()
method. Note that we have to be careful to pass the arguments (an array called $args
) through to the quote()
and quoteName()
methods.
The next method in the SubscriptionMonitor
class is getEndingSubscriptions()
. This is similar to the previous method, except that it selects subscriptions that will expire within the next five days. Here is the code:
protected function getEndingSubscriptions()
{
// Get a date object for now and five days into the future.
$now = JFactory::getDate($this->get('execution.datetime'));
$future = JFactory::getDate($this->get('execution.datetime'));
$future->add(new DateInterval('P5D'));
// Get the query builder class from the database.
$query = $this->db->getQuery(true);
// Set up a query to select subscriptions that have end dates between now and 5 days from now.
$query->select('*')
->from($this->db->qn('#__joompro_sub_mapping'))
->where($this->db->qn('end_date') . ' < ' . $this->db->q($future->toSQL()))
->where($this->db->qn('end_date') . ' > ' . $this->db->q($now->toMySQL()));
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Get all the returned rows from the query as an array of objects.
$rows = $this->db->loadObjectList();
return $rows;
}
The first thing we do is calculate a date and time that is five days in the future. We create a date called $future
equal to the current date and then we add five days to it using its add()
method. To get the five-day interval, we use the PHP DateInterval
class. This takes an argument in the form
P + <number> + <time interval>
In our example, we use P5D
to indicate five days. The result is that $future
now holds a date-time object five days later than $now
.
Now we use these date-time values to build a query that selects any subscriptions with ending dates later than now but before five days from now. As before, we return these as an array of objects.
Notice that there is a potential design problem with this query. If we run our monitor program on a daily basis, a subscriber would get an e-mail every day for up to five days, which could be annoying. If we didn’t want that result, we could run the monitor program less often (say every five days) or we could change the query to select subscriptions in a narrower time window (for example, subscriptions that will expire in between four and five days). Alternatively, we could record in the database that a reminder e-mail has been sent and use that to filter the query results.
The next method is doEndSubscriptions()
. This method does several things:
• Gets the subscription title and user name for the e-mail
• Deletes the row for the user-to-usergroup mapping table (thereby removing the user from the group)
• Deletes the row from the mapping table (thereby removing the subscription)
• Sends an e-mail to the user to notify them the subscription has been removed
The first part of the method is as follows:
protected function doEndSubscription($subscriptionMapping)
{
$subscription = $this->getSubscription( $subscriptionMapping->subscription_id);
$user = $this->getUser($subscriptionMapping->user_id);
Here we call the getSubscription()
and getUser()
methods. These check that the subscription and user ids are valid and also give us the subscription title and user name for the e-mail.
The next section of code is as follows:
// Set up a query to remove the user group mapping.
$query = $this->db->getQuery(true);
$query->delete()
->from($this->db->qn('#__user_usergroup_map'))
->where($this->db->qn('group_id') . ' = ' . (int) $subscription->group_id)
->where($this->db->qn('user_id') . ' = ' . (int) $subscriptionMapping->user_id);
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Execute the query.
$this->db->query();
At this point, we know that the subscription and user from the mapping table are valid. Now we begin updating the database. We build a new delete query to delete the row for the user in the user-to-user group mapping table. Then we execute the query. By deleting this row, we remove the user from the group. Recall that the user was added to the group when the user subscribed to the subscription.
The next code block is as follows:
// Get the query builder class from the database.
$query = $this->db->getQuery(true);
// Set up a query to remove the subscription mapping.
$query->delete()
->from($this->db->qn('#__joompro_sub_mapping'))
->where($this->db->qn('subscription_id') . ' = ' . (int) $subscriptionMapping->subscription_id)
->where($this->db->qn('user_id') . ' = ' . (int) $subscriptionMapping->user_id);
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Execute the query.
$this->db->query();
Here we create a new delete query—this time, to remove the row from the subscription mapping table. This unsubscribes the user from this subscription.
The last portion of the method is as follows:
// Build expiration email.
$subject = 'Your subscription has expired.';
$body[] = 'Hi, '.$user->name.',';
$body[] = '';
$body[] = 'Your subscription to '.$subscription->title.' has expired.';
$body[] = '';
$body[] = 'Regards,';
$body[] = $this->get('fromname'),
// Send the notification email.
$this->sendNotificationEmail($user->email, $subject, implode("
", $body));
JLog::add('Subscription removed for user='.$user->name.', title='.$subscription->title);
}
Here we build the contents of the notification e-mail to the user. Then we call the sendNotificationEmail()
method to send the e-mail. Finally, we add a log entry to indicate that this user’s subscription was removed.
Next comes the doSendSubscriptionEndingNotification()
method. Its code is as follows:
protected function doSendSubscriptionEndingNotification($subscriptionMapping)
{
$subscription = $this->getSubscription( $subscriptionMapping->subscription_id);
$user = $this->getUser($subscriptionMapping->user_id);
$subject = 'Your Subscription is ending soon.';
$body[] = 'Hi, '.$user->name.',';
$body[] = '';
$body[] = 'Your subscription to '.$subscription->title. ' will end on '.$subscriptionMapping->end_date.'.';
$body[] = '';
$body[] = 'Regards,';
$body[] = $this->get('fromname'),
// Send the notification email.
$this->sendNotificationEmail($user->email, $subject, implode("
", $body));
// Log the notification
JLog::add('Subscription ending notification sent for user='. $user->name.', title='.$subscription->title);
}
Again, we start by getting the user and subscription objects. Then we build the e-mail to send and send it using the sendNotificationEmail()
method. Finally, we log that this has been done. Note that in this case we aren’t changing the database. We are just sending the user a reminder.
The next method is getSubscription()
, as follows:
protected function getSubscription($subID)
{
// Set up a query to get the group_id from the subscription record.
$query = $this->db->getQuery(true);
$query->select('*')
->from($this->db->qn('#__joompro_subscriptions'))
->where($this->db->qn('id') . ' = ' . (int) $subID);
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Validate that the subscription exists.
$subscription = $this->db->loadObject();
if (empty($subscription))
{
throw new Exception('Invalid Subscription.'),
}
return $subscription;
}
Here we build a query to read the row from the subscriptions table using the subscription id. Recall that the id must be unique, so this query will return at most one row. If the subscription doesn’t exist for this item, we throw an exception to log this event. At the end, we return the $subscription
object with the data for this row of the database table.
The next method is getUser()
, as follows:
protected function getUser($userID)
{
// Set up a query to get the user object.
$query = $this->db->getQuery(true);
$query->select('*')
->from($this->db->qn('#__users'))
->where($this->db->qn('id') . ' = ' . (int) $userID);
// Set the database query object to the database connection object.
$this->db->setQuery($query);
// Validate that the user exists.
$user = $this->db->loadObject();
if (empty($user))
{
throw new Exception('Invalid User.'),
}
return $user;
}
This method is similar to the previous one. We try to read the row from the users table for this user id. If successful, we return the row as an object. If not, we throw an exception indicating that the user id was not valid.
The next method is sendNotificationEmail()
, as follows:
protected function sendNotificationEmail($email, $subject, $body)
{
JFactory::getMailer()->sendMail(
$this->get('mailfrom'),
$this->get('fromname'),
$email,
$subject,
$body
);
}
This method sends the e-mail using the sendMail()
method of the JMail class (libraries/joomla/mail/mail.php
). Note that this requires that your website is configured to send e-mail. If you are working on a local machine that isn’t set up to send e-mails, you can disable this method simply by commenting out all the lines, leaving an empty method. Make sure that the method is still defined. That way, you can test and run the rest of the program even if your e-mail is not set up.
The last method is loadDatabase()
. Recall that this is called in the constructor to create a database object with the information from our configuration file. The code is as follows:
protected function loadDatabase()
{
// Note, this will throw an exception if there is an error creating the database connection.
$this->db = JDatabase::getInstance(
array(
'driver' => $this->get('dbtype'),
'host' => $this->get('host'),
'user' => $this->get('user'),
'password' => $this->get('password'),
'database' => $this->get('db'),
'prefix' => $this->get('dbprefix'),
)
);
}
} // end of class
Here we simply create a new JDatabase
object using the values from our configuration file. Recall that the get()
method of JCli
reads values from the configuration object.
To test the program, follow these steps:
1. Enter one or more subscriptions into the subscriptions table.
2. Create a menu item to show the subscriptions.
3. Log in to the front end of the site and subscribe to some subscriptions.
4. Using phpMyAdmin (or some other database tool), change the end date on one subscription in the jos_joompro_sub_mapping
table to be in the past and change another to be less than five days in the future.
5. Make sure the configuration.php
file has the correct information to connect to the site’s database.
6. If needed, comment out the code in the sendNotificationEmail()
method (if you don’t have your test machine set up to send e-mails). Be sure to keep the empty method.
7. Open a console session and navigate to the platform-test/src
folder. Enter the command: php monitor.php
The program should run and return to the system prompt. At this point, a log file should be created in your logs folder and should have entries similar to that shown here:
#<?php die('Forbidden.'), ?>
#Date: 2012-10-15 01:07:35 UTC
#Software: Joomla! Platform 11.2.0 Stable [ Omar ] 27-Jul-2011 00:00 GMT
#Fields: datetime priority category message
2012-10-15T01:07:35+00:00 WARNING deprecated JDatabaseMySQLi::hasUTF() is deprecated.
2012-10-15T01:07:35+00:00 INFO - Subscription monitoring started
2012-10-15T01:07:35+00:00 INFO - Subscription removed for user=George Washington, title=Pontiac GTO
2012-10-15T01:07:35+00:00 INFO - Subscription ending notification sent for user=George Washington, title=Ford Mustang
2012-10-15T01:07:35+00:00 INFO - Subscription monitoring ended
Note that the first log entry is warning us that we are using a deprecated method, JDatabaseMySQLi::hasUTF()
. We can ignore this message. The next line shows that we started the monitoring program. Then we removed one subscription and sent one notification. Then the program finished.
Test the different error conditions by entering invalid data in the tables or by making the queries fail. For example, change a user or subscription id in the subscription mapping to a number that doesn’t match an existing user or subscription. Or change the table name to be an invalid table. In these cases, you should see the error messages reflected in the log.
If we want to write command-line programs that interact with the CMS, we have two options for where to locate those programs. In this example, we created an entirely separate application that uses its own version of the platform and is completely independent of the CMS programs. All we need to know about the CMS is how to connect to its database and e-mail program. The monitor application could easily be on a separate server and could be used, for example, to monitor any number of different Joomla CMS sites (just by using different configuration files).
Alternatively, we can create command-line applications inside the Joomla CMS folder structure. In version 1.7, a folder called cli
was added to the CMS folder structure for this purpose.
We can run this example program inside an instance of the CMS simply by copying the logs folder and the three program files (configuration.php
, monitor.php
, and subscriptionmonitor.php
) from the src
folder in the platform-test project to the cli
folder of a Joomla CMS installation. Make sure the configuration file has the desired values. Then navigate to the CMS cli
folder and run the monitor.php
program from the command line. It should work exactly as before.
Also, note how similar the programming techniques used in this example are to the techniques used in the previous examples in this book. Programming in the platform is very similar to programming for the CMS. Once we have the platform loaded, the classes used, database access, and other techniques are very similar to the techniques used for programming in the CMS.
In this chapter, we introduced the Joomla platform. We covered how to install the platform and how to run the example programs both for the command line and the browser. Then we examined how the programs work, including how they load the required library classes.
Finally, we created a realistic working command-line program that could be run unattended in the web server. This program monitors our subscriptions for expired subscriptions and ones about to expire. This demonstrates how the platform can allow us to create programs that interact with a Joomla website.
The platform is an exciting new project with almost unlimited potential. It is likely that a number of interesting programs will be built using the platform. Features added to the platform will also benefit the Joomla CMS.