Chapter 5. Working with CMS Data

Your job as a CMS developer is to make managing and publishing information as easy as possible. This challenge is compounded by the fact that the information can take many forms on the Web. There are as many solutions to this problem as there are CMSs, each with its own pros and cons.

One of the real benefits of developing with a framework like Zend Framework is that you don't have to worry about much of the low-level application logic. The framework provides a standardized API, which you can use as you focus on the specific needs of your project. In this chapter, we will follow this model to develop a flexible content management API that the entire CMS will use.

Exploring the Data Structure

How you structure this data plays a large role in how straightforward data management is. There are several approaches, each with its own pros and cons.

Traditional CMS Data Structures

Most traditional content management systems use a fairly standardized data structure that closely parallels the organization of written materials. This structure is broken down into sections and pages, which share some common characteristics, but they are unique entities in the database. Each additional form of content, such as a news article, would have its own table with its own properties.

There are obvious advantages to this approach. The most notable is simplicity. A database that is designed in parallel with the final output data structure is easier and more intuitive to work with. The code is simple and thus less error prone.

The foremost disadvantage is how this structure accommodates changes in the front-end format. Since the database is tightly coupled with the application logic, a simple change on the front end can require updates in every layer of the system. Over the course of time, this can lead to increasingly complex refactoring, which in turn can lead to more bugs. Eventually, such systems will need to be rewritten to accommodate new ways of publishing content.

Abstract Data Structures

Abstract data structures look at content in a different way. In an abstract system, content is content. The database's primary responsibility is to store and serve this content, not to make sense of it. This is solely the application's job.

Let's consider a common example to illustrate how this works. Say your CMS has two modules, one that manages news and one that manages events. On the front end of your site, news articles and events are unique items with unique properties. The key here is to look at the underlying data from another perspective, focusing on what these items have in common.

Looking at Table 5-1, it seems like a good idea to create a common content table that serves both news and events. This would work well initially but does not completely resolve the challenge of building a system that meets your current needs as well as those in the future.

Table 5.1. Comparing News Articles and Events

Field Type

News Article

Event

Text

Headline

Title

Text

Author

Event contact

Text

Teaser

Description

Text

Article

Event

Date

Publish date

Start date

Date

Archive date

End date

Designing a Database That Can Grow with Your System

The preceding example demonstrated that data can be standardized but did not allow for the fact that not all forms of data can fit into this standard structure. Say, for example, that you needed to add a location to the CMS events. Many news articles could use this field, but many would not. In a simple example like this, the problem may not have any practical impact; the articles that don't have a location could simply ignore this field. In a more complex, real-world example, this issue can compound and create more complex issues than the original structure resolves.

The traditional approach to this challenge is to create user-defined fields. A commercial CMS database I recently worked with had one large content table that had several extra fields of each data type: date_1, date_2, text_1, and so on. This solution does allow for future growth but has many serious issues, including:

  • What do you do when you need more than two date fields?

  • How will the next developer who takes over the project know what the text_2 field is in different contexts?

One solution to this challenge is the node pattern (Figure 5-1). This consists of two components: pages that are the main container and content nodes that are the actual blocks of content. A given page can have any number of content nodes.

Note

You may notice that the node pattern, as I refer to it, is very similar to the EAV database pattern. The one difference is the fact that the node pattern uses concrete tables, which can improve performance.

For example, a simple page might have two nodes, the headline and content. A more complicated example might be a blog post that has a headline, teaser, image, and content.

The content node pattern

Figure 5.1. The content node pattern

The key of this pattern is the fact that since any page can have any number of content nodes, the same database tables can serve the simple page, a blog page, or any other page. The abstract nature of the relationship between the CMS and its underlying data makes it possible to make substantial changes in your site structure without altering the database.

Note

Pages can contain other pages using the structure that we are developing.

Implementing the Data Management System

The data management system is the core of a CMS project. This system will manage all content for the CMS, so it is probably the most important aspect of the project. If you design it well, the CMS will be easy to work with and serve a wide range of needs; if not, you will inevitably be refactoring the system before long.

