Apostrophe 1.5: legacy documentation

Still using Apostrophe 1.5? Check out Apostrophe 2 for your future projects.

Developers Guide

Up to Overview

Developer's Guide

At this point you should already be familiar with the preceding material, which covers Apostrophe installation, the end-user experience of managing an Apostrophe site, and front end design issues. The designer's guide in particular is required reading to make good use of the following section, which is devoted to extending Apostrophe in new ways with your own code.

We strongly recommend that you complete the  Symfony tutorial before attempting to add new features to Apostrophe. We'll assume familiarity with Symfony development in this section.

Overriding Our JavaScript Files

Apostrophe ships with jQuery. It is not uncommon to want to override the version of jQuery used by Apostrophe (although you do need at least the 1.4.x series of jQuery to be compatible with our own UI code).

Beginning with Apostrophe 1.5 you can override this in a very flexible way via app.yml settings as shown here:

      # get jquery from somewhere else
      jquery: /js/myjquery.js
      # a.js contains most of our UI logic
      main: true
      # aControls.js contains necessary progressive enhancement controls
      controls: false
      # The json2 library might be elsewhere or unnecessary if you don't allow older browsers
      json2: false
      # jquery autogrow plugin
      jquery-autogrow: true
      # jquery hover plugin
      jquery-hover-intent: false
      # jquery ui, somewhere else
      jquery-ui: /js/myjqueryui.js
      # Tag typeahead widget from sfDoctrineActAsTaggablePlugin
      tagahead: true

Creating Custom Slot Types

You are not limited to the slot types provided with apostrophePlugin! Anyone can create new slot types by taking advantage of normal Symfony features: modules, components, actions, templates and Doctrine model classes.

You can speed this process enormously by using the apostrophe:generate-slot-type task:

    ./symfony apostrophe:generate-slot-type --application=frontend --type=mynewtypename

This task generates all of the scaffolding for a new, working slot type. The above example generates the necessary module, model class and form class in the frontend application of the project. You can also generate the scaffolding in an existing or new Symfony plugin:

    ./symfony apostrophe:generate-slot-type --plugin=mynewplugin --type=mynewtypename

We recommend the latter as it is easier to reuse your slot type in another project this way. Don't put your slot in apostrophePlugin itself. Make a plugin of your own, or add the slot at the project level. This way you won't have problems later when you update apostrophePlugin.

Reminder: to activate your slot you must add it to the list of allowed slot types for your project in app.yml, and also include it in the allowed_types option for individual Apostrophe areas in which you want to allow editors to add it. In app.yml, you add a new slot type like this:

      mynewtypename: "Nice Label For Add Slot Menu"

When inserting an area in a template or layout, you specify the allowed slot types like this:

a_area('body', array('allowed_types' => array('aRichText', 'mynewtypename')));

You can also use your new slot type in standalone slots with the a_slot helper.

Of course, to understand how to customize the behavior of your new slot type, you need to know how slot types work. So let's dig into the workings of the aFeed slot, which was generated with the apostrophe:generate-slot-type task and then edited to implement its own features.

1) A module. In this case, the aFeedSlot module. This contains actions, components and partials to edit and render the slot within a page. If the module lives in a plugin, don't forget to enable the plugin in your ProjectConfiguration class and the module in your settings.yml file.

2) A form class, such as aFeedForm.

3) A model class, such as aFeedSlot, which inherits from the aSlot model class via Doctrine's column aggregation inheritance feature. (Yes, we set this up for you when you use the apostrophe:generate-slot-type task.)

Slot Modules, Part I: Edit and Normal Views

With a few notable exceptions like our aImage slot, most slots have an "edit view" and a "normal view," rendered by editView and normalView components in the slot's module. The normal view presents the slot as a user will see it when not editing. The edit view displays the same slot as the user will see it after clicking the "Edit" button. For better editing performance, both views are present in the HTML whenever the logged-in user has sufficient privileges to edit the slot.

A simple _editView partial just echoes the form class associated with the slot:

<?php echo $form ?>

Of course, you can render the form differently if you wish.

A slot type generated with the apostrophe:generate-slot-type task will already contain the necessary supporting code in the editView component, in modules/aFeedSlot/actions/components.class.php:

    public function executeEditView()
      // Must be at the start of both view components
      // Careful, don't clobber a form object provided to us with validation errors
      // from an earlier pass
      if (!isset($this->form))
        $this->form = new aFeedForm($this->id, $this->slot->getArrayValue());

Notice that the form is initialized with two parameters: a unique identifier that distinguishes it from other forms in the page, and a value fetched from the slot. Most slots store their data in the value column of the slot table, using serialize and unserialize to store PHP data in any way they see fit. The aSlot::getArrayValue and aSlot::setArrayValue methods are conveniences that simplify this for you. aSlot::getArrayValue always returns a valid array, even if the slot is new (in which case the array will be empty). And aSlot::setArrayValue accepts an array and serializes it into the value column. You don't need to worry about any of that... although it is possible for you to implement custom database columns instead of using Doctrine column aggregation inheritance as explained below. This is usually not worth the trouble and the database schema changes it causes. However it can be worthwhile if you need foreign key relationships and must avoid extra queries. You can also approach that problem by referencing the id column of the a_slot table from a foreign key in a related table, rather than the other way around.

The normal view takes advantage of the information stored in the slot to render it with its normal, non-editing appearance. The feed slot's normal view component fetches the feed, via the web or via an sfFileCache object if it has already been fetched recently, and the normal view partial renders it.

Your normal view's implementation is up to you. Try the slot type generator task to see a very simple working example. One thing your normal view must do is provide an edit button at the top of the partial, or provide some other way to edit the slot's settings. The task generates code like this in the _normalView partial:

    <?php include_partial('a/simpleEditWithVariants', array('name' => $name, 'permid' => $permid, 'pageid' => $pageid, 'slot' => $slot)) ?>

This code displays the edit button, and also offers a menu of slot variants if any have been configured for this slot type on this particular site.

Variants and Custom Slots

