Module Development 1: Content Modules

From PapayaCMS

Jump to: navigation, search

Tutorial

Abstract This tutorial describes how to write a simple module or plugin for papaya CMS.
Intended audience PHP developers
Level intermediate
Software requirements Current nightly build of papaya CMS, to be downloaded here
Date 2010-05-06
Next tutorial Module Development 2: Adding Database Support

Before you can start writing modules, you need to install and preconfigure papaya CMS properly, which is described here.

Please note that you should adhere to the Papaya CMS Coding Standards, especially if you plan to contribute your modules for the papaya Community.

Contents

Overview of files and directories

After finishing this tutorial, you will have created the following files and subdirectories in papaya-lib/modules/special/myproject/tutorial (see Selecting your directory below for choosing special/myproject or possible alternatives):

File:Tutorial1moduledirstructure.png

In the testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial directory, you will have created the following structure:

File:Tutorial1testingstr.png

Getting started

The first module we want to write is a simple page module that outputs the famous "Hello World" message. It does not need database access and only consists of a single file. Later in this tutorial, we are going to extend the module, allowing a website editor to add a custom message to "Hello World".

Terminology: module vs. module package

If you explore the modules subdirectory in the papaya-lib directory, you will discover a few subdirectories (which are explained in detail in the next subsection). Each of these subdirectories contains either files or other subdirectories. A directory like modules/_base/community is called a module package. Most of the files in it share the same database tables and cooperate to provide a certain service; the community package, for example, handles frontend user-logins, user profiles, contacts, etc.

Modules are files located in the directory representing a module package and are registered in the modules.xml identified by a Global Universal Identifier (guid). A module provides one aspect of the package's purpose. The community package, for instance, contains a file called content_login.php which is a standard login page, or box_login.php, a login box to be included into other pages.

You can explore all available packages and their modules by clicking on Modules in the main toolbar of the papaya CMS backend. A sidebar on the left contains all packages. When you click on one of them, the middle column of the page will show the package's modules (and its database tables). If you click on a module, the right column will show information about the module and allow you to control some of its settings.

Selecting your directory

If you look around in the papaya-lib/modules directory of your papaya installation, you will discover at least the following subdirectories:

  • _base -- basic modules like simple HTML box or all of the community stuff
  • free -- free modules under the GNU GPL
  • gpl -- free modules depending on third-party GPL software
  • sample -- a very simple sample module similar to the one we are going to develop in this tutorial

If you happen to buy one or more commercial papaya modules, they will be stored in an additional subdirectory called commercial.

If you write a module that only suits the purposes of your specific project, create a new subdirectory called special, and within it another subdirectory whose name reflects your project. Modules for a less specific purpose are usually stored in the free directory (or in a beta directory while in development). Don't do that without contacting us, though, because it might complicate further updates of your papaya CMS difficult if someone else contributes a module package of the same name. So let's stick with special/myproject for now.

After you have selected or created the directory you want your module to be stored in, create a subdirectory of this one called tutorial.

Preparing the modules.xml file

Each module package needs a module information file called modules.xml. This file lists the modules and the database tables within this package, and by scanning the module directories recursively for these files, papaya CMS will know about new, changed, or deleted modules.

Here is a short sample for a modules.xml file, taken from the _base/countries module package:

<?xml version="1.0"  encoding="ISO-8859-1" ?>
<modulegroup>
  <name>Countries</name>
  <description>
    The country package provides backend functionality to manage continents and
    countries as well as a connector with form callback functions.
  </description>
  <modules>
    <module type="page"
            guid="fd53aef2d8bb7cb4637a64dabaf7b424"
            name="State List XML"
            class="content_statelist"
            file="content_statelist.php">
      Returns an XML list of states for a specific country, to be used for Ajax
    </module>
    <module type="admin"
            guid="bf6e40b71d3cfb0e80682c64b11d33af"
            name="Countries"
            class="edmodule_countries"
            file="edmodule_countries.php"
            glyph="countries.png">
      The administration module provides the facility to manage continents,
      countries, and their localized names.
    </module>
    <module type="connector"
            guid="99db2c2898403880e1ddeeebf7ee726c"
            name="Country Connector"
            class="connector_countries"
            file="connector_countries.php">
      Country Connector
    </module>
  </modules>
  <tables>
    <table name="continents"/>
    <table name="countries"/>
    <table name="countrynames"/>
    <table name="states"/>
    <table name="countries_old"/>
    <table name="states_old"/>
  </tables>