In the previous section, I went over how to create flexible data systems, focusing on the node pattern, which you will base the system on. Now that you understand the basic principles of the system, you need to determine how these abstract concepts can be used to manage the content of your CMS.

The first step is to decide exactly what information you want to make available in the page object. This data is then broken down into items that are going to be required for every page and items that may be variable. The items that are required by every page are managed by the page component; these include the name and content type. Then the headline, main content, and any other content blocks are managed by the content node component.

Managing Content Nodes

Content nodes are the core content containers, so they are a logical place to start.

Creating the content_nodes Table

The nodes table will be very simple because it will store the content as a list of key-value pairs, with one additional field to make the relationship to the page:

  • id: The primary key

  • page_id: The foreign key that is used to relate the content node to the page

  • node: The key for the content node, such as headline or teaser

  • content: The actual content data, usually HTML or text

Let's start by creating this table. Run the SQL script in Listing 5-1 to create a new table named content_nodes.

Example 5.1. SQL Statement to Create content_nodes Table

CREATE TABLE `content_nodes` (
  `id` int(11) NOT NULL auto_increment,
  `page_id` int(11) default NULL,
  `node` varchar(50) default NULL,
  `content` text,
  PRIMARY KEY  (`id`)
) DEFAULT CHARSET=utf8;

Creating the ContentNode Model Class

Once this table is created, you need to create the model. First create the file for the model class. Then create a new file in application/models named ContentNode.php. Open this file, and follow the standard Zend model routine of creating a model class that extends Zend_Db_Table_Abstract and then setting the table name property to content_nodes (Listing 5-2).

Example 5.2. Setting Up the Base ContentNode Model Class in application/models/ContentNode.php

<?php
require_once 'Zend/Db/Table/Abstract.php';
require_once APPLICATION_PATH . '/models/Page.php';

class Model_ContentNode extends Zend_Db_Table_Abstract {
      /**
            * The default table name
            */
            protected $_name = 'content_nodes';
}
?>

Creating and Updating Content Nodes

I like to consolidate create and update methods in my model classes; this saves a lot of repetitive coding. It works like this:

  1. It will check if whether the page already has this key set; it will use this row if it exists, or it will create a new one if it does not.

  2. The row's content is set to the new value.

  3. The row is saved.

The advantage of this approach is it makes it easier to use the model. When you need to set a content node, you don't have to repeat the code to determine whether it needs to be created in each of your child classes. Add the setNode() method shown in Listing 5-3 into the ContentNode.php file.

Example 5.3. The setNode() Method in application/models/ContentNode.php

public function setNode($pageId, $node, $value)
{
    // fetch the row if it exists
    $select = $this->select();
    $select->where("page_id = ?", $pageId);
    $select->where("node = ?", $node);
    $row = $this->fetchRow($select);

    //if it does not then create it
    if(!$row) {
        $row = $this->createRow();
        $row->page_id = $pageId;
        $row->node = $node;
    }

    //set the content
    $row->content = $value;
    $row->save();
}

Deleting Nodes

Zend Framework will handle the relationship between nodes and pages. When you define these relationships, you will turn on cascading deletes, so Zend_Db_Table will handle removing nodes when you remove a page.

Managing Pages

The Page model class will serve as a higher-level interface to the CMS pages and their associated content nodes. You will start by creating the pages table.

Creating the pages Table

The pages table will need to store information that every page must have:

  • id: This is the primary key.

  • namespace: This is the type of page, such as a news article.

  • parent_id: This is the parent page.

  • name: This is the name of the page.

  • date created: This is the date/time the page was created.

To create the pages table, use the SQL script shown in Listing 5-4.

Example 5.4. SQL Statement to Create the pages Table

CREATE TABLE `pages` (
  `id` int(11) NOT NULL auto_increment,
  `parent_id` int(11) default NULL,
  `namespace` varchar(50) default NULL,
  `name` varchar(100) default NULL,
  `date_created` int(11) default NULL,
  PRIMARY KEY  (`id`)
) DEFAULT CHARSET=utf8;

Creating the Page Model Class