Using variants with custom slots is trivially easy. Once you have generated your new slot type (beginning with Apostrophe 1.5 you'll get the simpleEditWithVariants partial by default, for older releases modify the normalView to read as shown above), you can define variants as previously discussed in the Designer's Guide. The name of the current variant becomes a CSS class on the slot, which is often all you need. In addition, you can set options for each variant in app.yml, and these options are passed to the slot exactly as if they had been specified in the template. So you can easily alter the behavior of your slot at the PHP level as well as the CSS/JS level.

Slot Forms

Most slots, specifically slots that have an edit button and take advantage of the edit view component, will have a form associated with them. By default this form is automatically echoed by the edit view component, fully initialized and complete with any validation errors from unsuccessful edits.

Here's the aFeedForm class:

    class aFeedForm extends sfForm
      // Ensures unique IDs throughout the page
      protected $id;
      public function __construct($id, $defaults)
        $this->id = $id;
      public function configure()
        $this->setWidgets(array('url' => new sfWidgetFormInputText(array('label' => 'RSS Feed URL'))));
        $this->setValidators(array('url' => new sfValidatorUrl(array('required' => true, 'max_length' => 1024))));
        // Ensures unique IDs throughout the page
        $this->widgetSchema->setNameFormat('slotform-' . $this->id . '[%s]');

Notice that this form class is not a Doctrine form class. Yes, aFeedSlot does inherit from aSlot via Doctrine column aggregation inheritance. However, Doctrine forms do not distinguish between column aggregation inheritance subclasses and will include all of the columns of all of the subclasses. Also, it's usually best to avoid custom columns and use setArrayValue and getArrayValue instead, which requires that we set up our own fields in the form class.

This class is only slightly changed from what the task generated. The url field has been added and given a suitable label and a limit of 1024 characters. The name format has been set in the usual way for a slot form and should not be changed (for a rare exception check out aRichTextForm, which must cope with certain limitations of FCK). The form formatter in use here is the Apostrophe form formatter, which produces nicely styled markup, but you may use another or render the form one element at a time in the _editView partial.

Slot Components, Part II: Actions

The edit action of your slot's module saves new settings in the form, or reports a validation error and refuses to do so.

Here's the edit action for aFeedSlot, which is exactly as the apostrophe:generate-slot-type task generated it:

    public function executeEdit(sfRequest $request)
      $value = $this->getRequestParameter('slotform-' . $this->id);
      $this->form = new aFeedForm($this->id, array());
      if ($this->form->isValid())
        // Serializes all of the values returned by the form into the 'value' column of the slot.
        // This is only one of many ways to save data in a slot. You can use custom columns,
        // including foreign key relationships (see schema.yml), or save a single text value 
        // directly in 'value'. serialize() and unserialize() are very useful here and much
        // faster than extra columns
        return $this->editSave();
        // Makes $this->form available to the next iteration of the
        // edit view so that validation errors can be seen, if any
        return $this->editRetry();

This action begins by calling $this->editSetup(), which takes care of determining what slot the action will be working with. The action then fetches the appropriate parameter, binds the form, validates it, and if successful saves the form's data in the slot with setArrayValue and calls $this->editSave() to save and redisplay the slot. If there is a validation error, $this->editRetry() should be called instead.

Depending on your needs you might not need to modify this action at all. The methods called by this action take care of version control, access control and everything else associated with the slot and let you get on with your job.

Custom Validation

Sometimes $this->form isn't quite enough to meet your needs. You might have more than one Symfony form in the slot (although you should look at embedForm() and mergeForm() first before you say that). Or you might not be using Symfony form classes at all.

Fortunately there's a way to pass validation messages from the executeEdit action to the next iteration of the editView component:

    // Set it in the action
    $this->validationData['custom'] = 'My error message';
    // Grab it in the component
    $this->error = $this->getValidationData('custom');
    // ... And display it in the template
    <?php if ($error): ?>
      <h2><?php echo $this->error ?></h2>
    <?php endif ?>

Note that $this->validationData['form'] is used internally to store $this->form, if it exists in the action. So we suggest that you use other names for your validation data fields.

Additional Actions

Things get interesting when you need to edit your slot with additional actions, possibly actions that go to different pages like the "Choose Image" buttons of our media slots.

You can write your own actions that use $this->editSetup(), $this->editSave() and $this->editRetry(). The tricky part is linking to these actions, from your normalView partial or elsewhere. Here's an example of a possible target for a form submission or redirect that would successfully invoke such an action:

    url_for('mySlot/myAction') . "?" .
        "slot" => $name,
        // The slug of the page where the slot lives.
        // Could be a virtual page if this is a global slot etc.
        "slug" => $page->slug, 
        // The actual page we were looking at when we began editing the slot
        "actual_slug" => aTools::getRealPage()->getSlug(),
        "permid" => $permid,
        // Optional: use this if you are redirecting
         // from another page and need the entire page to render
        "noajax" => 1))

For a fully worked example, we recommend taking a look at the aButtonSlot module, which uses both an edit view with a form (for the title and link) and a separate action that eventually redirects back (for selecting the image).

Adding Database Columns

The apostrophe:generate-slot-type task takes care of setting up the model classes for you so that Apostrophe can distinguish between your slot and other slots, even though all of them are stored in the a_slot table.

If you are using getArrayValue and setArrayValue or otherwise storing your data in the value column, you'll never have to worry about this directly.

However, if you do need to add custom columns, you'll need to know how this looks in config/doctrine/schema.yml.

Here's how aFeedSlot is configured:

      # Doctrine doesn't produce useful forms with column aggregation inheritance anyway,
      # and slots often use serialization into the value column... the Doctrine forms are not
      # of much use here and they clutter the project
          form:   false
          filter: false
      # columns:
      # You can add columns here. However, if you do not need foreign key relationships it is
      # often easier to store your data in the 'value' column via serialize(). If you do add columns, 
      # their names must be unique across all slots in your project, so use a unique prefix 
      # for your company.
      # This is how we are able to retrieve slots of various types with a single query from
      # a single table
        extends: aSlot
        type: column_aggregation
        keyField: type
        keyValue: 'aFeed'

Take a look at the inheritance section. The extends keyword specifies the class we are inheriting from, while the keyValue field must contain the name of the type. Doctrine uses this to figure out what class of object to create when loading a record from the a_slot table. The slot type name is recorded in the type column, already in the aSlot class. You don't need to worry about the details, but for more information about them, see the excellent Doctrine documentation.

Note that the keyValue setting does not include the word Slot.

To add extra columns at the database level, uncomment columns: and add new columns precisely as you would for any Doctrine model class. You can add new relations as well.

