4. On the shoulders of giants...
PHP!
Symfony 1.4
Doctrine
MySQL
Zend Lucene
minify ( http://code.google.com/p/minify/ )
... and ideas from sfSimpleCMSPlugin
5. Goals of Apostrophe
Easy for clients to update without specialized training
Hard for clients to screw up by accident!
Extensible by any Symfony developer
6. Making it easy
"When you log in, it just gets awesomer"
Do things in context
When you can't do things in context, keep it simple
Don't require a degree in Drupal-ogy!
Check it out: demo.apostrophenow.com
9. ... OK, but how do you extend it?
Relax! It's Still Symfony (RISS)
Apostrophe embraces Symfony idioms
Slot = Doctrine inheritance + Engine = Symfony module +
Edit view component + aRoute & aDoctrineRoute +
Normal view component + Apostrophe page as a "host"
Edit action +
Edit form
... And layouts, and (page) templates, and plain old Symfony actions
12. Feed Slot: edit view component
class BaseaFeedSlotComponents extends aSlotComponents
{
public function executeEditView()
{
$this->setup();
// If this is the first validation pass make the form
if (!isset($this->form))
{
$this->form = new aFeedForm($this->id, $this->slot-
>getArrayValue());
}
}
...
}
13. Feed Slot: normal view component
...
public function executeNormalView()
{
$this->setup();
$this->values = $this->slot->getArrayValue();
if (!empty($this->values['url']))
{
$this->feed = aFeed::fetchCachedFeed(
$this->url, ...);
...
}
}
14. Feed Slot: edit view partial
<?php use_helper('a') ?>
<ul class="a-slot-info a-feed-info">
<li><?
php echo a_('Paste an RSS feed URL, a Twitter @name (with
the @), ' .
'or the URL of a page that offers a feed. Most blogs do.'
) ?></li>
</ul>
<?php echo $form ?>
16. Feed Slot: edit action
class aFeedSlotActions extends aSlotActions
{
public function executeEdit(sfRequest $request)
{
$this->editSetup();
$value = $this->getRequestParameter('slot-form-' . $this->id);
$this->form = new aFeedForm($this->id, array());
$this->form->bind($value);
if ($this->form->isValid())
{
// Serialize usually better than extra db columns
$this->slot->setArrayValue($this->form->getValues());
return $this->editSave();
} else
{
// Another validation pass
return $this->editRetry();
}
...
17. Feed Slot: aFeedForm
class aFeedForm extends BaseForm
{
public function __construct($id = 1, $defaults = array())
{
$this->id = $id;
parent::__construct();
$this->setDefaults($defaults);
}
public function configure()
{
$this->setWidget('url', new sfWidgetFormInputText(
array('label' => 'RSS Feed URL'))));
// Validators for: Twitter handle, lazy URLs,
// valid URLs, regular pages with feed URLs in meta tags
$this->widgetSchema->setNameFormat('slot-form-' . $this-
>id . '[%s]');
$this->widgetSchema->setFormFormatterName('aAdmin');
}
}
18. aFeedForm validators
$this->setValidators(array('url' => new sfValidatorAnd(array(
// @foo => correct twitter RSS feed URL
new sfValidatorCallback(
array('callback' => array($this, 'validateTwitterHandle'))),
// www.foo.bar => http://www.foo.bar
new sfValidatorCallback(
array('callback' => array($this, 'validateLazyUrl'))),
// Must be a valid URL to go past this stage
new sfValidatorUrl(
array('required' => true, 'max_length' => 1024)),
// Find feeds via meta tags in plain old pages
new sfValidatorCallback(
array('callback' => array($this, 'validateFeed')))))));
19. validateFeed: find feeds in plain pages
public function validateFeed($validator, $value)
{
$content = @file_get_contents($value);
if ($content)
{
$html = new DOMDocument();
@$html->loadHTML($content);
$xpath = new DOMXPath($html);
$arts = $xpath->query('//link[@rel="alternate" and @type="application/rss+xml"]');
if (isset($arts->length) && $arts->length)
{
return $arts->item(0)->getAttribute('href');
}
}
return $value;
}
21. Engines: multiple-page experiences
A Symfony module...
"Grafted" into the page tree
Multiple instances allowed
Easy to distinguish with categorized content
Examples: Bob's blog, Jane's blog, public photo gallery
22. Media engine: actions class (simplified)
class BaseaMediaActions extends aEngineActions
{
public function executeIndex(sfWebRequest $request)
{
$this->items = Doctrine::getTable('aMediaItem')-
>findAll();
}
public function executeShow(sfWebRequest $request)
{
$this->item = Doctrine::getTable('aMediaItem')
->findOneBySlug($request->getParameter('slug'));
}
}
24. Media engine: routing examples
1. /admin/media ->
Engine page /admin/media
Matches a_media_index route (special case for /)
2. /admin/media/view/iguana ->
Engine page /admin/media
Matches a_media_show route, slug is iguana
3. /iguanapix/view/iguana ->
Engine page /iguanapix
Matches a_media_show route, slug is iguana
25. Virtual pages
• Any page object with a slug not starting with /
• Efficiently stores slots & areas that will be needed together
• ... Which is how our blog plugin works
• Created on demand when you save the first slot
• ... Or in advance, paired with another object (blog post)
• Search treats these as routes: @blog_show?id=5, blog/show?id=5
• Search ignores this: not-searchable-57
• a_area(‘blog-body’, array(‘slug’ => “@blog_show?id=$id”))
26. Demo #2
Various actions of the event engine page
Category filtering (with nice URLs)
Permalink pages (with nice URLs)
Some nice features of the blog (and event) plugin
Virtual pages for blog and event content
The feed slot does its magic
28. Too friendly?
• “If it’s friendly it must be for small sites”
• More users = more training?
• Can you afford to train 100 people?
• Less training required = more scalable
29. Demo #3: features for big sites
• Access controls
• Reorganizing the page tree