Once this table is created, you can create the model. First create the file for the page's Model class. Create a new file in application/models named Page.php. Open this file, follow the standard Zend model Model routine of creating a Model class that extends Zend_Db_Table_Abstract, and then set the table name property to pages (Listing 5-5).

Example 5.5. Setting Up the Base Page Model Class in application/models/Page.php

require_once 'Zend/Db/Table/Abstract.php';
require_once APPLICATION_PATH . '/models/ContentNode.php';

class Model_Page extends Zend_Db_Table_Abstract {
    /**
     * The default table name
     */
    protected $_name = 'pages';

}

Creating Pages

Next you need to create a method to create a new page. This method will use the Zend_Db_Table instance's createRow() method. Once it creates the row, it will set the row's name, namespace (content type), parent ID, and date it was created. It will return the ID of the page that was just created (Listing 5-6).

Example 5.6. The createPage() Method in application/models/Page.php

public function createPage($name, $namespace, $parentId = 0)
{
    //create the new page
    $row = $this->createRow();
    $row->name = $name;
    $row->namespace = $namespace;
    $row->parent_id = $parentId;
    $row->date_created = time();
    $row->save();
    // now fetch the id of the row you just created and return it
    $id = $this->_db->lastInsertId();
    return $id;
}

Updating Existing Pages

Now you need a method to update an existing page. This method will use the Zend_Db_Table instance's find() method to fetch the requested page. If it finds the row, then the first thing it will do is update each of the primary page fields. Once it has done this, it will unset these fields from the data array, leaving the non standard fields that will go in the content node table. Next it will cycle through the rest of the fields in the data array, setting each one as a content node (Listing 5-7).

Example 5.7. The udpatePage() Method in application/models/ContentNode.php

public function updatePage($id, $data)
{
    // find the page
    $row = $this->find($id)->current();
    if($row) {
        // update each of the columns that are stored in the pages table
        $row->name = $data['name'];
        $row->parent_id = $data['parent_id'];
        $row->save();
        // unset each of the fields that are set in the pages table
        unset($data['id']);
        unset($data['name']);
        unset($data['parent_id']);
        // set each of the other fields in the content_nodes table
        if(count($data) > 0) {
            $mdlContentNode = new Model_ContentNode();
            foreach ($data as $key => $value) {

                $mdlContentNode->setNode($id, $key, $value);
            }
        }
    } else {
        throw new Zend_Exception('Could not open page to update!'),
    }
}

Deleting a Page

Deleting a page is a simple process. The framework handles the cascading deletes, which will clean up the content_nodes table for you when you delete the parent page (Listing 5-8).

Example 5.8. The deletePage() Method in application/models/Page.php

public function deletePage($id)
{
    // find the row that matches the id
    $row = $this->find($id)->current();
    if($row) {
        $row->delete();
        return true;
    } else {
        throw new Zend_Exception("Delete function failed; could not find page!");
    }
}

Defining and Working with Table Relationships

One of the main factors that distinguishes a relational database from a spreadsheet or other list of data is that the rows in different tables can be related. Zend_Db_Table provides a mechanism to define and work with these relationships.

Defining the Relationships

When you are defining a one-to-many relationship, as in this case where one page has many content nodes, you need to do two things:

  • Page model: Tell Zend_Db_Table that the pages table is dependent on the content_nodes table.

  • ContentNode model: Tell Zend_Db_Table that the content_nodes table is related to the pages table, setting the columns that relate them, the reference table class (Page in this case), and the reference columns, which are the columns in the reference table.

The ContentNode to Page Relationship

You define the relationship by setting the protected _referenceMap property in the class declaration. In the case of the content_nodes to pages relationship, the content_nodes table's parent_id column is what relates it to the pages table's id column. When you delete a page, you want it to cascade delete all the related content nodes, and when you update the foreign key, you want it to restrict the update.

Add the _referenceMap property directly after the _name property in the ContentNode model, as in Listing 5-9.

Example 5.9. The content_nodes to Page Reference Map in application/models/ContentNode.php

protected $_referenceMap    = array(
    'Page' => array(
        'columns'           => array('page_id'),
        'refTableClass'     => 'Model_Page',
        'refColumns'        => array('id'),
        'onDelete'          => self::CASCADE,
        'onUpdate'          => self::RESTRICT
    )
);