YOU MUST PREFIX YOUR CUSTOM COLUMN NAMES WITH A UNIQUE PREFIX to avoid collisions with other slots. Doctrine does not do this automatically, so please take care to avoid names that may lead to conflicts down the road.

Opening the Edit View Automatically For New Slots

By default, when a user adds a new slot to an area the user must then click the edit button before making changes to the slot. To take the user straight to the editView of your slot, you have to set the member variable editDefault to true. See the aRichTextSlot or aTextSlot for an example.

If your slot lives in a plugin, the right class to edit is plugins/myPlugin/lib/model/doctrine/PluginmySlot.class.php. If your slot lives at the project level, the right file is lib/model/doctrine/mySlot.class.php.

Managing Global Admin Buttons to the Apostrophe Admin Menu

When a user with editing privileges is logged in and visiting a page for which they have such privileges, a bar appears at the top of each page offering links to appropriate administrative features. Admins will see a button offering access to the sfGuardUser admin module. Editors in general will have access to the media module. You can add links of your own, or change the order of the buttons to be displayed.

Adding, Removing and Reordering Buttons via app.yml

The simplest way to add new buttons is to set app_a_extra_admin_buttons in app.yml. This allows you to add buttons that point to Symfony actions you coded yourself. By default, this is equivalent to:

        label: Users
        action: 'aUserAdmin/index'
        class: 'a-users'
        label: Reorganize
        action: a/reorganize
        class: a-reorganize

If you override this setting you will almost certainly want to keep these two buttons in the list.

Note that the key and the label field are different. label is shown to the user, and automatically internationalized by Symfony. The label is also looked for in the apostrophe i18n catalog. The key is used in app.yml settings to reorder buttons.

Note that the built-in media repository will always add a Media button to this list (with the name media), and the blog plugin, if installed, will add Blog and Events buttons.

To specify the order of the buttons, or discard buttons that were added by plugins, use app_a_global_button_order:

      - users
      - reorganize
      - media
      - blog

Global buttons will be displayed in the order specified here (specify button keys, not button labels). Any buttons you leave off the list will not be displayed at all, even if they were added by plugins, the media repository, etc.

If you do not specify app_a_global_button_order the buttons will be displayed in alphabetical order by name.

Adding Buttons Programmatically

app.yml is the easiest way to add buttons, but sometimes it's not enough. Perhaps you're writing a plugin that adds new features to Apostrophe and should register new buttons on its own. Or perhaps you want to add a button that targets a specific engine page. You can do both of these things by responding to the a.getGlobalButtons event.

First provide a static method in a class belonging to your own plugin or application-level code which invokes aTools::addGlobalButtons to add one or more buttons to the bar:

    class aMediaCMSSlotsTools
      // You too can do this in a plugin dependent on apostrophePlugin, see 
      // the provided stylesheet for how to correctly specify an icon to go 
      // with your button. See the apostrophePluginConfiguration class for the 
      // registration of the event listener.
      static public function getGlobalButtons()
          new aGlobalButton('media', 'Media', 'aMedia/index', 'a-media')));

The first argument to the aGlobalButton constructor is the name of the button. This is used to refer to that button elsewhere in your code and in app.yml. The second argument is the label of the button, which may contain markup and will be automatically internationalized. The third is the action (in your own code, typically). And the fourth is a CMS class to be added to the button, which is typically used to supply your own icon and a left offset for the image to reside in.

If your own plugin, like our media system, implements its administrative page as an apostrophe CMS engine page under /admin and also might have public engine pages elsewhere on the site, you'll want to make sure your button targets the "official" version. You can do that by providing the right engine page as a fifth argument to the aGlobalButton constructor:

    static public function getGlobalButtons()
      $mediaEnginePage = aPageTable::retrieveBySlug('/admin/media');
      // Only if we have suitable credentials
      $user = sfContext::getInstance()->getUser();
      if ($user->hasCredential('media_admin') || $user->hasCredential('media_upload'))
          new aGlobalButton('media', 'Media', 'aMedia/index', 'a-media', $mediaEnginePage)));

For more information see "Engines: Grafting Symfony Modules Into the CMS Page Tree" below.

Now, in the initialize method of your plugin or project's configuration class, make the following call to register interest in the event:

    // Register an event so we can add our buttons to the set of global 
    // CMS back end admin buttons that appear when the apostrophe is clicked. 
      array('aMediaCMSSlotsTools', 'getGlobalButtons'));

The bar at the top of each page will now feature your additional button or buttons.

Note: you should not add large numbers of buttons to the bar. Usually no more than one per plugin is advisable. It's important that the bar remain manageable and convenient for site admins.

Engines: Grafting Symfony Modules Into the CMS Page Tree

Suitably coded Symfony modules can now be grafted into the page tree at any point in a flexible way that allows admins to switch any page from operating as a normal template page to operating as an engine page, with all URLs beginning with that page slug remapped to the actions of the engine module. When the engine page is moved within the site, all of the virtual "pages" associated with the actions of the module move as well.

A single engine module can now be grafted into more than one location on a site. To take advantage of this feature, you must disable the Symfony routing cache. Disabling the routing cache is the default in Symfony 1.3 and 1.4 because the routing cache causes performance problems rather than performance gains in most cases (and in some cases they are quite severe and unpredictable). However, if you require the Symfony routing cache, you can still use engines as long as you don't install the same engine at two points in the same site. Even without multiple instances, engines still allow components such as a staff directory to be located at the point in the site where the client wishes to put them without the need to edit configuration files.

Engine modules are written using normal actions and templates and otherwise-normal routes of the aRoute and aDoctrineRoute classes.

This is a very powerful way to integrate non-CMS pages into your site. The media browser of apostrophePlugin already takes advantage of it, and the forthcoming apostropheBlogPlugin will as well.

Engines should always be used when you find yourself wishing to create a tree of dynamic "pages" representing something other than normal CMS pages, beginning at a point somewhere within the CMS page tree.

To create a a engine, begin by creating an ordinary Symfony module. Feel free to test its functionality normally at this point. Then change the parent class from sfActions to aEngineActions.

NOTE: if your actions class has a preExecute method of its own, be sure to call parent::preExecute from that method. Otherwise it will not work as an engine.

If your actions class must have a different parent class, implement your own preExecute() method in which you call Apostrophe's helper method for engine implementation. For example, this admin generator actions class has been modified to work as an Apostrophe engine:

class departmentActions extends autoDepartmentActions
  public function preExecute()

