Many websites - especially those driven by editorial content - have one or more aggregation pages like home pages, index pages, category pages and so on. Their purpose is to aggregate the underlying contents often with the latest contents on top but also you often find contents listed manually by an editor using some kind of administration tool.
The difficulty is that the underlying contents can be of various types like articles, galleries, videos, quizes, etc. .. Most of these contents will have their own custom visualisation as i.e. a gallery will render differently than an article.
gjPositionsPlugin provides means to compose such aggregation pages from a flexible list of design elements such as slideshows or teasers and to manually assign any type of content to them.
This functionality is provided in form of admin modules that you can generate for any of your models (in case a home page and an index page are not the same).
Currently you can only get this plugin from GitHub. It is still highly experimental and will change.
$ cd plugins
$ git clone git://github.com/caefer/gjPositionsPlugin.git
This plugin depends on my fork of sfDoctrineDynamicFormRelationsPlugin which is originally developed by Kris Wallsmith. A pull request is issued already so hopefully both versions can be merged again.
$ cd plugins
$ git clone git://github.com/caefer/sfDoctrineDynamicFormRelationsPlugin.git
Then edit your ProjectConfiguration class and enable the plugins.
<?php
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();
class ProjectConfiguration extends sfProjectConfiguration
{
public function setup()
{
$this->enablePlugins('sfDoctrinePlugin');
$this->enablePlugins('gjPositionsPlugin');
$this->enablePlugins('sfDoctrineDynamicFormRelationsPlugin');
}
}
Next you need to publish the plugins assets by running the following.
$ php symfony plugin:publish-assets
Please note that gjPositionsPlugin requires jQuery and jQuery UI. It was tested with jQuery version 1.4.2 (jquery-1.4.2.min.js) and jQuery UI version 1.8.6 (jquery-ui-1.8.6.min.js). Both files have to be loaded for the admin module to work!
- Composition Canvas
- The Composition Canvas is a space inside the generated admin module. You can drop multiple Design Elements onto it, reorder or delete them. When rendering you record you can access these design elments through a relation and render them accordingly.
- Design Element
- A Design Element is a partial or component plus some settings in your app.yml. DesignElements are developed by yourself according to your requirements. Design Elements can be static (partials) or dynamic (components) and they can be parameterized with settings you define and they can have Content Elements assigned to them.
- Content Element
- Content Elements are placeholders for a single record of any of your models. They represent the relation between a content record and a Design Element.
To get started you have to follow these following steps.
Assuming you have a model called Homepage
you simply have to add the gjCompositionCanvas
behaviour to its schema definition.
Homepage:
actAs:
Timestampable: ~
gjCompositionCanvas: ~
columns:
title: string(255)
headline: string(255)
...
This one is totally up to you! A Design Element can be anything from a simple partial
template to a complex component implementing business logic. The only thing you have to regard is that the only the following parameters will be passed to your partial/component:
params
is an array of settings saved for this Design Element. You will see in a moment how this can be done.subject
is the instance of your composition model (i.e.Homepage
) that this Design Element is assigned to.contents
is a list of Content Elements that were manually assigned to the Design Element.
Lets assume you wrote a simple partial yourmodule/banner
that renders a bit of HTML and contains no other PHP (more complex examples will follow later in this document).
all:
gjPositionsPlugin:
design_elements:
banner:
description: "This is a simple banner"
applies_to: [ Homepage ]
include: "yourmodule/banner"
accept: ~
params: ~
With these settings you defined a Design Element called banner that can be assigned to a Homepage
and will render the partial yourmodule/banner
.
If you want to provide a little business logic you may prefer a component. Here is what the settings would look like.
all:
gjPositionsPlugin:
design_elements:
banner:
description: "This is a simple banner"
applies_to: [ Homepage ]
include: [ yourmodule, banner ] // for a component you provide an array
accept: ~
params: ~
The other settings will be explained further on.
You simply generate an admin module for your Homepage
model with the symfony/doctrine admin generator.
$ php symfony doctrine:generate-admin --theme=composition frontend Homepage
The only difference is that you use the composition theme provided by gjPositionsPlugin.
Now browse to your freshly generated admin module and take a look. You will see that the edit view of your admin module consists of three columns.
- The left one displays the form for your `Homepage and also holds the Composition Canvas.
- The center column displays all Design Elements that can be applied to a
Homepage
- The left column displays Content Elements which we haven not used so far.
You don't have to change anything in your action so it might look as simple as this.
public function executeIndex(sfWebRequest $request)
{
$id = $request->getParameter('id');
$this->forward404unless($id);
$this->homepage = Doctrine_Core::getTable('Homepage')->find($id);
}
The interesting part happens in the template.
<?php use_helper('gjPositions') ?>
...
<?php foreach($page['DesignElements'] as $name => $designElement): ?>
<?php include_design_element($designElement); ?>
<?php endforeach; ?>
...
Through the DesingElements
relation you can simply iterate of all Design Elements that you have assigned to this Homepage
in the admin module. The gjPositionsHelper
provides a helper function include_design_element()
which merges the Design Element with the settings from your app.yml
and returns either an include_partial()
or include_component()
.
And that's all there is to it to be able to compose a Homepage
from various Design Elements.
But of course there is more to it!
If make heavy use of gjPositionsPlugin and turned multiple models into Composition Canvases you might find the situation that some Design Elements are not suitable for all Composition Canvases.
I.e. a contactform
might be only suitable for a Sidebar
but not for a Homepage
.
Taking the above example the following settings will limit the use of a banner
to only Homepages
and Sidebars
.
all:
gjPositionsPlugin:
design_elements:
banner:
description: "This is a simple banner"
applies_to: [ Homepage, Sidebar ]
include: [ yourmodule, banner ]
accept: ~
params: ~
With all the above you would be able to implement for example a Design Element that displays the latest articles that have been created in your database. This would be a simple component that fetches a number of articles from the database and renders them in a list in its partial template.
You can use this one Design Element on all your Homepages
. But you might want to filter the articles for a section homepage to show only those related to this section? Or you might want to list the latest 20 articles on your homepage but only 10 on your section homepage?
For this you can configure parameters on Design Elements like the following.
all:
gjPositionsPlugin:
design_elements:
latestArticles:
description: "Shows the latest articles"
applies_to: [ Homepage ]
include: [ article, latest ]
accept: ~
params:
number: { type: text, default: "5" }
section_id: { type: text, default: false }
These above settings will result in two more input fields on the Design Element in your admin module. They can be edited per assignation.
All types of <input/>
tags are available but probably only a few like text
, checkbox
or radio
make sense.
The values of these parameters will be available in your component and/or partial.
// in your component
...
$this->articles = Doctrine_Query::create()
->from('Article a')
->orderBy('a.created_at DESC')
->limit($this->params['number'])
->execute();
...
// in your partial
...
<?php for($i=0; $i < $params['number']; $i++): ?>
...
<?php endfor; ?>
...
Another form of parameterisation but with added usability is the assignment of Content Elements to a Design Element.
To give you an example you might want to show a Design Element "slideshow" on the top of your homepage
but you want to assign the images manually.
Lets prepare the Image
model first.
Image:
actAs:
gjCompositionContent: ~
columns:
title: string(255)
file: string(255)
...
After you regenerated the model classes you Image
will have a relation to the Content Elements (gjContentElement
) which are loosely coupled to it. No extra fields will be added.
Then you implement a slideshow component in your "gallery" module that expects a number of images to be displayed in a loop (i.e. using a jQuery plugin). Configure it like this.
all:
gjPositionsPlugin:
design_elements:
slideshow:
description: "Shows a slideshow of manually assigned images"
applies_to: [ Homepage ]
include: [ gallery, slideshow ]
accept: [ Image ]
params: ~
Now in your component and partial template you can access the list of all assigned content elements.
// in your component
...
foreach($this->contents as $content)
{
...
}
...
// in your partial
...
<?php foreach($contents as $content): ?>
...
<?php endforeach; ?>
...
Each content will be of type gjContentElement
on which the original content (in this case an Image
object) is available throught the getObject()
method. So you can do the following.
// in your partial
...
<?php foreach($contents as $content): ?>
<?php $image = $content->getObject(); ?>
<?php echo image_tag($image->file, array('title' => $image->title)); ?>
<?php endforeach; ?>
...
When you created your admin module symfony added an sfDoctrineRouteCollection to your applications routing.yml
like this.
homepage:
class: sfDoctrineRouteCollection
options:
model: Homepage
module: homepage
prefix_path: /homepage
column: id
with_wildcard_routes: true
As the composition admin module will contain a lot of informations it will issue a lot of queries to your database because all relations are by default lazy loaded which will happen for each and every iteration on the page.
To reduce the number of queries to a minimum you simply have to add two more lines to the above route definition so it looks like this.
homepage:
class: sfDoctrineRouteCollection
options:
model: Homepage
module: homepage
prefix_path: /homepage
column: id
with_wildcard_routes: true
model_methods:
object: getObject
This will add all appropriate left joins
to the queries which will in effect avoid a lot of similar queries.
When introducing gjPositionsPlugin to an existing project chances are that you are not able to regenerate your admin modules as you have already made some modifications to it. So instead there needs to be a way to adapt this functionality without loosing your old work.
You can do this in three simple steps.
In your admin module you have to create the file components.class.php
just next to your actions.class.php
in the actions
folder.
<?php
require_once sfConfig::get('sf_module_cache_dir').'/auto'.ucfirst('demo_page').'/actions/components.class.php';
/**
* demo_page components.
*
* @package positionsdemo
* @subpackage demo_page
* @author Your name here
* @version SVN: $Id: components.class.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
*/
class demo_pageComponents extends autoDemo_pageComponents
{
}
The above assumes the name
demo_page
for your module and you have to replace it with the real name.
Open your generator.yml
and change the theme
to "composition".
generator:
class: sfDoctrineGenerator
param:
model_class: DemoPage
theme: composition
non_verbose_templates: true
with_show: false
singular: ~
plural: ~
route_prefix: demo_page
with_doctrine_route: true
actions_base_class: sfActions
config:
actions: ~
fields: ~
list: ~
filter: ~
form: ~
edit: ~
new: ~
Now what's left is to clear the cache in order for the admin generator to generate the module from scratch in your cache directory.
$ php symfony cc
This plugin helps you i.e. to place articles (as a Content Element)on a homepage together with other Design Elements. Sometimes it may be useful to be able to overload specific fields of an article.
For SEO reasons it is often required for articles to show up with a different headline on an overview page that they have on their detail page.
Imagine the following schema:
Article:
actAs:
gjCompositionContent: ~
columns:
title: string(255)
text: string(10000)
...
To be able to override the title
field individually for each "placement" you can extend the above schema like the following:
Article:
actAs:
gjCompositionContent:
override: [ title ]
columns:
title: string(255)
text: string(10000)
...
By passing the override
option to the gjCompositionContent
behaviour you can specify an array of fieldnames you want to be able to override.
Now each Content Element holding an Article
will present an input field in your admin module.
In your template displaying the title
you can now define a fallback mechanism like this:
...
<?php echo $content['override']['title'] ? $content['override']['title'] : $content['Object']['title'] ?>
...
This will prefer the overriden over the original title. If no override was specified then the original will be displayed.