The Page to Parent Page Relationship

The Page model has two relationships to control: the page to content_nodes relationship and the page-to-page relationship.

The page to content_nodes relationship is defined by the child table, which is content_nodes in this case. You simply need to tell Zend_Db_Table that the pages table is dependent on the content_nodes table.

The page-to-page relationship is handled by the parent_id and id fields. You define this using the same technique as you did with the ContentNode model.

Add the dependentTables and referenceMap properties in Listing 5-10 to the top of your Page model, directly below the $_name property.

Example 5.10. The content_nodes to page and Page-to-Page References in application/models/Page.php

protected $_dependentTables = array('Model_ContentNode'),
protected $_referenceMap    = array(
    'Page' => array(
        'columns'           => array('parent_id'),
        'refTableClass'     => 'Model_Page',
        'refColumns'        => array('id'),
        'onDelete'          => self::CASCADE,
        'onUpdate'          => self::RESTRICT
    )
);

Working with Related Items

Zend_Db_Table_Row has a number of methods for fetching related records using the references you just set up. These include standard methods, such as findDependentRowset(), as well as a set of magic methods; these are methods that are created on the fly.

The following code samples are examples only; you will create the actual methods in the Content Items section, which follows.

To fetch all the content nodes for a given page, you first need to find the page row. Once you have that row, you use the findDependentRowset() method, to which you pass the class name of the table you want it to search, as in the example in Listing 5-11.

Example 5.11. Fetching Dependent Rows

$mdlPage = new Model_Page();
$page = $mdlPage->find(1)->current();
$contentNodes = $page->findDependentRowset(' Model_ContentNode'),

To go the other direction and find a parent row for a given row, you use the findParentRow() method. You pass this method the class name for the parent table as well, as in Listing 5-12.

Example 5.12. Fetching Parent Rows

$mdlContentNode = new Model_ContentNode();
$node = $mdlContentNode->find(1)->current();
$parentPage = $node->findParentRow(' Model_Page'),

Cascading Updates and Deletes

Zend_Db_Table supports cascading write operations, but this is intended only for database systems that do not support referential integrity, such as MySQL. You set these options in the referenceMap and can set either to cascade or to restrict. If you set this to cascade, it will automatically update the child rows. If you set it to restrict, then it will throw an error when you attempt to modify a row with dependencies.

Working with Content Items

The Page and ContentNode model classes provide an abstraction layer between your application and the underlying database structure. This makes working with the data much more straightforward, but it still has room for improvement.

The issue is that over the course of creating this flexible data structure, you have made managing the data more complicated than it would be if there were a specific table tailored for each content type. This is a fair trade-off in my opinion, but there is also a way around this complexity. It involves creating another layer of abstraction on top of these models: content item objects. These objects extend a base abstract class that handles all the interaction with the Page and ContentNode models, giving you truly object-oriented data access.

Using the Abstract CMS Content_Item Class

The abstract content item class serves as the base for all the content items that you will create. It adds all the standard functions that the content items need; for example, the methods for loading content items, saving changes, fetching related items, and so on.

To get started, create a new folder in the library/CMS folder named Content and then another folder in Content named Item. Then create a new file in this folder named Abstract.php.

Creating the Base Class

Create a new class in this file named CMS_Content_Item_Abstract. Since this class wraps a standard page, you need to add properties for each of the pages table's columns to it. Make all the fields except namespace public. The namespace field will be set on an item level and should be protected so only child classes can change it. Also, add a protected property for the Page model, which will be loaded in the constructor, so it needs to get loaded only once.

Once you set these up, add the constructor. This should create a new instance of the Page model and set the pageModel property. Then it should load the page if a page ID has been passed to it. Use the loadPageObject() method, which you will create shortly (Listing 5-13).

Example 5.13. The Base CMS_Content_Item_Abstract Class in library/CMS/Content/Item/Abstract.php

<?php
abstract class CMS_Content_Item_Abstract
{
    public $id;
    public $name;
    public $parent_id = 0;
protected $_namespace = 'page';
    protected $_pageModel;

