apostrophenow.com / punkave.com
A Symfony-powered CMS
   your clients will love

     Thomas Boutell
What is Apostrophe?

Content Management System

Content Management Framework
On the shoulders of giants...

PHP!

Symfony 1.4
Doctrine
MySQL
Zend Lucene
minify ( http://code.google.com/p/minify/ )


... and ideas from sfSimpleCMSPlugin
Goals of Apostrophe

Easy for clients to update without specialized training

Hard for clients to screw up by accident!

Extensible by any Symfony developer
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
Demo #1

In-context editing

Button slots

Media repository

Cropping

Version History

... All “free” when you build custom slots
8
... 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
Templates: adding slots and areas

     <?php a_area('link', array( 
   'area_add_content_label' => 'Add Program', 
    'allowed_types' => array(
     'aButton',     
    ),
  'type_options' => array(
     'aButton' => array(
       'itemTemplate' => 'homeButton', 
       'width' => 144, 
       // 'flexHeight' => true, 
       'height' => 124, 
       // 'resizeType' => 'c', 
       'constraints' => array(
         'minimum-width' => 144, 'minimum-height' => 124, 
         'aspect-width' => 144, 'aspect-height' => 124),  
       'title' => true, 
       'description' => false)))) ?>
Feed Slot: schema.yml

aFeedSlot:
 inheritance:
   extends: aSlot
   type: column_aggregation
   keyField: type
   keyValue: 'aFeed'
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());
    }
  }
  ...
}
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, ...);
       ...
     }
 }
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 ?>
Feed Slot: normal view partial

<?php use_helper('a') ?>
<?php if ($editable): ?>
  <?php // Display the edit button ?>
  <?php include_partial('a/
simpleEditWithVariants', ... ) ?>
<?php endif ?>
<ul class="a-feed">
  <?php foreach ($feed->getItems() as $feedItem): ?>
    <?php include_partial('aFeedSlot/'.
$options['itemTemplate'],
      array('feedItem' => $feedItem, ... )) ?>
  <?php endforeach ?>
</ul>
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();
    }
  ...
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');
  }
}
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')))))));
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;
  }
apostrophe:generate-slot-type FTW!

./symfony apostrophe:generate-slot-type
     --plugin=myPlugin
     --type=monster

Generates:

schema.yml
Actions and components
monsterForm class

... Everything for a basic form-driven slot
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
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'));
    }
}
Media engine: routing (yml style)

a_media_index:
  url: /
  class: aRoute
  param: { module: aMedia, action: index }

a_media_show:
  url: /view/:slug
  class: aRoute
  param: { module: aMedia, action: show }
  requirements: { slug: ^[w-]+$ }
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
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”))
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
27
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
Demo #3: features for big sites

• Access controls
• Reorganizing the page tree
30
Bonus: safe, efficient JS calls + minifier
layout.php:
<head>
<?php a_use_javascripts() ?>
<?php a_use_stylesheets() ?>
</head>
<body>
... At the very END of the body:
<?php a_include_js_calls() ?>
</body>
_list_footer.php:
<?php a_js_call('apostrophe.enableUserAdmin(?)',
    array('choose-one-label' => a_('Choose One...'))); ?>
Apostrophe 2.0
Apostrophe 2.0
Apostrophe 2.0
Apostrophe 2.0
Apostrophe 2.0
Apostrophe 2.0
Apostrophe 2.0
Conclusions

1.5 is awesome now

2.0 will be awesome later

You are awesome

Let's be awesome together
apostrophenow.com
     apostrophenow.com
http://joind.in/view/talk/2744
        apostrophenow.com

Apostrophe (improved Paris edition)

  • 1.
  • 2.
    A Symfony-powered CMS your clients will love Thomas Boutell
  • 3.
    What is Apostrophe? ContentManagement System Content Management Framework
  • 4.
    On the shouldersof giants... PHP! Symfony 1.4 Doctrine MySQL Zend Lucene minify ( http://code.google.com/p/minify/ ) ... and ideas from sfSimpleCMSPlugin
  • 5.
    Goals of Apostrophe Easyfor clients to update without specialized training Hard for clients to screw up by accident! Extensible by any Symfony developer
  • 6.
    Making it easy "Whenyou 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
  • 7.
    Demo #1 In-context editing Buttonslots Media repository Cropping Version History ... All “free” when you build custom slots
  • 8.
  • 9.
    ... OK, buthow 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
  • 10.
    Templates: adding slotsand areas  <?php a_area('link', array(     'area_add_content_label' => 'Add Program',      'allowed_types' => array(      'aButton',          ),   'type_options' => array(      'aButton' => array(        'itemTemplate' => 'homeButton',         'width' => 144,         // 'flexHeight' => true,         'height' => 124,         // 'resizeType' => 'c',         'constraints' => array( 'minimum-width' => 144, 'minimum-height' => 124,  'aspect-width' => 144, 'aspect-height' => 124),          'title' => true,         'description' => false)))) ?>
  • 11.
    Feed Slot: schema.yml aFeedSlot: inheritance: extends: aSlot type: column_aggregation keyField: type keyValue: 'aFeed'
  • 12.
    Feed Slot: editview 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: normalview 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: editview 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 ?>
  • 15.
    Feed Slot: normalview partial <?php use_helper('a') ?> <?php if ($editable): ?> <?php // Display the edit button ?> <?php include_partial('a/ simpleEditWithVariants', ... ) ?> <?php endif ?> <ul class="a-feed"> <?php foreach ($feed->getItems() as $feedItem): ?> <?php include_partial('aFeedSlot/'. $options['itemTemplate'], array('feedItem' => $feedItem, ... )) ?> <?php endforeach ?> </ul>
  • 16.
    Feed Slot: editaction 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 classaFeedForm 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 feedsin 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;   }
  • 20.
    apostrophe:generate-slot-type FTW! ./symfony apostrophe:generate-slot-type --plugin=myPlugin --type=monster Generates: schema.yml Actions and components monsterForm class ... Everything for a basic form-driven slot
  • 21.
    Engines: multiple-page experiences ASymfony 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: actionsclass (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')); } }
  • 23.
    Media engine: routing(yml style) a_media_index: url: / class: aRoute param: { module: aMedia, action: index } a_media_show: url: /view/:slug class: aRoute param: { module: aMedia, action: show } requirements: { slug: ^[w-]+$ }
  • 24.
    Media engine: routingexamples 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 • Anypage 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 actionsof 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
  • 27.
  • 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: featuresfor big sites • Access controls • Reorganizing the page tree
  • 30.
  • 31.
    Bonus: safe, efficientJS calls + minifier layout.php: <head> <?php a_use_javascripts() ?> <?php a_use_stylesheets() ?> </head> <body> ... At the very END of the body: <?php a_include_js_calls() ?> </body> _list_footer.php: <?php a_js_call('apostrophe.enableUserAdmin(?)', array('choose-one-label' => a_('Choose One...'))); ?>
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
    Conclusions 1.5 is awesomenow 2.0 will be awesome later You are awesome Let's be awesome together
  • 40.
    apostrophenow.com apostrophenow.com
  • 41.