No announcement yet.

Programming your own CMS Content Type.


  • Edwin Brown
    started a blog post Programming your own CMS Content Type.

    Programming your own CMS Content Type.

    As several of you know, I've been promising to blog on how to write your own CMS Content type. The ability to do that was one of the requirements for the new system, and it's real. For a number of reasons it's available now.

    I've attached a zip file with the code and a Word file that includes everything from here down. You can download that and skip everything from here down.

    We on the vBulletin team are proud of the changes coming in version 4.0. One of the things we’ve promised is that the changes in version 4.0 are just the beginning. We’ve implemented a new framework, almost from the ground up, and in doing so we’ve made a lot of things possible. I’ve previously blogged about creating your own widgets, which is actually pretty easy if you know PHP code. Creating a new content type is the next step in capability. I’ve seen a number of questions about this, and I’ve promised to provide some direction as to how to create your own new content type. So here it is.

    Let me make a few introductory comments. If you aren’t a reasonably fluent php programmer you aren’t likely to get much out of this. If you don’t understand the vB phrasing and templating system then you should study that first.

    I also need to point out that I am not the author of the new routing/controller framework. Nothing is ever done by just one person, but the primary author was Darren Gordon. I’ve learned to get things done, often efficiently, using the new system. Every time I talk to him I learn something new, and he makes it look easy. If you like the way the CMS system works, he deserves the credit. If you don’t, it’s probably because I didn’t explain it well.

    This is not production-quality code. It’s something I knocked off over a few weekends so I could have an example of a written-from-scratch content type. This is not a supported product. It’s not even an unsupported product. It’s just some code you are welcome to use at your own risk.
    Nobody but me has looked at it. I’m sure it’s got some bugs in it that I haven’t found yet. Personally, if I wanted to put it on a website I managed, I would do so with caution. You are welcome to do whatever you want with it, except (1) complain if it breaks, or (2) claim it’s your own. Actually I would appreciate some feedback from anyone who tests it. Please don’t throw it onto a live site without some serious testing.

    As you go through this document I’m assuming you’ll have the source code open. Because I’m still testing and tweaking the source code, I’m not going to give line numbers

    To upload:
    • ·Unpack the file You can remove the doc file, which you are reading now.
    • ·Upload the rest to the file where you installed vBulletin. That should create /packages/contrib./. If it didn’t, you uploaded to the wrong location.
    • ·Install the package, which is in /includes/xml/product-contrib.xml

    At the time I am writing this, this type exercises parts of the content controller that haven’t been exercised before. As a result I have had to make changes that are not currently in the release code (beta 5). I am sure that the next release will have these updates, so if you are using code later than that, DON’T UPLOAD THIS FILE: /packages/vbcms/content/controller.php. If you are using beta 5 or before, you need to upload it.

    I wanted to create a new content type that was complex enough to be interesting and likely to be useful for at least some users. I believe probably the most-requested addition to the current vB 4.0 CMS system has been the ability for users to create their own user-defined types. So that’s what I chose for this example. Specifically this means:
    • ·The admin can define a new content type, selecting some number of fields of different types. For each of these fields the admin sets the caption to be displayed. If the field is a list, the admin defines what options are on the list. All of this is defined as a content type. The admin can also specify that some of these fields are editable by the end user.
    • ·The author can create a new content node based on an existing type, and fills in the data for the fields specified by the admin.
    • ·The end-user sees the data, and can edit fields if the type allows that and they are logged in.
    • ·The new node appears on the standard CMS navigation screen.

    What’s missing
    This is NOT a product that meets vB standards for release. Some of the things that are missing include:
    • ·The text editing fields are just HTML textareas. They should use the advanced editor.
    • · The picture fields are just text input boxes. You have to put an image on the web somewhere, know the URL, and type it in. Ugly.
    • · The fields are not searchable.
    • · For user editable fields there is no history. There should be.
    • · The code does not allow for tagging.

    Before too long we will release a supported user-defined content type. Several, actually. That’s Don Kuramura’s field, and he has posted a future development plan elsewhere. I expect they will share some of the code you see here. My goal here is to show you enough so you know what the unique challenges and requirements are and at least one way to solve them. Also, expect me to be continuing to test for the next week or two, and if I find significant bugs (I expect to) I’ll post updates.

    Routers, Controllers, and Actions
    Which are, of course, the heart of the new Content Management scheme. To give the simplest overview:
    • ·Routers deal with requests. They parse and validate the URL’s given by the user, hand the tasks off to the controller, and generate new URL’s for links. This ensures that the URL will be correct regardless of the friendly-url settings. Currently there are three- list, widget, and content. (There is also an editor router, but it’s not currently used.)
    • ·Controllers take the parameters from the router and generate the page. There will generally be one controller per router. That’s not essential but keeps things more understandable.
    • ·Actions define what the router and controller do. The Content router has ten:'View','EditContent','EditPage', 'AddNode', 'DeleteNode', ConfigContent', 'PublishNode', 'UnPublishNode', 'NodeOptions', and 'List'. Not all of these are currently in use.

    An example of why you would create your own router and controller is the “list” router/controller. We wanted to be able to list (1) Everything in a section, (2) Everything in a category, or (3) everything written by an author. This is substantially different from the content router/controller, which is based around the concept of a specific content node. The list controller doesn’t know, for example, how to create or edit content. That would be inappropriate.

    It’s possible you may have a need to create your own content controller/router. That is beyond the scope of this document. I would encourage you to first create a simple content type, and then take some time and understand the list router and controller.

    Getting Links
    You don’t want to hard-code any CMS links in your code and templates. Ever. I know. I did it at first and had to re-do them all. There are getUrl()(static) and getCurrentUrl() methods in the router. Use them. There are plenty of examples in the code.

    Minimum for a new Content-based Content

    Database Entries
    It’s probably possible to create a new content type without creating a new database table, but most of the time you will want to. That’s your responsibility. Please prefix your tables so they are less likely to collide with somebody else’s tables.

    Class and Package
    You need to create a new package, something descriptive enough that it isn’t going to collide with someone else. A record goes in the package table. You then define one or more content types contained in that package. You need to create a record for each type in the contenttype table. There are a series of flags in that table that you need to set properly:
    • ·canplace- you can create it inside the CMS using “addnew”. This needs to be ‘1' for you.
    • ·cansearch- is this a searchable type? If you set this to 1 then you have some extra work to do. You need to create search indexer, type, searchcontroller, and result classes, and create at least a results template. I’m not explaining that here, but if you want to make your type searchable go to packages/vbcms/search to see what these classes look like.
    • ·cantag- set to 0 unless you want to do more work.
    • ·canattach- if you set this then you can use the attachment manager, and you have more work to do. I recommend set to 0.
    • ·isaggregator: Set to zero, so it will show in the content manager and on the dropdown list of creatable types in CMS.
    • You will need to create a set of directories and files, which at a minimum look like this
    Item Content Class
    The first thing you will want to do is create your item content. This controls the selection of data to create your content type. We’ll go into each of these classes in detail later

    Content Class
    This is the workhorse, and is where you’ll do most of your programming. You need to respond to whichever of the actions you plan to implement. The minimum methods are
    • ·getAddNewView- creates a new content node of your type.
    • ·createDefaultContent- creates a new empty node
    • ·getInlineEditBodyView- to edit your type
    • ·populateViewContent- populates the view template for the preview and view actions. “Preview” is called to populate the section list, and “view” is the end-user display of this item.

    Within these classes you of course want to define other helper methods, and there are additional interface methods that you might decide to use. For example, if your content type needs per-item configuration (section and article don’t) then you will have to create a link to the configcontent router action, and create a getConfigView method to display the configuration interface.

    Data Manager Class
    We’ve done a lot to try to make it easy to save your data. If you have a standard one-record-per page database table, then all you have to do is to define the fields and create the methods that define the “where” clause for an update. If your database layout is more complex, then you have to handle that in your code.

    Templates and Phrases
    You can get creative in your template names, but we encourage you to avoid doing so. By default the system will assume these templates are available:
    • ·_content__inline- for editing
    • ·_content__view- for end-user display
    • ·_content__config- for configuration, if your type needs it
    • ·_content__preview- for end-user display

    Please use these names. If you don’t you’ll make more work for yourself and possibly create weird side-effects.

    Of course you will need to create whatever phrases you need.

    Navigation and Widgets
    CMS is based around nodes. The node table uses a modified-preorder sort algorithm to manage a hierarchical page system. If you haven’t worked with one of these before, here is a reference:

    If you use the content router & controller, which I HIGHLY recommend, you don’t need to understand any of this. You just need to know that when you create a new your-content-type, the system will create a new node and store your type id. Then when it needs to display your type it will pass that id back to you and expect you to do the rendering.

    That may not be enough for navigation purposes. For example, I PM’d with one person who had his own database with thousands of his own records, which he wanted to display inside CMS. Now there are two different ways to do that. He could create one node for each of his content items, with whatever sections and categories he want, and let the CMS navigation handle it.

    Or he could create his own content type and essentially put all his content in that one node. In that case the navigation widgets we provide would of course know nothing about his content. It just sees one node. There are two ways to handle selecting the specific content the viewer wants.
    ·Without a widget:. In our navigation widgets there would be a section called “Joe’s Special Content”, and inside that section would be one preview page. The preview page is of course whatever he creates as a preview. Since that can get rendered with other node content on a list, it shouldn’t be too long. There would need to be some navigation either on the Preview page or in the View page.
    ·With a widget: I personally think it would usually be better to create a separate navigation widget that defines the new content type. I’ve already blogged about how to do that, so I won’t repeat it here. The widget would be placed on the layout for whatever sections the admin wants to allow navigation to that new content type, and the new type-specific widget is responsible for providing what selections are appropriate.


    Approach and Database
    Speaking in terms of database architecture there is an interesting challenge. Normally when we create a PHP application we know what database structure we are using, but here the whole point is to create a flexible structure. I know three ways to handle these requirements.
    ·Create a database layer in PHP- have some tables that allow fundamental types to be created, and create the record-level constructs entirely in PHP. That would have the advantage of compact database, but requires a lot of programming. That doesn’t seem appropriate her.
    ·Let the user define the data structure, then create the necessary tables in PHP. I actually like that approach. If I thought that the end-user would create a small number of types and then reuse them I would have taken this approach. It has the disadvantage of clumsiness in editing. If you have defined a type with three text fields and four number fields and you decide you want four text fields and only two number fields, it’s a bit complex to change.
    ·Create a fixed table with as many fields as you might want, and just ignore the fields you don’t need. That is the simplest method and the easiest to program. It has the disadvantage of some inefficiency. The tables will be larger than necessary, so consequently data transfers will be somewhat slower. I felt that the inefficiency in operation was balanced by the ease in programming and the flexibility of altering types and creating new ones. So that’s the choice I made here. If you would prefer a different method, feel free to borrow anything you find useful here.
    To store the data I needed four new tables:
    • ·cont_usertypedef: Stores the type definition record
    • ·cont_usertypedetail: Stores the captions for the various fields
    • ·cont_usertypelist: Stores the captions for dropdown lists
    • ·cont_usertype: Stores the individual type detail records

    If I were starting again, there are a few things I would do differently. For example, in composing the sql for the data fields I do a bit more in the php than I should. If I passed all the list entries as an array, for example, I would give the template editor a bit more freedom.

    User Interface
    From the main CMS page, when the users wants to create a new content node, our content type is instantiated and the getAddNewView method is called. In that case I want the user to be placed in the edit view with a type specified. The user can either keep this type, select a different previously-defined type, or (if they have the rights) create a new type or edit an existing type.
    If the user is creating or editing a type, then I want them to first select the number fields of each type, then save. The screen will refresh with input elements to enter the caption for each field. If the user has specified a list field, then we need to prompt them for the number of list options. Then they save, and this time when the screen refreshes there are the appropriate number of list item entries. The user keeps saving until the type definition is what they want. Note that the template to be used for rendering must have a default but allow override. I would expect many admins to want to use different rendering for different content types.

    The edit and view pages are created based on the type definition. Not much to say

    The CMS permissions structure has five permissions. These are set at the top level and optionally at any subsection. When set for a section, the five permissions are set on or of for each usergroup. The permissions are :
    • canView
    • canCreate: Can create new content, and includes canView
    • canEdit: can edit other’s content, and includes canCreate and canView
    • canPublish: can set content to published, and includes canEdit and below.
    • canUseHTML: With this set the user can insert HTML in the wysiwyg editor.

    I chose to let only those with canPublish rights to create a new content type definition. Those with canCreate and canEdit can only use existing types.

    Class Structure
    I chose to make a new package “contrib”, and the class “usertype” The file structure is as defined above, except I needed one new file. Since I store both type definition data and individual record data, I found it convenient to make two different data managers for these two types of data. Therefore you will see

    Item Content
    The item content class is used to load the content from the database. Unless you add some options of your own, it can be called with various options. The standard ones are defined in the array
    protected $query_info
    I recommend you use these. You will virtually always need to respond to the three shown here, but you can let the parent class handle INFO_PARENT.

    You need to declare all the fields your type uses, and declare a private variable to match each field name. The parent classes will set these fields based on the values returned in your queries. If your query returns a field with a name that isn’t set both as a class member and in the array, either here or in the parent classes, then you’ll get an error.

    The getLoadQuery is the core of this class. You need to define SQL that will return the appropriate fields for each of the actions defined in $query_info.

    You can write individual setters and getters for each field. Because there are so many, I prefer to write a single getField method.You also need a getContentTypeId method. I added getExistingTypes because I felt this is a logical place to put it. You can put any such helper functions that get data from the database here if you like. There are already getters for all the non-type-specific data. So, for example, if you want to get the nodeid of the current page you call (usually you are calling from the Content class) $this->content->getNodeId(); Look in /packages/vbcms/item/content.php to see the generic get methods.

    Data Manager

    Here I have two data managers. That’s because I have to save two different types of data- . configuration and content. Most classes have only one.

    This is the class you would normally create. If you have content in one table then it’s pretty easy. Populate the $type_fields array with the list of fields you want to be auto-saved. You can put a value in the’required’ field and auto-validation. Look in /vb/dm.php for the values recognized. I don’t usually set these, but if your content type would benefit from field checking it’s there.

    You probably don’t need to create any methods. If you set the properties properly, the parent classes will handle the saves.

    This is more complex than the Usertype DM. Most content types don’t need this, but because of the ability to completely configure this content type something extra is needed. The data manager is designed to work with a one-record-per-node data definition, but in our case we have two-tier heirarchy of records to handle. I let the standard DM methods handle saving the updates to cont_usertypedef, but I handle the cont_usertypedetail and cont_usertypelist records myself. I could have created dm classes for those, but I’m pretty sure it would have been as much code as what I selected, and slower.

    First we have the field definitions, much like the Usertype data manager. Then the five methods to handle passing records to be modified. (Note that until runtime we know very little about what we are saving.) I had to write my own save() function, and the updateListItems() to handle the select list details for list field types. I won’t take time to go through these in detail. If you’ve done much work with SQL data you’ll recognize the “populate the sql query and execute” style.

    Collection Content
    The collection content item is required when you are creating a list. The current use for this is the section list.First you populate the query_info array, which will normally be exactly the same as this. Create the two methods shown. In getContentQueryFields you define the list of fields to be pulled (don’t forget the alias name) and getContentQueryJoins returns the sql for joining in your type-specific table(s). That should be all you need to do.

    The Content Class
    This is the meat of your content type. We’ll go through the main methods and how they work. I won’t go through the helper functions in detail, but in summary:

    • ·getExistingTypes: This creates the select members for the list of existing types
    • ·
    • getConfig: pulls the configuration data from the database and puts into an associative array. We cache the data, so this approach reduces the database queries to one very light one.
    • ·makeOptions: given a field like, like “list”, it creates the array of radio button sql that will be displayed.
    • ·This composes the list of field names. For example, if the configuration says there are three integer fields, it returns an array of (int1, int2, int3). It’s useful in handling the variable field names.
    • ·getListOptions: called by the getConfig method, this pulls the list captions, sorts them, and returns them to be added to the config array.
    • ·getListEdit: creates the edit interface for a list field.
    • ·getListConfig: creates the configuration interface for a list field.
    • ·getInputs: Called to create all the configuration interfaces
    • ·getEditInput: creates the edit interface elements for one field
    • ·getEdits: called to create all the edit interfaces
    • ·saveData: Saves the data on an edit. If you haven’t used vB::$vbulletin->input->clean_array_gpc before, this is our standard way of validating user input. Normally we would just set the fields in the data manager and call “save”, but in this content type we have a bit of extra work to do. In the foreach(vB::$vbulletin->GPC['field_names'] as $fieldname) loop we check each field to see if it has changed. If the user has reduced the number of fields of a type (say there were four text fields and now there are only four) we need to delete the unnecessary fields. This is done by calling the usertypedef data manager addDeleteFieldDefs(). New fields are added with addFieldDefs. List elements are handled by similarly passing $update_listitems, $new_listitems, or $delete_listitems arrays.Also, we allow the type creator to define fields that can be edited by end user. This means we have to do extra field-level validation and unset fields that the user can't save.
    • ·saveConfig: Save configuration data. Notice that because we store date fields as integers, unix-style, we need to convert them when saving You will notice we check $this->content->canEdit() to see whether the user can save data.

    The Code

    Coding AddNew Action
    This is one of the standard methods always need to provide. This creates a new data record, creates the desired view, and returns the view to the controller. Normally you would return an edit view, but in this case the first order of business is to configure the content so we return a configuration view. You will note that we call vBCMS_Permissions::canCreate($parentnode) to verify our permissions. Once the node has been created we can call $this->canCreate().

    Coding CreateDefaultContent

    The only thing we really need is a parent node, which defines where in the hierarchy the new node belongs. We also need a content type, but we already have that. There is one small thing to watch out for here. The standard data manager will not store a table record unless there is at least one field set. Therefore we set the typeid to one. That gives us the single field necessary to make the data manager store the record. Since the user will immediately see the configuration interface, they can change it if they like.

    Coding ConfigView Action

    This is handled by the getConfigView method. Now we’re getting into some meat. This method creates the configuration interface. Let’s talk about what we’re going to do. To render a view in the new system, we:
    • ·Create the view with “new ”. I just use vB_View. You pass it the name of the template you’re going to use.
    • ·Make sure any phrasegroups you need are available. If you haven’t worked with this- quick introduction. vBulletin is multi-lingual. We NEVER put text that will be displayed to a user in the php code or a template. We create phrases, which are divided into phrase
    • groups. These map a phrase to the currently active language, so someone who communicates in a new language has the opportunity to plan.
    • ·Pass whatever variables the template needs. I have tended more and more to pass any variable that an admin might possibly want. Unused variables are ignored, and there’s very little overhead.
    • ·Return the view. If you’ve worked with vBulletin 3.X and earlier- don’t render it- later the main page controller will render the page. Rendering it yourself is less efficient and adds nothing.

    You can’t make any assumption about what phrases are available. You need to load anything you’re going to need. So we do that first, with the fetch_phrase_group calls.

    We call $this->loadContent() to make sure we’ve got the data we need.
    Call getConfig() to get the stored configuration for the current content type.
    See if we need to save.
    Verify the permissions.

    Now we call new vB_view to create our view.
    The parameters we need to pass are:
    • ·The SELECT list of existing types
    • ·url’s for the view, configure, and edit pages
    • ·The select lists for each of the variable types. These let us select how many of each field type we have on our edit screen.
    • ·The edit bar. This is a standard element we use on all edit pages, and it’s a simple call.
    • ·can_publish. We need this because if the user does NOT have publish rights, they cannot create a new content type. That means we want to turn part of the template off.
    • ·can_edit. This tells the template whether to show the “edit pencil”

    See the lines
    $segments = array('node' => $this->content->getUrlSegment(),
    'action' => vB_Router::getUserAction('vBCms_Controller_Content', 'ConfigContent'));
    // Add URL to submit to
    $view->submit_url = vBCms_Route_Content::getURL($segments);
    This is how we compose an URL in CMS. Another way would be to use $this->content->getUrlSegment(). If you were on a page with node id 100 and the url “my-page”, then getUrlSegment() will return 100/my-page. If you want the edit page, you can call
    $url_segment = $this->content->getUrlSegment() . ‘/edit’;
    vB_Route::create('vBCms_Route_List', $url_segment)->getCurrentURL();

    Coding Edit Action

    This is handled by the getInlineEditBodyView method. Everything I said about the getConfigView method still applies. You’ll the first half of the method is almost exactly the same as the last one. In addition to the editbar, we also need to create the publisher and metadata editor. The editbar goes across the top of the edit region and the publisher and metadata editor go on the right. Everything you create for your type will be on the left two-thirds of the screen below the edit bar. So for the edit template you will always include these variables with the appropriate css. The easiest way to do this is to copy an existing edit template. There are only a few variables passed beyond the url’s and these three edit regions. I count existing_types, title, url, and type_field_edits. The last is an array with all the type-specific edit data. We reviewed the code for that earlier, in the getEdits method.

    Coding View Action

    Normally you don’t even need to write this method. If you don’t have one, the controller will try to instantiate a template called _content__view. Assuming there is one of these, it passes it to populateViewContent. Assuming you have one of those.

    For my user-defined type it is entirely likely that someone will want to create different templates. So I needed to write my own method. You’ll see that all it does is get the type-specific configuration, decide what template to use, create the view, and call populateViewContent.

    Coding PopulateViewContent
    Somewhere I need to check to see if I have content to update or delete. I like to put that check here. So walking through this method.
    First I make sure I have the type-specific configuration. Without that I’m dead.
    Check to see if I should save. Note that if I delete a record I need to redirect to the parent node. Nothing left here to display.
    Call the parent function for any setup.
    Generate the URL
    Walk the array of fields, calling the appropriate method for each, to build the array of captions and display or edit values.
    There are about a dozen miscellaneous variables we pass to the template. Pretty much just a matter of calling the appropriate function for each.

    • Edwin Brown
      Edwin Brown commented
      Editing a comment
      Sorry for the delay. I just saw your posts.

      As far as the button- the first incarnation of that toolbar had that as a dropdown list with a "go" button, but 4.00 only had one content type. So the graphics people turned it into a button, which does look better. It's a very simple template fix in vbcms_content_edit_editbar. I've got agreement that it will become a dropdown list again, probably in 4.02. If you need it before then, PM me and I'll send you a revised template. I've got it around somewhere. I'd highly recommend against adding a widget, because we're going to be adding types and we expect others. Better to have that all in one place. Also remember the widget would be visible to users who don't have create rights.

      As far as the code base. I expect/hope to do some rewrite, but as long as you write within the framework there should be zero impact. We wrote this to be extensible, and that would be a wasted effort if we change the interfaces. For example, in the last couple weeks I completely rewrote the data access to cache results & minimize data queries, but I did it so that the content/item content/dm interfaces were unchanged. I had one call to invalidate cache on saving, and I'm moving that back out to the model. When I've done that all my content, item, and node classes won't have changed, for any of the three content types I've written. We might add some interface hooks- I hope we do- but not invalidate existing ones.

    • DirtyHarry
      DirtyHarry commented
      Editing a comment
      Thanks Edwin, for the time being I do not need the template.

    • Redseal
      Redseal commented
      Editing a comment
      can you import and export content types or do they have to be rewritten every time? If not, maybe this should be an option on .org
    Posting comments is disabled.

Related Topics