Now, create routes for all of the actions of your module, or a catch-all route for all of them. Make sure you give these routes the aRoute class in routing.yml. The following are sample routes for a module called enginetest:

    # Engine rules must precede any catch-all rules
      url:  /
      param: { module: enginetest, action: index }
      class: aRoute
      url:  /foo
      param: { module: enginetest, action: foo }
      class: aRoute
      url:  /bar
      param: { module: enginetest, action: bar }
      class: aRoute
      url:  /baz
      param: { module: enginetest, action: baz }
      class: aRoute

You can also use more complex rules to avoid writing a separate rule for each action, exactly as you would for a normal Symfony module. This example could replace the foo, bar, and baz rules above:

      url: /:action
      param: { module: enginetest }
      class: aRoute

You can also use Doctrine routes. Configure them as you normally would, but set the class name to aDoctrineRoute:

      url:     /:slug
      param:   { module: aEvent, action: show }
      options: { model: Event, type: object }
      class:   aDoctrineRoute
      requirements: { slug: '[\w-]+' }

Finally, in the forthcoming version 1.5 (and in the current trunk), you can use an aDoctrineRouteCollection:

  class: aDoctrineRouteCollection
    model:                Department
    module:               department
    prefix_path:          ''
    column:               id
    with_wildcard_routes: true

The above is the same route collection that doctrine:generate-admin-module added to routing.yml automatically for this module, except that the class has been changed to aDoctrineRouteCollection and the prefix path set to an empty string as a reminder that prefix paths are not relevant for engines (the prefix path is automatically overridden to an empty string in any case).

In general, you may use all of the usual features available to Symfony routes.

Note that the URLs for these rules are very short and appear to be at the root of the site. aRoute will automatically remap these routes based on the portion of the URL that follows the slug of the "engine page" in question.

That is, if an engine page is located here:


And the user requests the following URL:


The aRoute class will automatically locate the engine page in the stem of the URL, remove the slug from the beginning of the URL, and match the remaining part:


To the appropriate rule.

As a special case, when the engine page is accessed with no additional components in the URL, aRoute will match it to the rule with the URL /.

Note that as a natural consequence of this design, engine pages cannot have subpages in the CMS. In general, it is appropriate to use engines only when you wish to implement "virtual pages" below the level of the CMS page. If you simply wish to customize the behavior of just part of a page, a custom page template or custom slot will better suit your needs.

Once you have established your routes, you can create subnavigation between the actions of your module by writing normal link_to and url_for calls:

    echo link_to('Bar', 'enginetest/bar')