    public function __construct($pageId = null)
    {
        $this->_pageModel = new Page();
        if(null != $pageId) {
            $this->loadPageObject(intval($pageId));
        }
    }
}
?>

Loading Pages

You now need to set up the method to load the content items. This method will fetch the current item's (that is, the ID that you passed it) row in the database. If it finds the row, then it needs to validate that the row's namespace field matches the content item's. Then it loads the base properties, which are the properties that you set in this abstract class and are stored in the pages table. Next, the load method needs to fetch the content nodes and attempt to load each of these into class properties. It will do this using a _callSetterMethod() method, which will call a _set method for each node if it exists. This is done so you can manipulate the data that the content item uses.

You will need three additional functions for the loadPageObject() method. It would be possible to simply add this logic to the loadPageObject(), but that undermines the goal of creating reusable code and makes the code less readable. These are the methods you need to create:

  • getInnerRow(): This method will fetch the row that the content item relates to from the pages table.

  • _getProperties(): This method will return an array of the properties of the content item.

  • _callSetterMethod(): This method will attempt to call a setter method for the value you pass it.

The getInnerRow() method is very straightforward; it simply wraps the Zend_Db_Table find() method and sets the content item's inner row, which is the underlying data set (Listing 5-14).

Example 5.14. The getInnerRow() Method in library/CMS/Content/Item/Abstract.php

protected function _getInnerRow ($id = null)
{
    if ($id == null) {
        $id = $this->id;
    }
    return $this->_pageModel->find($id)->current();
}

The _getProperties()method will utilize several global PHP methods for inspecting classes, including the get_class() method, which returns the class name of an object, and the get_class_vars() method, which returns an array of the properties for a class (Listing 5-15).

Example 5.15. The _getProperties() Method in library/CMS/Content/Item/Abstract.php

protected function _getProperties()
{
    $propertyArray = array();
    $class = new Zend_Reflection_Class($this);
    $properties = $class->getProperties();
    foreach ($properties as $property) {
        if ($property->isPublic()) {
            $propertyArray[] = $property->getName();
        }
    }
    return $propertyArray;
}

The _callSetterMethod() will be a little more complex. You first need to establish the naming convention for the setter methods; in this case, I chose to prepend _set to the camelCased content node name, so my_value will get set with _setMyValue, for example. Then you need to check whether the method exists. If it does, you pass it the data set, and if not, you return a message to the calling method. Note that it is considered a best practice to use class constants for any of these messages, so you will need to add a NO_SETTER constant to the head of the class (Listing 5-16 and Listing 5-17).

Example 5.16. Setting the NO_SETTER Constant in library/CMS/Content/Item/Abstract.php

const NO_SETTER = 'setter method does not exist';

Example 5.17. The _callSetterMethod() in library/CMS/Content/Item/Abstract.php

protected function _callSetterMethod ($property, $data)
{
    //create the method name
    $method = Zend_Filter::filterStatic($property, 'Word_UnderscoreToCamelCase'),
    $methodName = '_set' . $method;
    if (method_exists($this, $methodName)) {
        return $this->$methodName($data);
    } else {
        return self::NO_SETTER;
    }
}

Now you have the base methods in place and can load your content items (Listing 5-18).

Example 5.18. The loadPageObject() Method in library/CMS/Content/Item/Abstract.php

public function loadPageObject($id)
{
    $this->id = $id;
    $row = $this->getInnerRow();
    if($row) {
if($row->namespace != $this->_namespace) {
            throw new Zend_Exception('Unable to cast page type:' .
                $row->namespace . ' to type:' . $this->_namespace);
        }
        $this->name = $row->name;
        $this->parent_id = $row->parent_id;
        $contentNode = new Model_ContentNode();
        $nodes = $row->findDependentRowset($contentNode);
        if($nodes) {
            $properties = $this->_getProperties();
            foreach ($nodes as $node) {
                $key = $node['node'];
                if(in_array($key, $properties)) {
                    // try to call the setter method
                    $value = $this->_callSetterMethod($key, $nodes);
                    if($value === self::NO_SETTER) {
                        $value = $node['content'];
                    }
                    $this->$key = $value;
                }
            }
        }
    } else {
        throw new Zend_Exception("Unable to load content item");
    }
}