</modulegroup>

The modulegroup root element encloses all the content. The name and description elements only contain plain text -- the package's name and abstract, respectively. The modules section contains a module element for each of the package's modules, while tables has got a table entry for each database table.

A module element consists of five or more attributes and an enclosed description in plain text. The attributes are defined as follows:

  • type -- the module type, e.g. "page" for a page module or "box" for a box module
  • guid -- a unique identifier for the module: a string containing a hexadecimal 128-bit number
  • name -- the module's name to be displayed in the papaya backend
  • class -- real name of the PHP class that declares the module
  • file -- the file containing the module (optionally, you can provide relative paths for files in subdirectories)
  • glyph -- the name of an icon file for the module (only for admin type modules as they are used to manage the whole package)
  • outputfilter -- an optional attribute for content modules (types page or box): If you set this attribute to the value "no", it will not use an output filter to create its final output (the concept of output filters is explained more detailed in ##papaya_architecture##)

The table elements in the tables section only contain a name attribute. The table structures themselves are stored in the DATA subdirectory of each module package directory. These files should never be written manually; the next part of this tutorial will show you how to generate them from the papaya backend.

For now, we need a fresh modules.xml file for our intended package (that will only contain a single module and no database tables). Open your preferred text or XML editor and type (or copy and paste) the following:

<?xml version="1.0" encoding="utf-8" ?>
<modulegroup>
  <name>Hello World tutorial module</name>
  <description>A tutorial to learn papaya CMS module development.</description>
  <modules>
    <module type="page"
            guid=""
            name="Hello World Page"
            class="HelloPage"
            file="Hello/Page.php">
      This simple page module displays a Hello World message
    </module>
  </modules>
</modulegroup>

Please note that we have left the guid attribute blank for now. But obviously we need a value because the module will be registered using this identifier. You can either use the GUID Generator, or write your own simple PHP code (most other languages would do, as well) such as the following:

<?php
 
echo md5(rand());
 
?>

Now paste your new hash value in between the quotes of the guid attribute and save the file.

Writing the module

There is a difference in the recommended directory and file name structure for papaya modules between older, PHP-4-compatible parts and new PHP-5-only packages. This is because we want to use unit testing for the new modules. If you have never heard about unit testing in general or PHPUnit in particular, never mind -- everything necessary will be explained along the way. One of the best resources for getting started with unit testing, though, is the article Test Infected: Programmers Love Writing Tests by Erich Gamma and Kent Beck. For detailed information about PHPUnit, visit the official PHPUnit site.

For this project, we are even going to use the test-first approach: Write a unit test for each part of your code before you write the actual code.

Before we start writing the actual code, we are going to create the basic directory structure.

Just create the following directories in your selected location:

+ tutorial [you already created this one, containing the modules.xml file]
|
+--+ Hello
   |
   +--+ Page

Now find the testing/test-unittests directory of your papaya installation. It should already contain a papaya-lib/modules subpath (if not, just create it). Next up, create the same nested structure of tutorial/Hello/Page directories in the subdirectories of the testing environment that reflects the structure's location in the module hierarchy.

Top-down and test-driven: a static content module

Test-driven, object-oriented code should be written using a top-down approach: You start with a static implementation of what the user will see on screen and implement the underlying logic later. This approach guarantees that each part of the code is independent and can be flexibly exchanged by another implementation at any given time.

The first part to be implemented is the page module itself. Create an empty PHP file called Page.php in the tutorial/Hello directory, and another empty PHP file called PageTest.php in the tutorial/Hello directory in the unit testing directory tree.

Following the test-first approach, you should prepare the unit test class file and write the first test before writing any implementation code. A PHPUnit test case class extends the PHPUnit_Framework_TestCase base class. For the specific needs of papaya CMS unit tests, there's already a PapayaTestCase class you can extend, it's located in the Framework subdirectory of the testing/tests-unittests path.

The stub of the PageTest.php class file is as follows:

<?php
 
require_once(substr(dirname(__FILE__), 0, -53).'/Framework/PapayaTestCase.php');
require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page.php');
 
class HelloPageTest extends PapayaTestCase {
}
 
?>

To determine the correct include path of the PapayaTestCase.php file, you need to count the characters of your test directory's subpath underneath testing/tests-unittests. In the above example, 53 characters are used, assuming the subpath to be /papaya-lib/modules/special/myproject/tutorial/Hello. You need to adjust this if your directory structure is different. The same applies to the next line that imports the class to be tested.

There is one more issue we need to consider if we want to unit-test a papaya content module class: The constructor of base_plugin, the common ancestor of the base classes for both page and box content modules (base_content and base_actionbox, respectively) takes in an owner object reference. As a test class is no suitable owner for an object like this, we are going to provide a so called proxy class that extends our module class and overrides the constructor with one that does not need an owner reference. Just add the following code underneath the closing curly brace of the HelloPageTest class definition:

class HelloPageProxy extends HelloPage {
  function __construct() {
    // Nothing to do here, just override parent's constructor
    // to get rid of the mandatory parameter
  }
}

Now you can write a private method within the test class to instantiate the class to be tested (or, in this case, the proxy class). Add the following method to the HelloPageTest class:

/**
* Instantiate the HelloPage object to be tested
*
* @return HelloPage
*/
private function getHelloPageObjectFixture() {
  return new HelloPageProxy();
}

The method we want to test and then implement is called getParsedData( ). It is the only mandatory method in a papaya content module and is used to return well-formed XML that can be transformed to its final output format using XSLT templates. In the top-down approach we have chosen to follow, the first implementation of this method is static (not static in terms of a class method, but in terms of a determined, constant return value): We write the test with the expectation of some fixed XML to be returned, and then implement the method to return exactly this.

Add the following test method to the test class:

/**
* @covers HelloPage::getParsedData
*/
public function testGetParsedData() {
  $helloPageObject = $this->getHelloPageObjectFixture();
  $expectedXml = '<title>Hello world!</title>
<text>Greetings from the new module</text>';
  $this->assertXmlStringEqualsXmlString(
    $expectedXml,
    $helloPageObject->getParsedData()
  );
}

The names of testing methods in unit tests always need to begin with test in order to be executed by PHPUnit, and they have to be public. The @covers PHPDoc annotation is used to determine the code coverage of your unit tests -- simply put, the percentage of code covered by tests. Using these annotations, you can prevent your tests from covering other methods that are called implicitly by the tested methods.

The most important part of unit tests is the set of assert...( ) methods that can be used to match the return values of tested methods against virtually any kind of condition. The basic assertEquals( ) method tests for equality and is one of the most frequently used methods of the set. The variant assertXmlStringEqualsXmlString( ) ignores whitespace differences which makes it ideal for XML. Please note that it is common practice to choose the above order: the expected value as the first and the actual method call to be tested as the second argument.

Next, you can start writing your HelloPage class and add the stub of the getParsedData( ) method to it:

<?php
 
/**
* Hello World tutorial page module
*
* @package Papaya-Modules
* @subpackage tutorial
*/
 
/**
* Base class base_content
*/
require_once(PAPAYA_INCLUDE_PATH.'system/base_content.php');
 
/**
* Hello World tutorial page module class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
class HelloPage extends base_content {
 
  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
  }
 
}
 
?>

Like in this example, you should make use of PHPDoc to document your code. Details about PHPDoc can be found on the official website.

It may sound quite absurd, but you should now execute your unit test before implementing the actual code although you already know it will fail. The working order for test-driven development is:

  1. Write a test that fails
  2. Implement the code that is necessary to pass the test
  3. Refactor the code to match the overall needs of your project

or, even shorter: "red, green, refactor" -- reflecting the fact that in most graphical representations of unit tests, failed tests are displayed in red, while passed ones are displayed in green.

To run your PHPUnit test from console, type the following:

$ phpunit [path/to/]HelloPageTest.php

As the test fails, the output looks like this:

PHPUnit 3.4.2 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) HelloPageTest::testGetParsedData
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-<title>Hello world!</title>
-<text>Greetings from the new module</text>
+

.../testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial/Hello/PageTest.php:23

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

As you can see, the expected return value (the XML code) differs from the actual return value (nothing at all). Which means that it's definitely time to implement a static version of getParsedData( ) that matches the test:

  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
    $result = '<title>Hello world!</title>'.LF;
    $result .= '<text>Greetings from the new module</text>';
    return $result;
  }

Run your test again, and it should report success (a single '.' for a single passed test instead of an 'F' for a failed one):

PHPUnit 3.4.2 by Sebastian Bergmann.

.

Time: 0 seconds

OK (1 test, 1 assertion)

...and that's it! You have successfully written an, albeit static, papaya page module. If you work with SVN or another version control system, it's now time to commit your code to the repository. This behavior is known as continuous integration, a basic part of agile software development methods: Commit as often as you can, providing "clean code that works" by matching the preset unit tests before you commit.

Time for a real-life frontend test

Apart from unit testing, you should also test your module in your browser. Choosing the title and text XML elements for the result allows us to use the existing XSLT template for default pages, so there's no need to write a template now. To test your module in papaya CMS, follow these steps:

  1. Log in to the papaya backend as an administrator.
  2. In the main toolbar, choose Modules, and click on Search modules. Your module should be added after some scanning time depicted by a progress bar.
  3. Now click on Views in the main toolbar. A view is a module linked with a template that results in a specific type of frontend output and functionality.
  4. In the Views section, you should see a form with Title and Module fields to create a new view; if not, click on the Views button in the subtoolbar.
  5. Enter Hello World Page as the module's title, select [page] Hello World Page from the Hello World tutorial module section of the Module selector, and click the Add button. The message View added. is displayed.
  6. On the right, you see a sidebar called Output filter (if not, you haven't configured your papaya CMS yet). Check the Linked checkbox next to the html extension. The warning No XSLT file specified. will be issued.
  7. From the XSL stylesheet selector, choose page_main.xsl, ignore the rest of the settings, and click on Save. There is a Changes saved. message and a XSLT file "page_main.xsl" may not support module "HelloPage". warning; you can safely ignore the latter for now.
  8. Choose Pages from the main toolbar.
  9. Navigate through the page tree of your website using the sidebar on the left. When you have found a location for your page, click on Add page to create a new page on the same level as the currently selected page, or on Add subpage to create a page on a sublevel of the current one.
  10. On the Properties tab for the new page, enter the title Hello World Page and click on Save.
  11. Now select the View tab and choose the Hello World Page view from the Hello World tutorial module section by clicking onto its name.
  12. You don't need to set any content for your new page as it's static; the Content tab just says No edit dialog for this module. So you can select the Preview tab and watch the frontend output of your new module: Hello World! as a headline and Greetings from the new module as page content.
  13. The Preview section allows you to switch between HTML and XML view modes, so you can see how the page XML gets transformed into HTML by the default template.

Refactoring: creating a dynamic module

Our static module is ready, but now we want to make it dynamic: The text underneath the Hello World headline should be editable by a website editor. This requires some refactoring. The common approach for modern, test-driven papaya modules is to have a Base class underneath the frontend class handle the dynamic output and other logic. In this step, we are going to create this Base class and connect it to the frontend class.

To make a frontend class configurable, just add an array-type public attribute called $editFields. If there is a lot of stuff to be edited, you can choose the alternative $editGroups, a nested array in which sets of edit fields are displayed on multiple pages. Let's stick with the classic $editFields for now as there is only a single field for our module.

Add the following code to your HelloPage class, just above the doc block and declaration of the getParsedData( ) method:

/**
* Parameter namespace
* @var string
*/
public $paramName = 'tut';
 
/**
* Edit fields for page configuration
* @var array
*/
public $editFields = array(
  'text' => array(
    'Text',
    'isNoHTML',
    TRUE,
    'textarea',
    5,
    '',
    'Greetings from the new module'
  )
);

After saving this change, you can already test it: In the Pages section of the papaya CMS backend, choose the Content tab of your new page, and instead of the previous warning, you will see a textarea to edit the page text and a Save button.

The string $paramName is the common namespace for parameters of this module, both in frontend and in backend (for the configuration dialog). It will prepend parameters using a configurable delimiter.

In the $editFields array, the key for each field is its internal name, while the value is an array with the following fields:

  • The caption to be displayed in the page's content form
  • Check type (as defined in the sys_checkit class of papaya's system directory). 'isNoHTML' is used to check for arbitrary text that must not contain HTML markup.
  • A boolean that determines whether data in this field is mandatory (TRUE) or not (FALSE)
  • The field type (e.g. 'input' for a single-lined text field, 'textarea', 'checkbox', etc.)
  • The field settings (depends on the field type; for 'input' it determines the maximum number of chars that can be entered, for 'textarea' it's the visible number of lines)
  • Optional tooltip that will be displayed as a mouseover text for a little lamp icon next to the caption, if present
  • Default content -- optional as well, but you should make sure that each field with a mandatory value of TRUE has got default content

You can also add simple strings without dedicated indexes to the $editFields array. These will serve as section headings in your edit dialog.

Each field you define in the $editFields array will later be present in an attribute called $data, either with its default value or with the one set by the website editor.

The next step is to create the Base.php class file in the Page subdirectory and its unit test. The stub of the Base class looks as follows:

<?php
 
/**
* Hello World tutorial page module, base class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
 
/**
* Hello World tutorial page module class, base class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
class HelloPageBase {
}
 
?>

And here's the basic content of the the unit test, Page/BaseTest.php:

<?php
 
require_once(substr(dirname(__FILE__), 0, -58).'/Framework/PapayaTestCase.php');
require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php');
 
class HelloPageBaseTest extends PapayaTestCase {
}
 
?>

The Base class doesn't have a dedicated constructor, and it doesn't extend any other class, either, so the code to load the tested object is as straightforward as this:

  /**
  * Instantiate the HelloPageBase object to be tested
  *
  * @return HelloPageBase
  */
  private function getHelloPageBaseObjectFixture() {
    return new HelloPageBase();
  }

The first method we test and implement is called setPageData( ). It is used to pass the configuration data from the page class's edit fields to the Base class, and to pass other data from the unit test. This is the test for the intended method:

  /**
  * @covers HelloPageBase::setPageData
  */
  public function testSetPageData() {
    $helloPageBaseObject = $this->getHelloPageBaseObjectFixture();
    $data = array('text' => 'Hello');
    $helloPageBaseObject->setPageData($data);
    $this->assertAttributeEquals($data, '_data', $helloPageBaseObject);
  }

As the configuration data will be stored in a private attribute called $_data, the assertion has to be done using the assertAttributeEquals( ) method. This method takes in three parameters: The value you want to compare the attribute to, the attribute name as a string without leading $ sign, and the object whose attribute you want to read.

Now add the following code to the Base class, right underneath the opening curly brace of the class definition:

   /**
   * Page configuration data
   * @var array
   */
   private $_data = array();
 
   /**
   * Set page configuration data
   *
   * @param array $data
   */
   public function setPageData($data) {
   }

Run the unit test, watch it fail, and then add the actual implementation of the setPageData( ) method to a line bewteen its curly braces:

    $this->_data = $data;

Now the test should work, so you can commit your code once again. Using the same test-failure-implementation workflow, you can now implement a method called getPageXML( ) which will be called by the page module's getParsedData( ) method to create the page's dynamic output. Here's the test:

  /**
  * @covers HelloPageBase::getPageXML()
  */
  public function testGetPageXML() {
    $helloPageBaseObject = $this->getHelloPageBaseObjectFixture();
    $helloPageBaseObject->setPageData(array('text' => 'Hello'));
    $expectedXml = '<title>Hello world!</title>
<text>Hello</text>';
    $this->assertXmlStringEqualsXmlString(
      $expectedXml,
      $helloPageBaseObject->getPageXML()
    );
  }

And this is the method implementation itself (but don't forget to run the test without the actual code first):

  /**
  * Get the page's XML output
  *
  * @return string XML
  */
  public function getPageXML() {
    $result = '<title>Hello world!</title>'.LF;
    $result .= sprintf('<text>%s</text>', papaya_strings::escapeHTMLChars($this->_data['text']));
    return $result;
  }

The implementation is pretty straightforward. Please note the static papaya_strings::escapeHTMLChars( ) method call, though. It makes sure that HTML charactars are escaped in strings that are supposed to be plain text. It should be used whereever plain text user input or configuration data are returned for output in both frontend and backend. LF is a system-wide papaya CMS constant that creates a line break.

Now our Base class is ready, and we can refactor the content class to make use of it. First, we implement a method called setBaseObject( ) to set the instance of the Base class to be used. This is called dependency injection. It can be used both for unit testing (to replace the real object by a so-called mock object that behaves just like we want it to) and to simply change implementation details later by inserting another object.

As usual, we write the test first (note that we switch back to the HelloPageTest class now). First, add one more require_once( ) statement underneath the others:

require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php');

We won't use a real instance of this class, but if the definition is present, the mock object we are going to use instead of it will automatically be modeled matching the real class, with all public attributes and methods. Now for the test method:

  /**
  * @covers HelloPage::setBaseObject
  */
  public function testSetBaseObject() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $helloPageBaseObject = $this->getMock('HelloPageBase');
    $helloPageObject->setBaseObject($helloPageBaseObject);
    $this->assertAttributeSame($helloPageBaseObject, '_baseObject', $helloPageObject);
  }

Please note that we use assertAttributeSame( ) instead of assertAttributeEquals( ) this time. The parameter syntax is the same, but as we're dealing with an object reference rather than a plain value, it's appropriate to test for strict object identity.

Before you implement the method, add the following attribute declaration to the start of the class body:

  /**
  * Instance of the HelloPageBase class
  * @var HelloPageBase
  */
  private $_baseObject = NULL;

And here's the method's implementation:

  /**
  * Set the HelloPageBase object to be used
  *
  * @param HelloPageBase $baseObject
  */
  public function setBaseObject($baseObject) {
    $this->_baseObject = $baseObject;
  }

The counterpart of the setBaseObject( ) method is getBaseObject( ) which instantiates the HelloPageBase class only if necessary, i.e. if the object has not been set using setBaseObject( ) before. This is another important technique called lazy initialization. Along with dependency injection, it's one of the key patterns of test-driven, object-oriented software design.

Write the following test for the getBaseObject( ) method:

  /**
  * @covers HelloPage::getBaseObject
  */
  public function testGetBaseObject() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $baseObject = $helloPageObject->getBaseObject();
    $this->assertTrue($baseObject instanceof HelloPageBase);
  }

The implementation of the method looks as follows:

  /**
  * Get (and, if necessary, initialize) the HelloPageBase object
  *
  * @return HelloPageBase
  */
  public function getBaseObject() {
    if (!is_object($this->_baseObject)) {
      include_once(dirname(__FILE__).'/Page/Base.php');
      $this->_baseObject = new HelloPageBase();
    }
    return $this->_baseObject;
  }

As everything is up and running now, we only need to reimplement the getParsedData( ) method to pass the configuration data to the Base object and return whatever the Base object's getPageXML( ) method returns. Here's the revised test:

  /**
  * @covers HelloPage::getParsedData
  */
  public function testGetParsedData() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $helloPageBaseObject = $this->getMock('HelloPageBase');
    $expectedXml = '<title>Hello world!</title>
<text>Greetings from the new module</text>';
    $helloPageBaseObject
      ->expects($this->once())
      ->method('getPageXML')
      ->will($this->returnValue($expectedXml));
    $helloPageObject->setBaseObject($helloPageBaseObject);
    $this->assertXmlStringEqualsXmlString(
      $expectedXml,
      $helloPageObject->getParsedData()
    );
  }

Here's one of the virtues of dependency injection in action: We create our own Base object and model it to behave however we like, and then set it using the setBaseObject( ) method. To create this custom object, we call PHPUnit's getMock( ) method with the class name (there are more, optional parameters for getMock( ) like an array of method names, but we don't need them because we required the original class). The expects( ) structure defines the intended return value for a given method -- in this case, we expect the method getPageXML( ) of our Base object to be called exactly once, returning an XML string value. Within the tested method, the mock object will return the expected value, and the test will fail if the method is not called exactly once.

After we've written the test, we can reimplement the getParsedData( ) method using the Base object:

  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
    $this->setDefaultData();
    $baseObject = $this->getBaseObject();
    $baseObject->setPageData($this->data);
    return $baseObject->getPageXML();
  }

One last thing that needs to be explained here is the setDefaultData( ) method in content modules: If edit fields have got default values (the optional last element in their arrays), this call sets all elements in the $this->data attribute to those default values unless a special value has been set in the page configuration.

Run the test to see it pass. Aftwards, you can use the page's Content tab to enter an arbitrary message. Test the output using the Preview section, and your custom content will be displayed. Congratulations -- you have written your first papaya CMS module including dynamic data, and along the way you should have learned two or three things about test-driven development if you're not already familiar with it.

Next tutorial: Module Development 2: Adding Database Support

Personal tools
In other languages