To make the user interface aware of your engine, add it to the templates option in app.yml (beginning in Apostrophe 1.5; see Apostrophe 1.4's sample app.yml for more information about the old way):

      # "a:" contains "ordinary page templates"
        default: Default Page
        home: Home Page
      # This is how you enable engines as page type choices. If the 
      # engine supports alternate page templates you can specify more 
      # than one entry for an engine
        default: Media
        default: Standard Blog
        alternate: Alternate Blog
        default: Events

This option configures the "page type" menu seen when creating a page. The list begins with "normal" page templates like the default and home page templates. These are followed by settings for engines, specifically the media, blog and event engines.

An engine must declare at least one template, named default. You can choose to define more than one. If you define more than one template, you can access the name of the template the user chose (default or alternate for instance) as $this->originalTemplate in your engine's actions class, and as $originalTemplate in the Symfony templates associated with your actions.

Alternatively, you can just return $this->pageTemplate from your actions. If your action is named show and the user chose the default template, this will return Success, causing Symfony to do the usual thing and load the showSuccess template. However if the user chose the alternate template this will return showAlternate, allowing the page to be rendered in a different way based on the user's preference. Note that the first letter is automatically uppercased to follow the Symfony standard for template filenames. This latter approach is followed by our own blog plugin. Which one to use depends on whether you want to keep things simple by making the templates entirely distinct, or save your front end designers time by allowing them to use a few inline if statements rather than entire redundant templates.

Linking to Engine Pages

Linking to the "index" action of an engine page is as simple as linking to any other page on the site. But what if you need to generate a link to a specific engine action from an unrelated page? For instance, what if you wish to link to a particular employee's profile within an engine page that contains a directory of staffers?

Just call link_to exactly as you did before:

    echo link_to('Bar', 'enginetest/bar')

If the current page is not an engine page matching the route in question, the a routing system will find the first engine page in the site that does match the route, and generate a link to that engine page.

Note: if there is currently no engine page for the given engine, this will throw an exception and generate a 500 error. This makes sense: trying to generate a link to an engine page that doesn't exist is a lot like trying to use a route that doesn't exist. You can test to make sure the engine page exists like this:

    <?php if (aPageTable::getFirstEnginePage('enginetest')): ?>
      <?php echo link_to('Bar', 'enginetest/bar') ?>
    <?php endif ?>

Which Engine Page Does My Link Point To?

When there is just one engine page on the site for a particular engine module, things are simple: links to routes for that engine always point to a URL beginning with that page. With multiple instances of the same module, things get trickier. Here's how to sort it out.

There are three simple rules:

1. When there is only one engine page on the site for a particular engine module 'blog', links always target that page by default. For many purposes, this is all you need.

2. When the page for which the link is being generated (the current CMS page) is an engine page for 'blog', links generated on that page will point back to that page by default, even if other engine pages for that engine module do exist. If the current page is not an engine page for the engine in question, the first matching engine page found in the database is used by default.

3. When you wish to target a specific engine page with link_to and url_for calls, you add an extra engine-slug parameter to the Symfony URL (beginning in Apostrophe 1.4):

    <?php echo link_to('Jane's Blog', 'blog/index?engine-slug=/janesblog') ?>

The engine-slug parameter will automatically be removed from the URL and will not appear in the query string. It is used only to determine which engine page to target. If you do not specify this parameter, you get the first matching engine page, as explained above. If you have an aPage object and wish to target it just set engine-slug to $myPage->slug.

There is an alternative to engine-slug which may be appropriate if you wish to override the engine slug for a large block of code or a partial you are about to include:

    <?php aRouteTools::pushTargetEnginePage('/janes-blog') ?>
    <?php echo link_to('Jane's Blog', 'blog/index') ?>
    <?php aRouteTools::popTargetEnginePage('blog') ?>

For convenience, you may pass either a page slug (like /janes-blog) or an aPage object to the aRouteTools::pushTargetEnginePage method.

Now all aRoute and aDoctrineRoute-based URLs generated between the push and pop calls that use the blog module will target the "Jane's Blog" page. URLs generated after the pop call revert to the usual behavior.

Since you may find yourself writing partials and components that are included in other pages, it is advisable to always pop after pushing a different engine page in order to avoid side effects.

We recommend using engine-slug rather than pushing and popping engine pages wherever practical, as it adheres more closely to the philosophy of newer versions of Symfony which emphasize dependency injection and frown on global state.

Again, you must not enable the Symfony routing cache if you wish to include multiple engine pages for the same engine in your site. The routing cache is turned off by default in both Symfony 1.3 and 1.4. If you have upgraded an older project you may need to manually shut it off in apps/frontend/config/routing.yml.

Extending the Page Settings Form: Creating an Engine Settings Form

"If I have separate engine pages for Jane's blog and Bob's blog, both using the blog engine, how do I distinguish them?" That part is easy. Just use the id of the engine page as a foreign key in your own Doctrine table and keep the details that distinguish them in that table. This is how our media repository associates particular engine pages with particular media categories.

"But how can editors change settings for that particular engine page?" Well, you could create your own settings action of course, and link to it from your engine page. Remember, Apostrophe is still Symfony, and you can always do normal Symfony development.

But we also provide a convenient way to extend the page settings form that rolls down when you click "This Page" and then click on the gear.

Assuming your engine module is called blog, this is all you have to do:

  1. Create a form class called blogEngineForm. The constructor of this class must accept an aPage object as its only argument.
  2. Create a blog/settings partial that renders $form. This partial can be as simple as <?php echo $form?> if you wish.

Apostrophe will automatically look for this form class. If it exists, Apostrophe will render both the standard page settings form and your engine settings form if that engine is selected. Apostrophe will automatically fetch your form on the fly if the user switches the template of the page to blog.

The page settings will not be saved unless both forms validate successfully. If both forms validate, Apostrophe will save them consecutively (the page settings form, followed by your engine settings form).

One simple way to create a form that works with this approach is to add a relation between aPage and your own table in your application or plugin schema. Then you can extend the aPageForm class and immediately remove all fields, then add back the fields you're interested in. Take a look at aMediaEngineForm:

class aMediaEngineForm extends aPageForm
  public function configure()
    $this->setWidget('media_categories_list', new sfWidgetFormDoctrineChoice(array('multiple' => true, 'model' => 'aMediaCategory')));
    $this->widgetSchema->setLabel('media_categories_list', 'Media Categories');
                $this->widgetSchema->setHelp('media_categories_list','(Defaults to All Cateogories)');
    $this->setValidator('media_categories_list', new sfValidatorDoctrineChoice(array('multiple' => true, 'model' => 'aMediaCategory', 'required' => false)));

The corresponding schema is:

  tableName: a_media_category
    Timestampable: ~
    Sluggable: ~
      type: integer(4)
      primary: true
      autoincrement: true
      type: string(255)
      unique: true
      type: string
      class: aMediaItem
      local: media_category_id
      foreign: media_item_id
      foreignAlias: MediaCategories
      refClass: aMediaItemCategory
    # Used to implement media engine pages dedicated to displaying one or more
    # specific categories
      class: aPage
      local: media_category_id
      foreign: page_id
      foreignAlias: MediaCategories
      refClass: aMediaPageCategory

This form takes advantage of the fact that Doctrine will automatically save the MediaCategories relation when save() is called on the form, looking for a widget named media_categories_list.

You don't have to extend aPageForm and use a relation in this way, but if you don't you'll need to make sure your updateObject() and save() methods do the right thing in your own way.

Testing Your Engine

After executing symfony cc, you will begin to see your new engine module as a choice in the new "Page Engine" dropdown menu in the page settings form. Select your engine and save your changes. The page will refresh and display your engine.

Note that engine pages can be moved about the site using the normal drag and drop interface.

You can create your own subnavigation within your engine page. We suggest overriding appropriate portions of your page layout via Symfony slots.


Internationalization is supported at a basic level: separate versions of content are served depending on the result of calling getCulture() for the current user. When you edit, you are editing the version of the content for your current culture. The user's culture defaults, as usual, to the sf_default_culture settings.yml setting. The search index also distinguishes between cultures. Webmasters who make use of internationalization will want to add a "culture switcher" to their sites so that a user interface is available to make these features visible. Thanks to Quentin Dugauthier for his assistance in debugging these features.

The user interface for editors is not yet internationalized. We plan to do this in the future.

Refreshing Slots

This is not necessary for any of our standard slot types. However, if your custom slot types contain metadata that should be refreshed nightly, you might wish to take advantage of the apostrophe:refresh task, which updates all current slots by calling their refreshSlot() method:

    ./symfony apostrophe:refresh --env=prod --application=frontend

Again, currently our own media slots do not require the use of this task. Thanks to architectural improvements deleting an item from the media plugin immediately updates the related slots. In future we may implement a handshake with YouTube? via this task to check whether video slots are still pointing to valid videos.

For performance reasons this task only looks at the latest version of each slot. You can use the --allversions option to specify that older versions of slots should be refreshed as well:

    ./symfony apostrophe:refresh --env=prod --application=frontend --allversions

Extending Search

You can override the a/search action at the application level. Just like any other Symfony application.

You can do that in three ways. The easiest way is to override the a/searchBefore and/or a/searchAfter partials to bring in a component of your own that displays "teaser" search results for your custom tables and invites the user to a dedicated page for searching them in more detail. You can grab the search query string from $sf_data->getRaw('q'). This is often the best way to go.

A second, equally easy way is to interleave your results with regular results by mirroring your content to a virtual page. Methods have been added recently to the 1.5 branch of apostrophePlugin allowing you to do this without a fuss:

1. Call Doctrine::getTable('aPage')->mirrorForSearch($this) from your Doctrine model object's postSave() event handler, and call Doctrine::getTable('aPage')->deleteSearchMirror($this) from your preDelete() event handler.

2. Your object must implement $this->getSearchUrl(), which must return a Symfony URL redirecting to a good landing page for the object. You can return null for objects that should not be indexed for search purposes.

3. Your object must implement $this->getSearchTitle() and $this->getSearchText(). It's OK to return a long text for getSearchText as the summary is automatically abbreviated. The entire text is indexed.

4. If you are adding this code to an existing project your existing objects are not automatically retroactively added to the search index. You'll want to write a one-time task that calls mirrorForSearch for each object, possibly breaking this up into multiple batches to avoid out of memory problems with Doctrine.

See below for a discussion of how to do this manually if you prefer.

A third approach is to directly implement inclusion of your objects in the search results. You can choose to use the aZendSearch class to somewhat simplify the process if you wish. This class requires that you implement quite a few wrapper calls to properly mix Doctrine and Lucene. For a complete example see the PluginaMediaItem class. But we really don't recommend it. It's much better to use the first or second approach above in 99.9% of cases.

Apostrophe Virtual Pages and Search

It is possible to store (or mirror) your additional data using Apostrophe virtual pages, as introduced in ManualDesignersGuide. Virtual pages are included in Apostrophe search results if the virtual page slug appears to be a valid Symfony URL: that is, if it begins with a @ or contains a / internally (not at the beginning). This is what the simple mirrorForSearch technique described above is doing "under the hoo.d" If you are already using virtual pages to display and edit content relating to your object, though, you may prefer to take charge of the process yourself.

apostropheBlogPlugin uses the vitual page technique to create valid links when a search matches a published blog post. In the blog plugin, all content is stored as Apostrophe virtual pages, and the page slugs look like this:


Search Helper Classes

Beginning in Apostrophe 1.5, if your virtual pages have a setting in the engine column, Apostrophe will append SearchHelper to it and look for a class by that name. If it exists, it will be instantiated and Apostrophe will call its getPartial method with no arguments. This allows you to return the name of an alternate partial to render search results for that type of virtual page. The partial receives the individual search result object as $result. This is a simple object with class, title, summary and url fields.

This is a powerful and effective approach but may not suit your needs if you don't wish to store or mirror your custom content in Symfony slots.

Your search helper class must also have a filterUpdateLuceneIndex method. This method is called when Apostrophe is about to index your virtual page. The method receives an array of arguments intended for aZendSearch::updateLuceneIndex. Your method can add to or alter this array before returning it, thus acting as a filter. This allows you to store additional data needed to render your partial properly, such as the start and end dates of events. Keep in mind you won't have access to your object unless you access the database from your custom partial, which can be expensive. So it's usually better to take advantage of the opportunity to store the extra fields in the search index.

In addition to other components, the arguments array has several subarrays that are relevant here: stored, keywords and indexed. In most cases you will add additional fields to stored in order to make additional data available to your search results partial. These fields are not indexed for searching purposes, so if you want them to be searchable, make sure you also add them to the indexed subarray (under a different name as otherwise the one overrides the other). If you want to add something large for search purposes and don't absolutely need it when rendering the results, add it only to indexed. keywords can be used for fields that are in both categories but will not be stored in a literal and unaltered form, so it's not a good choice for anything case-sensitive.

For clarity's sake, here is the complete BaseaEventSearchHelper class from apostropheBlogPlugin (note that aEventSearchHelper is just an empty extension of this class as a convenience to let you override it at project level):

class BaseaEventSearchHelper
  public function filterUpdateLuceneIndex($args)
    $slug = $args['stored']['slug_stored'];
    // We are only interested in the virtual pages associated with
    // this engine, leave the actual engine pages alone
    if (preg_match('/^@.*?(\d+)$/', $slug, $matches))
      $id = $matches[1];
      return $args;
    $event = aEventTable::getInstance()->find($id);
    $args['stored']['start_date'] = $event->start_date;
    $args['stored']['start_time'] = $event->start_time;
    $args['stored']['end_date'] = $event->end_date;
    $args['stored']['end_time'] = $event->end_time;
    return $args;
  public function getPartial()
    return 'aEvent/searchResult';

And here is the _searchResult partial:

<?php $url = $result->url ?>
<dt class="result-title <?php echo $result->class ?>">
	<?php echo link_to($result->title, $url) ?> 
<dd class="result-daterange"><?php include_partial('aEvent/dateRange', array('aEvent' => $result)) ?></dd>
<dd class="result-summary"><?php echo $result->summary ?></dd>
<dd class="result-url"><?php echo link_to($url, $url) ?></dd>

Deprecated Approach: searchAddResults

Prior to the introduction of apostropheMysqlSearchPlugin as the standard search engine in the sandbox project it was not uncommon to interleave your own search results directly. This technique is documented here for historical purposes and is supported only if you are using the Zend search functionality rather than the mysql search plugin, which is the default in newer projects.

To do this, override the searchAddResultsmethod in your application-level aActions class, which should extend BaseaActions?.

The code below demonstrates how you might handle search results for a blog that chooses not to use our virtual pages approach, if you are using zend search:

// This is at the application level,

class aActions extends BaseaActions
 protected function searchAddResults(&$values, $q)
    // Inside a loop through many articles....

    // Best when used with results from a aZendSearch::searchLuceneWithScores call. That call gives
    // you access to the scores so you can pass them along to Apostrophe.

    $value = new stdclass();
    $value->url = $url;
    $value->title = $title;
    $value->score = $scores[$id];
    $value->summary = $summary;
    // 'class' is used to set a CSS class (see searchSuccess.php) to distinguish result types.
    $value->class = 'Article';
    $values[] = $value;

    // IF YOU CHANGE THE $values ARRAY you must return true, otherwise it will not be sorted by score.
    return true;

This method's job is to add more results to the results array (note that it is passed by reference), which then get sorted by score with everything else and presented as part of the search results.

If you are wondering how to integrate Zend Search into your own modules, check out our aZendSearch class (apostrophePlugin/lib/toolkit/aZendSearch.class.php), which does it for our own data types. That class provides methods you can call from your model classes to add search indexing to them, and also expects your class to provide some methods of its own that get called back.

For a working example, see the aMediaItem class. The save() method calls:

// Let the culture be the user's culture
return aZendSearch::saveInDoctrineAndLucene($this, null, $conn);

That method calls back to your doctrineSave method, which is usually a simple wrapper around parent::save for a Doctrine model class, but you can extend it as needed:

 public function doctrineSave($conn)
   $result = parent::save($conn);
   return $result;

And it also calls back to updateLuceneIndex, which should invoke aZendSearch::updateLuceneIndex with an associative array of fields to be included in the search index:

  public function updateLuceneIndex()
    aZendSearch::updateLuceneIndex(array('object' => $this, 'indexed' => array(
      'type' => $this->getType(),
      'title' => $this->getTitle(),
      'description' => $this->getDescription(),
      'credit' => $this->getCredit(),
      'categories' => implode(", ", $this->getCategoryNames()),
      'tags' => implode(", ", $this->getTags())

The array we pass as the second argument to updateLuceneIndex contains fields that should be indexed so that we can search on them, but not stored in full for display purposes. That's great here because with media we know we'll want to retrieve those objects from Doctrine later anyway. But it is also possible to ask Lucene to actually store some fields for you. And that is crucial if you want to display complete search results with our unmodified a/search template.

Specifically, you'll need to store:

  • The title ('title')
  • The summary text ('summary')
  • The URL ('url'), which can be a Symfony URL or a regular URL
  • 'view_is_secure' (a boolean flag indicating whether logged-out users

and logged-in users without guest permissions are allowed to see this search result)

Your 'getFullSearchText' method would typically just append all of the fields that contain text relevant to searching (title, tags, actual body text) into a single string and return that.

You must also call aZendSearch::deleteFromDoctrineAndLucene from your delete method (I'm leaving out some stuff specific to the media item class here):

 public function delete(Doctrine_Connection $conn = null)
   return aZendSearch::deleteFromDoctrineAndLucene($this, null, $conn);

That method will call back to your doctrineDelete method, which is usually just a wrapper around parent::delete:

 public function doctrineDelete($conn)
   return parent::delete($conn);

You may wonder why there are so many aZendSearch-related methods. Basically, we would have used multiple inheritance here, but PHP doesn't have it. So instead we provide helper methods in the aZendSearch class which give us a flexible way to "inherit" the searchable behavior without explicit support for multiple inheritance in PHP.

Including Your Tables in Search Index Rebuilds

When you first set up search for a new table you'll want to rebuild your site's search index to include your existing data. And you'll likely notice that Apostrophe doesn't know to include it yet.

You'll need to turn this on in app.yml:

      - 'aPage'
      - 'aMediaItem'
      - 'myOwnTable'

Note that this approach applies only for classes that have their own search indexing implementation. For classes that rely on mirrorForSearch, you shouldn't need it, because this is called automatically when the objects are manipulated. But to index your existing objects you'll want to write a one-time task that saves each one without changes, possibly breaking this up into multiple batches to avoid out of memory problems.

Add the name of your own Doctrine table to the list to ensure that the apostrophe:rebuild-search-index task knows about it. Be sure not to remove aPage or aMediaItem from the list.

Search Index Rebuilds and Out of Memory Errors

If you have hundreds of objects, rebuild-search-index may run out of memory due to Doctrine's issues with memory management. Fortunately Apostrophe supports breaking search engine updates up into multiple runs of PHP in a simple and transparent way. All you have to do is add a lucene_dirty boolean column to your table, defaulting to false, and Apostrophe will automatically use it to schedule your objects to be indexed in batches of 100 rather than exhausting the memory in the system all at once.

Search and I18N

If your object is internationalized, be sure to set the culture field in the options array when calling updateLuceneIndex. aZendSearch will automatically maintain separate indices for each culture and return results for the searching user's culture. If the object is culture-independent, you don't need to do this.

Manipulating Slots and Pages Programmatically

Normally slots and pags are created through the UI. Apostrophe is an in-context CMS and that's as it should be. But there are times when you need to create or manipulate slots on the fly in your own code, and Apostrophe provides an API to help you do that.

Fetching Pages With Their Slots

Doctrine developers may be tempted to just use findOneBySlug and then iterate over areas and so on. Don't do this. You will get all of the related objects for all versions of the page throughout time, and the first one you get will not be the current version, nor will the slots in an area be in the right order.

The correct way to fetch the home page is:

$page = aPageTable::retrieveBySlugWithSlots('/');

You can pass any page slug; the home page is just an example.

This method fetches the page with its current slots in the correct order.

Note that if you are writing a page template or partial you can get the current page much more cheaply. In a page template you can just use the $page variable which is already correctly populated for you. In a partial you can call $page = aTools::getCurrentPage().

You can then fetch the slots of any area by name, in the proper order:

$slots = $page->getSlotsByAreaName('body');

This suggests an easy way to check whether an area is empty:

if (count($page->getSlotsByAreaName()) == 0)
  // This area is currently empty

You can also fetch an individual slot by its area name and permid. Note that the permid of a singleton slot (inserted with a_slot rather than a_area) is always 1:

$slot = $page->getSlot('footer', 1);

Usually you won't manipulate slot objects directly, but you may find the $slot->getText() method useful in some situations. This method returns entity-escaped text for the slot. Normally you'll rely on Apostrophe to display and edit slots via the a_slot() and a_area() helpers.

Advanced Queries for Pages

If you need to fetch more than one page, or have other Doctrine criteria for fetching the page, or need to add additional joins, consider:

$query = aPageTable::queryWithSlots();
$query->whereIn('p.id', array(some page ids...));
$pages = $query->execute();

aPageTable::queryWithSlots returns a Doctrine query with the necessary joins to correctly populate returned pages with the current versions of the correct slots.

Adding and Updating Slots

Usually you'll want to create a new slot type and let BaseaSlotActions and BaseaSlotComponents do the dirty work for you. But sometimes you may want to manipulate slots directly.

You can modify a slot object and save it, but if you do, you're not creating a history that the user can roll back.

To do that, make a *new* slot object and use newAreaVersion to add it to the history:

   $page = aPageTable::retrieveBySlugWithSlots('/foo');
   $slot = $page->createSlot('aText');
   $slot->value = $title;
   $page->newAreaVersion('title', 'update',
       'permid' => 1,
       'slot' => $slot));

Note that every slot in an area has a distinct value for permid. For a singleton slot permid is always 1.

Note that you want to use a new slot object. Often it's easiest to copy the previous version:

$slot = $slot->copy();

// Make your changes to $slot, then call newAreaVersion

If you want to add an entirely new slot (like "add content" does), specify 'add' rather than 'update'. You do not have to specify a permid since that is generated for you when adding a new slot:

$page->newAreaVersion('myareaname', 'add', array('slot' => $slot));

You can explicitly specify that it should or should not be at the top of the area:

$page->newAreaVersion('myareaname', 'add', array('slot' => $slot,
'top' => false));

The default is to add the slot at the top.

Deleting Slots

You can delete a particular slot with newAreaVersion as well:

$page->newAreaVersion('myareaname', 'delete', array('permid' => 1));

Looping Over All Slots in a Page

It's easy to loop over all of the current slots in a page. Note that there can be slots that are not actually used in the current template if a page has changed templates.

You can do it with a loop like this after you retrieveBySlugWithSlots:

foreach ($page->getAreaNames() as $areaName)
  foreach ($page->getSlotsByAreaName($areaName) as $slot)
    ... Do things with $slot ...

Recall that areas can contain multiple slots (as a result of the "add content" button). The permid is the slot's unique identifier within its area. All versions of the same slot will have the same permid. You need the permid to save a new revision of the slot.

Creating New Pages

Creating a virtual page (a page that is not part of the page tree) is easy:

$page = new aPage();
$page->setSlug('@my_search_redirect_route?id=' . $id_of_some_associated_object);
$page->setTitle('my title');

Now you can add slots using the APIs mentioned above.

Never, ever start the slug of a virtual page with a /. That is reserved for navigable pages in the page tree, which must be correctly added to the tree (see below). Your slug does not have to be a valid Symfony URL, but if it is, search results returned for your virtual page will offer a link that redirects to that URL.

Creating a page that is part of the page tree shown to the user requires just a little more care. You must add the page as a child of the appropriate parent page:

$parentPage = aPageTable::retrieveBySlug('/slug/of/parent');
$page = new aPage();
$page->setTitle('my title');

The insertAsFirstChildOf API is provided by the Doctrine ORM's nested set feature.

Deleting Pages

This one is easy: call the delete method on the page.

Adding Support For New Embedded Media Services

Out of the box, version 1.5 of Apostrophe supports embedding almost any media service (such as Youtube) by pasting embed tags via the "Embed Media" button. In addition, Apostrophe has native support for Youtube and Vimeo. That means that you can do the following things with Youtube and Vimeo that you can't do with other services:

  • Search for videos to add to the media repository via the "Search Services" button
  • Use the "Linked Accounts" feature to automatically bring media into the repository
  • Paste a video URL rather than a full embed code on the "Embed Media" page
  • Avoid typing in the title, description and tags manually
  • Automatically retrieve a thumbnail when adding the item

Beginning in Apostrophe 1.5 (and currently available in the svn trunk), you can add additional services at the project level or in a Symfony plugin. All you have to do is extend the aEmbedService class and implement the methods you find there, fetching the appropriate information from the API for the service you're interested in.

The APIs of these methods have deliberately been kept very simple. You should have very little trouble implementing them if you are comfortable with the service API you're talking to (and most are very easy to work with). Documentation of the expected return values is provided in comments in the aEmbedService class.

Although you can start from scratch, we recommend copying the aVimeo or aViddler class as a starting point, as it makes it easier to ensure you are returning data in the right format. The aYoutube class may be too simple as a starting point because their API has fewer requirements and served as our starting point. Vimeo and Viddler are better examples of how to transform API results into the format Apostrophe requires.

After you write your class, you'll need to tell Apostrophe about it with appropriate settings in app.yml:

      - class: aYoutube
        media_type: video
      - class: aVimeo
        media_type: video
      - class: aViddler
        media_type: video
      - class: aMyservice
        media_type: video

Here the standard YouTube?, Vimeo and Viddler services have been kept in place. You can choose to remove them if you wish by not including them in your settings.

Icons For Your Service

When you first enable your service you will see a regular text label with a radio button when you click "linked accounts" or "search services" in media. You can enhance this by creating two icons representing your service, the dimensions for the large icon are 108x44 pixels and the dimensions for the small icon are 44x18 pixels.

Then add the following to a CSS file in your plugin or project CSS:

.a-media-services-form .a-form-row.service label.aMediaSearchServices_service_Myservice,
.a-media-services-form .a-form-row.service label.a_embed_media_account_service_Myservice { background: url(../images/a-media-service-myservice.png) top left no-repeat; text-indent: -9999px; }
.a-media-linked-account .a-service.a-Myservice { background: url(../images/a-media-service-myservice-small.png) center center no-repeat; text-indent: -9999px; }

Change Myservice to the name returned by the getName() method of your service class ("inspect element" with Firebug to make sure the name has not been slugified slightly differently).

(In Apostrophe 1.4 there was no support for adding new media services that receive the same special treatment as Youtube, however pasting embed tags for most services is supported via the "Add via Embed Code" option in the media repository. We recommend moving to version 1.5 if you are interested in adding support for custom services.)

Using the aAdmin theme for Export Functionality

Apostrophe also comes with a Admin Generator theme that comes bundled with export functionality. Steps to enable and customize the export process are included below.

Modify generator.yml of the module to use the aAdmin theme and enable the export action.

  class: sfDoctrineGenerator
    theme:                 aAdmin
          display: [_user_id, ip_address, created_at]
            action: export
            label: '<span class="a-ui a-btn icon a-download"><span class="icon"></span>Export</span>'

Once the above is done exporting will just work, the fields will be pulled based on those defined in generator.yml the list_display key of generator.yml, in this case user_id, ip_address, and created_at.

The export process can be customized by overriding several methods of AdminGeneratorConfiguration?. Examples of possible overrides are shown below.

public function getExportDisplay()
  return array('user_id', 'ip_address');
public function getExportManager(sfWebResponse $response)
  return new loginExportManager($response);
public function getExportTitle()
  return "{$this->study->name}logins as of ".date('Y-m-d');

Overriding getExportDisplay() allows for the fields contained in the export to differ from those displayed in the list view. !getExportTitle defines the title of the file that is generated by the export process. !getExportManager returns the class that is used to generate the export file, by default this method returns an instance of !sfExportManager, by overriding this method further customization is possible. An example of a custom !exportManager is included below.


class loginExportManager extends sfExportManager
  public function exportField($object, $field)
    if($field == '_user_id')
      return 'user_id:'.$object->user_id;
    return parent::exportField($object, $field);


Thanks for checking out Apostrophe! We hope you're excited to experiment with our CMS. If you've read this far, there's a good chance you'll build custom slots and engines... and even send us bug reports. Hey, we love bug reports. Visit the  Apostrophe Trac to submit your reports, requests and concerns. And also be sure to join the  apostrophenow Google group to share your experiences with other Apostrophe developers.

Continue to Internationalizing Apostrophe

Up to Overview