Using Utility Methods

Next you need to create the utility methods. These are the methods that will make your life easier when you are working with the items, and you will likely add to them.

Initially, you will need to create a toArray() method. The toArray() method will first get the item's properties. Then it will go through these properties, building an array of the values of the public properties (Listing 5-19).

Example 5.19. The toArray() Method in library/CMS/Content/Item/Abstract.php

public function toArray()
{
    $properties = $this->_getProperties();
    foreach ($properties as $property) {
            $array[$property] = $this->$property;
    }
    return $array;
}

Manipulating Data

Now that you have the methods in place for loading and working with the content items, you are ready to create the methods to manipulate the underlying data. You need to create a method to insert a new row, update an existing row, and delete a row.

The insert and updated methods will be consolidated into a save() method for convenience. By doing this, you will be able to create a new instance of the content item, set the values, and then call the save() method, much in the same way that Zend_Db_Table_Row works. The save() method will determine whether the current item is a new item (by checking to see whether the ID is set) and then call the protected _insert() or _update() method appropriately (Listing 5-20).

Example 5.20. The save() Method in library/CMS/Content/Item/Abstract.php

public function save()
{
    if(isset($this->id)) {
        $this->_update();
    } else {
        $this->_insert();
    }
}

The _insert() method will call the Page model's createPage() method. Then it will set the current item's ID and call the _update() method (Listing 5-21).

Example 5.21. The _insert() Method in library/CMS/Content/Item/Abstract.php

protected function _insert()
{
    $pageId = $this->_pageModel->createPage(
        $this->name, $this->_namespace, $this->parent_id);
    $this->id = $pageId;
    $this->_update();
}

The _update() method will call the item's toArray() method and then pass this to the Page model's updatePage() method (Listing 5-22).

Example 5.22. The_update() Method in library/CMS/Content/Item/Abstract.php

protected function _update()
{
    $data = $this->toArray();
    $this->_pageModel->updatePage($this->id, $data);
}

Finally, the delete() method will validate that the current item is an existing row in the database (through the presence of the id field) and call the Page model's deletePage() method if it is (Listing 5-23).

Note

The page model will delete the related content nodes, since you turned cascading deletes on in the page model class.

Example 5.23. The _delete() Method in library/CMS/Content/Item/Abstract.php

public function delete()
{
    if(isset($this->id)) {
        $this->_pageModel->deletePage($this->id);
    } else {
        throw new Zend_Exception('Unable to delete item; the item is empty!'),
    }
}

Extending the Base Content Item Class

Now that you have this base content item class, you can create new forms of content for your CMS project very easily, without altering the underlying model or database. You simply create a new class that extends CMS_Content_Item_Abstract and add public properties for each of the data.

For example, say you are creating a module for a tour operator to display their trips. A trip would probably have fields for the title, short description, content, date, length, and cost. You also need to set the namespace, which is how the CMS differentiates between the different page types. So, your content item would look like the code in Listing 5-24.

Example 5.24. An Example Content Item for Trips

<?php
class Trip extends CMS_Content_Item_Abstract
{
    public $title;
    public $short_description;
    public $content;
    public $date;
    public $length;
    public $cost;
    protected $_namespace = 'trip';
}
?>

Then to create a new trip, you simply create a new instance of the trip, set each of the properties as necessary, and call the save() method (Listing 5-25).

Example 5.25. Creating a New Example Trip

$trip = new Trip();
$trip->title = "Long Range Tuna Fishing";
$trip->short_description = "This trip will ...";
$trip->content = "More in depth content ...";
$trip->date = "September 15, 2009";
$trip->length = "15 Days";
$trip->cost = "$2,995";
$trip->save();

As you can see, the CMS_Content_Item_Abstract class makes working with this data structure totally transparent.

Summary

In this chapter, you reviewed different patterns for CMS data. You then implemented the node pattern. With the database tables complete, you set up the Zend_Db_Table models. Finally, you learned how to create content item objects that abstracts all of this, giving the end developer a very simple interface to the CMS data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset