153
1 | Page = Apostrophe Manual = == Overview == Welcome to the 1.0 stable release of Apostrophe! Although this is our first official stable release, our CMS is already in regular use on a number of production sites. This release reflects the fact that our code has been stable and ready for your professional use for quite some time. So there's no need to wait; let's get started building your site. This document has nine chapters: * [ManualInstallation Installation] * [ManualUpgrade Upgrade] * [ManualEditorsGuide Editor's guide] * [ManualDesignersGuide Designer's guide] * [ManualDevelopersGuide Developer's guide] * [ManualBlogPlugin Blog plugin] * [wiki:ManualI18N Internationalization guide] * [ManualImportGuide Import & migration guide] * [ManualDeployment Our deployment process] Installation, the editor's guide, the designer's guide and the developer's guide are intended to be read consecutively by those who intend to do it all on their own. Naturally you might divide these tasks among your team, but the editor's and designer's material should be understood first before tackling the developer's section. The blog plugin is used to manage news and events and also to present timely content in other parts of the site. The Internationalization guide is an optional extra for those who need to support content in multiple languages or present clients with an editing interface in a language other than English.

Apostrophe Manual

Embed Size (px)

Citation preview

Page 1: Apostrophe Manual

1 | P a g e

= Apostrophe Manual =

== Overview ==

Welcome to the 1.0 stable release of Apostrophe! Although this is our first official stable release, our

CMS is already in regular use on a number of production sites. This release reflects the fact that our

code has been stable and ready for your professional use for quite some time. So there's no need to

wait; let's get started building your site.

This document has nine chapters:

* [ManualInstallation Installation]

* [ManualUpgrade Upgrade]

* [ManualEditorsGuide Editor's guide]

* [ManualDesignersGuide Designer's guide]

* [ManualDevelopersGuide Developer's guide]

* [ManualBlogPlugin Blog plugin]

* [wiki:ManualI18N Internationalization guide]

* [ManualImportGuide Import & migration guide]

* [ManualDeployment Our deployment process]

Installation, the editor's guide, the designer's guide and the developer's guide are intended to be

read consecutively by those who intend to do it all on their own. Naturally you might divide these

tasks among your team, but the editor's and designer's material should be understood first before

tackling the developer's section.

The blog plugin is used to manage news and events and also to present timely content in other parts

of the site. The Internationalization guide is an optional extra for those who need to support content

in multiple languages or present clients with an editing interface in a language other than English.

Page 2: Apostrophe Manual

2 | P a g e

Those who wish to migrate content from an existing CMS system will want to read the import &

migration guide.

The installation section is intended for developers and system administrators with some knowledge

of PHP and command line tasks. The upgrade section is appropriate for a similar audience and also

includes important notes for designers and developers who are affected by changes impacting

custom slots and CSS.

The editor's guide is suitable for end users who will have administrative responsibilities on the site,

such as editing and managing content. No programming skills required.

The designer's guide addresses the needs of front-end developers, discussing how to edit and

manage stylesheets, page templates and page layouts.

Finally, the developer's guide explores how to extend Apostrophe with new content slots that go

beyond our provided rich text and media features and new "engines" that extend the CMS with full-

page content that doesn't always fit neatly into the page metaphor. Most readers will never need to

refer to this section.

See the end of this document for information about community and professional support, the

Apostrophe community and how to participate in further development.

=== Guiding Philosophy ===

[http://www.apostrophenow.com/ Apostrophe] is a content management system. Apostrophe is

open source, and built upon the great work of other open source projects. That's why our

apostrophePlugin is a plugin for the [http://www.symfony-project.org/ Symfony] web application

framework.

The philosophy of Apostrophe is that editing should be done "in context" as much as possible,

keeping confusing modal interfaces to a minimum and always emphasizing good design principles

and an intuitive user experience. When we are forced to choose between ease of use and a rarely

used feature, we choose ease of use, or make it possible to discover that feature when you truly

need it.

Page 3: Apostrophe Manual

3 | P a g e

Before we decided to write our own CMS, we used sfSimpleCMSPlugin, and although our system is

quite different you can see its influence in Apostrophe. We'd like to acknowledge that.

=== Who Should Use Apostrophe Today? ===

Right now Apostrophe is best suited to PHP developers who want to make an intuitive content

management system available to their clients. Apostrophe is very easy for your clients to edit,

administer and maintain once it is set up. Right now, though, Apostrophe installations does call for

some command line skills and a willingness to learn about Symfony. We are working to reduce the

learning curve.

Front-end developers who do not yet have PHP and Symfony skills but wish to set up an Apostrophe

site by themselves should consider tackling the [http://www.symfony-

project.org/jobeet/1_4/Doctrine/en/ Symfony tutorial] to get up to speed. It's not necessary to

complete the entire tutorial, but it helps to have at least a passing familiarity with Symfony.

And of course we at [http://www.punkave.com/ P'unk Avenue] are available to develop complete

Apostrophe sites for those who see the value in a truly intuitive CMS and do not have the

development resources in-house to implement it.

=== Apostrophe Features ===

Standard features of Apostrophe include version control for all content slots, locking down pages for

authenticated users only, and in-context addition, deletion, reorganization and retitling of pages.

When a user is logged in with appropriate privileges intuitive editing tools are added to the usual

navigation, neatly extending the metaphors already present rather than requiring a second interface

solely for editing purposes.

Apostrophe also introduces "areas," or vertical columns, which users with editing privileges are able

to create more than one slot. This makes it easy to interleave text with multimedia and other custom

slot types without the need to develop a custom PHP template for every page.

Content "slots" include plaintext, rich text, RSS feeds, photos, slideshows, videos, PDFs and raw

HTML slots.

Page 4: Apostrophe Manual

4 | P a g e

Apostrophe includes support for media management, including a built-in media library that allows

you to manage locally stored photos and remotely hosted videos. When media are embedded in

pages they are automatically sized to cooperate with the page templates created by the designer.

Rich text editing, of course, is standard equipment. And unlike most systems, Apostrophe

intelligently filters content pasted from Word and other programs to ensure there are no design-

busting markup conflicts.

=== Supported Browsers ===

Editing works 100% in Firefox 2+, Safari 4+, Chrome and Internet Explorer 7+.

Editing is expressly not supported in Internet Explorer 6. Of course,

browsing the site as a user works just fine in Internet Explorer 6. Although IE 6 cannot support our

full editing experience, we recognize the need to support legacy browser use by the general public

when visiting the site.

=== System Requirements ===

Please see the [ManualInstallation Installation Guide] for a list of Apostrophe's system

requirements. A `servercheck.php` page is provided with the sandbox. This page can be accessed to

verify that your site meets the requirements.

== Support, Community and News Sources ==

Please be sure to join the [http://groups.google.com/group/apostrophenow apostrophenow Google

group]. This is the right place to obtain community support.

Bug tracking, subversion access and a community Wiki are all available at

[http://trac.apostrophenow.org trac.apostrophenow.org].

Professional support for Apostrophe is available from our team here at [http://www.punkave.com/

P'unk Avenue].

Also be sure to [http://twitter.com/apostrophenow follow our Twitter account].

Page 5: Apostrophe Manual

5 | P a g e

For bleeding-edge development news, subscribe Google Reader or your feed reader of choice to our

svn commit notes:

[http://www.apostrophenow.com/svn.rss http://www.apostrophenow.com/svn.rss]

And of course, be sure to [http://www.apostrophenow.com/ visit the Apostrophe Now site].

Please continue reading with one of the following:

* [ManualInstallation Installation]

* [ManualEditorsGuide Editor's guide]

* [ManualDesignersGuide Designer's guide]

* [ManualDevelopersGuide Developer's guide]

Page 6: Apostrophe Manual

6 | P a g e

= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Installation ==

There are two ways to get started with Apostrophe. You can start with our sandbox project, which

we heartily recommend, or add the apostrophePlugin to your existing Symfony project. The latter

only makes sense for experienced Symfony developers whose sites are already well underway. I'll

describe both approaches.

But first, let's make sure your webserver meets the requirements for Apostrophe.

== System Requirements ==

Apostrophe requires the following. Note that virtually all of the requirements are included in the

asandbox project which you can easily check out from svn, copy from svn to your own repository as

described below, or just download as a tarball.

The following must be installed on your system:

* PHP 5.2.4 or better, with a PDO driver for MySQL (we recommend PHP 5.3.3 or better if possible;

the latest in the 5.2.x series is also OK)

* MySQL (we will accept patches for greater compatibility with other databases provided they do

not damage MySQL support)

* For the media features: GD support in PHP, or the netpbm utilities. netpbm uses much less

memory

* Optional, for previews of PDFs in PDF slots: ghostscript (you must also have netpbm to use this

feature)

* A few truly excellent hosting companies already have netpbm and ghostscript in place. If you have

a Virtual Private Server (and you should, shared hosting is very insecure), you can most likely install

Page 7: Apostrophe Manual

7 | P a g e

netpbm and ghostscript with a few simple commands like sudo apt-get install netpbm and sudo apt-

get install ghostscript.

If you are choosing a Linux distribution, we recommend Ubuntu. Ubuntu includes a sufficiently

modern version of PHP right out of the box. If you are using Red Hat Enterprise Linux or CentOS, you

will need to upgrade PHP to version 5.2.x on your own. This is unfortunate and Red Hat really ought

to get a move on and fix it.

A `servercheck.php` page that verifies these requirements is included in the sandbox. Just visit that

page after adding the `web` folder of the sandbox to your server as the document root of a new site.

Mac users: want to build your site on your own computer first? Please say yes, this is the right way

to avoid surprises. You can most easily meet the web server and PHP requirements by installing

the latest version of [http://www.mamp.info/ MAMP]. Note that MAMP's PHP

must be your command line version of PHP, not Apple's default install of PHP.

To fix that, add this line to the `.profile` file in your home directory:

{{{

export PATH="/Applications/MAMP/Library/bin:/Applications/MAMP/bin/php5/bin:$PATH"

}}}

Of course your production server will ultimately need to meet the same requirements with regard to

PHP and PDO.

Apple's default version of PHP for Snow Leopard is theoretically capable of working with

Apostrophe, but unfortunately Apple chose to ship the very first bleeding-edge version of PHP 5.3

and as of this writing they have not updated it to address the many PHP bugs since discovered and

fixed in the PHP 5.3 series.

Yes, you can also use Apostrophe with Microsoft Windows as a hosting environment. Core

Apostrophe features do not depend on Unix-specific command line tools and services. We strongly

recommend that you use the Microsoft Web Platform PHP accelerator for reasonable performance

on Windows.

Page 8: Apostrophe Manual

8 | P a g e

=== Ways to Download the Apostrophe Sandbox Project ===

==== Download The Tarball ====

The easiest way is to just [http://www.apostrophenow.com/downloads/asandbox-stable.tar.gz

download a tarball of the sandbox project.] This tarball is updated nightly from the stable branch of

our sandbox. This is a great way to get started, but note that you won't be able to get security and

stability fixes just by typing `svn update` this way.

==== Check It Out From Subversion ====

Alternatively you can check it out with subversion:

{{{

svn co http://svn.apostrophenow.org/sandboxes/asandbox/branches/1.4 asandbox

}}}

That will check out the 1.4 stable branch, which is suitable for use with both Symfony 1.3 and

Symfony 1.4.

If you prefer to live dangerously and use our most bleeding-edge code ('''as of 6/22 the trunk is

currently quite bleeding-edge and we would recommend using the 1.4 branch instead'''), you can

check out the trunk:

{{{

svn co http://svn.apostrophenow.org/sandboxes/asandbox/trunk asandbox

}}}

We love it when you do this, because it results in more feedback for us, but you should definitely

consider using the stable branch for your client projects. We use the stable branch for our own client

projects.

Page 9: Apostrophe Manual

9 | P a g e

==== Our Favorite: Copying the Sandbox to Your Own Repository ====

Checking out the sandbox from svn is a nice way to get started, but you'll soon wonder how to save

your own changes when the project is part of our own svn repository. That's why we recommend

that you instead copy it to your own repository with

[https://sourceforge.net/projects/svnforeigncopy/ svnforeigncopy] each time you want to start a

new site. That's what we do. With `svnforeigncopy` you get a copy of the asandbox project in your

own svn repository, with the `svn:ignore` and `svn:externals` properties completely intact. You don't

get the project history, but since 99% of the code is in the externally referenced plugins and libraries,

that's really not a big deal. The important thing is that you are still connected to the latest bug-fix

updates for our plugin.

This will give you all of the necessary plugins and the ability to

`svn update` the whole shebang with one command.

Here's an example. In this case I have already copied the `svnforeigncopy` script to my personal

`~/bin` folder and made it executable with the `chmod` command. (That assumes `~/bin` is in your

PATH, which you can adjust in `~/.profile` or ~/.bashrc`. If this is all Greek to you, it might be better

to download the tarball.)

```NOTE:``` You need svn version 1.5 or better. MacOS X Snow Leopard includes a 1.6.x version of

svn, which is plenty good enough. So does Ubuntu Linux. If you don't have svn 1.5 or better, don't

worry about it, just install our tarball instead.

This command creates a new `svn` repository, of course you can use an existing one, such as a

beanstalk repository. For more information about svn see the [http://svnbook.red-bean.com/ official

svn red bean book]. Of course you should substitute your own preferred location for `/Users/boutell`

here:

{{{

svnadmin create /Users/boutell/myrepo

}}}

Page 10: Apostrophe Manual

10 | P a g e

```IMPORTANT:``` if you are making your own repository make sure you turn off `enable-rep-sharing`

to avoid this error:

{{{

svn: no such table: rep_cache

}}}

To prevent this, after you create your repository (see above), edit

`/Users/boutell/myrepo/db/fsfs.conf` and add this line:

{{{

enable-rep-sharing = false

}}}

This disables an optional optimization that can cause problems. Unfortunately it is turned on by

default in svn on MacOS X Snow Leopard. Thanks to Pablo Godel of !ServerGrove for pointing out

the issue.

Now, this command copies the project from our repository to yours:

{{{

svnforeigncopy http://svn.apostrophenow.org/sandboxes/asandbox/branches/1.4

file:////Users/boutell/myrepo/demosite Sites/demosite

}}}

Those arguments are the path you're copying from, the path you're copying to, and the local

checkout folder where you'll work with the end result (also used as a scratchpad during the copy

operation).

You'll be prompted to commit to svn a couple of times (you may need to set your VISUAL

environment variable if you have never done so before), and the checkout of symfony in lib/vendor

Page 11: Apostrophe Manual

11 | P a g e

means it'll take a little while to do the final 'update' that brings down the related plugins and

Symfony itself. When it's over, though, you have our project cloned in your own repository where

you can commit your own changes without losing the connection to the stable branch of our plugin.

=== Apache Configuration ===

Once you have fetched the code by your means of choice, you're ready to configure your testing

web server to recognize a new website. Light up the folder `asandbox/web` as a website named

`asandbox` or whatever suits your needs via MAMP, `httpd.conf` or whatever your web hosting

environment requires. As with any Symfony project you'll want to allow full Apache overrides in this

folder. See `config/vhost.sample` for tips on virtual host configuration for Apache. Make sure the

directives in `web/.htaccess` will be honored by your server.

(Confused about this MAMP stuff? Wondering how you can test websites on your own computer?

You really should pick up [http://www.mamp.info/ MAMP] if you are on a Mac. Windows and Linux

users should consider using [http://www.apachefriends.org/en/xampp.html XAMPP]. These are all-

in-one packages with PHP, MySQL, Apache and everything else you need to test real websites on

your own computer. You can also install Apostrophe directly on a web hosting account. We just think

you'll find web development with Apostrophe and in general oh so much more pleasant if you learn

to test on your own computer before uploading things.)

=== PHP Configuration ===

There is a `php.ini` setting that determines whether parameters in URLs are separated by & or

by &. Apostrophe currently requires that they be separated by &.

In most web hosting setups, you can add this line to `web/.htaccess` of your Apostrophe project to

ensure that the separator is &:

{{{

php_value arg_separator.output &

}}}

If PHP directives are not honored in `.htaccess` files in your hosting setup, you can do this in your

`php.ini` file:

Page 12: Apostrophe Manual

12 | P a g e

{{{

arg_separator.output = "&"

}}}

You must also have a reasonable PHP `memory_limit` setting. We recommend 64 megabytes of

memory for PHP:

{{{

memory_limit = 64M

}}}

A few Linux distributions set unrealistic memory limits on PHP by default. In this case you may get a

blank page when accessing Apostrophe sites.

Don't forget to restart Apache if you make changes in `php.ini`.

Note that without this rule content editing will not work properly in Apostrophe.

=== Symfony Configuration ===

Yes, this is a preconfigured sandbox project, but you do have to adjust a few things to reflect reality

on your own computer.

Now create the `config/databases.yml` file, which must contain

database settings appropriate to ''your'' system. Copy the file

`config/databases.yml.sample` as a starting point:

{{{

cp config/databases.yml.sample config/databases.yml

Page 13: Apostrophe Manual

13 | P a g e

}}}

If you are testing with MAMP the default settings

(username root, password root, database name asandbox) may work

just fine for you. If you are testing on a staging server you will

need to change these credentials.

Also create your `properties.ini` file:

{{{

cp config/properties.ini.sample config/properties.ini

}}}

In Symfony `properties.ini` contains information about hosts that a project can be synced to, in

addition

to the name of the project. The sample properties.ini file just defines the name of the project. You'll

add information there when and if you choose to sync the project to a production server via

`project:deploy` or our enhanced version, `apostrophe:deploy`. See the Symfony documentation for

more information about that technique.

At this point you're ready to use the checkout of Symfony's 1.4.x stable branch

that is included in the project. If you want to use a different installation

of Symfony, such as a shared install for many sites (note that only 1.3.x and 1.4.x are likely to work),

copy config/require-core.php.example to config/require-core.php and edit the

paths in that file.

Next, cd to the `asandbox` folder and run these commands.

Page 14: Apostrophe Manual

14 | P a g e

(Windows users: you should remove the `web/apostrophePlugin`, `web/apostropheBlogPlugin`, etc.

folders first. These are the subfolders in `web` that end in `Plugin`. Unix/Linux users don't have to

worry about this.)

{{{

./symfony cc

./symfony plugin:publish-assets

./symfony doctrine:build --all

./symfony doctrine:data-load

}}}

This will create a sample database from the fixtures files.

If you prefer, you can pull down our full demo site as an alternative to the somewhat bland fixtures

site. Replace the `doctrine:data-load` command with this one (it's OK to do this if you already did the

other command):

{{{

./symfony apostrophe:demo-fixtures

}}}

Note that this task will run for quite a while as media files are included in the download.

Now set the permissions of data folders so that they are writable by the web

server. Note that svn does NOT store permissions so you can NOT assume they are

already correct:

{{{

./symfony project:permissions

Page 15: Apostrophe Manual

15 | P a g e

}}}

Our apostrophePlugin extends project:permissions for you to include the

data/writable folder in addition to the standard web/uploads, cache and log folders. Handy, isn't it?

If you prefer you can do this manually:

{{{

chmod -R 777 data/a_writable

chmod -R 777 web/uploads

chmod -R 777 cache

chmod -R 777 log

}}}

More subtle permissions are possible. However be aware that most

"shared hosting" environments are inherently insecure for a variety

of reasons. Production Symfony sites should run on a virtual machine of their own, or share a VM

only with other sites written by you. So before criticizing the "777 approach," be sure

to [http://trac.symfony-project.org/wiki/SharedHostingNotSecure read this article on shared hosting

and Symfony].

Next, build the site's search index for the first time (yes, search is included). It doesn't live in

the database so it needs to be done separately. After this, you won't

need to run this command again unless you are deploying to a new

environment such as a staging or production server and don't plan to sync your content with

sfSyncContentPlugin:

{{{

./symfony apostrophe:rebuild-search-index --env=dev

Page 16: Apostrophe Manual

16 | P a g e

}}}

(You can specify `staging` or `prod` instead to build the search indexes

for environments by those names. You'll want that later when working on a production server.)

'''You can now log in as `admin` with the password `demo` to see how the site behaves when you're

logged in''' (if you used the apostrophe:demo-fixtures task, the password will be `demo`). Start

adding subpages, editing slots, adding slots to the multiple slot content area... have a ball with it!

{{{

Note: For all versions of the demo site, the login is `admin` and the password is `demo`.

}}}

=== The Hard Way: Adding apostrophePlugin To An Existing Site ===

''Those who installed the sandbox just now can skip right over this section.''

Installing the sandbox is the easy way to install Apostrophe. The notes that follow assume you're

doing it the hard way, without starting from the asandbox project.

Begin by installing the following Symfony plugins into your Symfony 1.3/1.4 project:

* sfJqueryReloadedPlugin

* sfDoctrineGuardPlugin (4.0.x - version 5.0.x has undocumented backwards compatibility breaks

and no documentation of its new features - we cannot recommend it right now)

* sfDoctrineActAsTaggablePlugin

* sfWebBrowserPlugin

* sfFeed2Plugin

* sfSyncContentPlugin (recommended but not required)

* And of course, [http://www.symfony-project.org/plugins/apostrophePlugin apostrophePlugin]

Page 17: Apostrophe Manual

17 | P a g e

We strongly encourage you to do so using svn externals.

'''For sfDoctrineGuardPlugin you currently must use the 4.0.x series. This is not what `plugin:install`

gives you by default.''' The 5.0.x series has been declared stable, but there is no documentation of

the many changes, including backwards compatibility breaks in the names of relations. So we cannot

recommend it yet.

If you are using svn externals to fetch plugins, pin your `svn:external` for sfDoctrineGuardPlugin to

the 1.3 branch:

`http://svn.symfony-project.com/plugins/sfDoctrineGuardPlugin/branches/1.3`

Also, if you are using svn externals you will need to be sure to create the necessary symbolic links

from your projects web/ folder to to the web/ folders of the plugins that have one. Use the

`plugin:publish-assets` task, or create your own links with a relative path:

{{{

cd web

ln -s ../plugins/apostrophePlugin/web apostrophePlugin

# Similar for other plugins required

}}}

The search features of the plugin rely on Zend Search, so you must

also install the Zend framework.

[http://framework.zend.com/download/latest The latest version of the minimal Zend framework is

sufficient.] If you choose to install this system-wide

where all PHP code can easily find it with a `require` statement, great.

If you prefer to install it in your Symfony project's

`lib/vendor` folder, you'll need to modify your `ProjectConfiguration` class

Page 18: Apostrophe Manual

18 | P a g e

to ensure that `require` statements can easily find files there:

{{{

class ProjectConfiguration extends sfProjectConfiguration

{

public function setup()

{

// We do this here because we chose to put Zend in lib/vendor/Zend.

// If it is installed system-wide then this isn't necessary to

// enable Zend Search

set_include_path(

sfConfig::get('sf_lib_dir') .

'/vendor' . PATH_SEPARATOR . get_include_path());

// for compatibility / remove and enable only the plugins you want

$this->enableAllPluginsExcept(array('sfPropelPlugin'));

}

}

}}}

Create an application in your project. Then create a

module folder named `a` as a home for your page templates

and layouts (and possibly other customizations):

{{{

mkdir -p apps/frontend/modules/a/templates

}}}

Page 19: Apostrophe Manual

19 | P a g e

The CMS provides convenient login and logout links. By default these

are mapped to sfGuardAuth's signin and signout actions. If you are

using sfShibbolethPlugin to extend sfDoctrineGuardPlugin, you'll

want to change these actions in apps/frontend/config/app.yml:

{{{

all:

sfShibboleth:

domain: duke.edu

a:

actions_logout: "sfShibbolethAuth/logout"

actions_login: "sfShibbolethAuth/login"

}}}

You can also log in by going directly to `/login`. If you don't want to display the login link

(for instance, because your site is edited only you), just shut that feature off:

{{{

all:

a:

login_link: false

}}}

You will also need to enable the Apostrophe modules in

your application's `settings.yml` file. Of course you may need

other modules as well based on your application's needs:

Page 20: Apostrophe Manual

20 | P a g e

{{{

enabled_modules:

- a

- aSync

- aNavigation

- aMedia

- aMediaBackend

- aRichTextSlot

- aTextSlot

- aRawHTMLSlot

- aSlideshowSlot

- aVideoSlot

- aImageSlot

- aButtonSlot

- aPDFSlot

- aFeedSlot

- sfGuardAuth

- aUserAdmin

- aGroupAdmin

- aPermissionAdmin

- sfGuardPermission

- taggableComplete

- aNavigation

- default

- aAdmin

}}}

Page 21: Apostrophe Manual

21 | P a g e

Apostrophe edits rich text content via the FCK editor. A recent version of FCK is

included with the plugin. However you'll need to enable FCK in your settings.yml file, as follows:

{{{

all:

rich_text_fck_js_dir: apostrophePlugin/js/fckeditor

}}}

==== Disabling Output Escaping ====

By default, a new Symfony 1.4 project escapes all output that is emitted in templates. Apostrophe's

strategy is to store valid UTF-8 encoded HTML to begin with, addressing this issue in validators.

Currently you must disable output escaping in settings.yml, otherwise Apostrophe slots will not work

properly:

{{{

all:

# Output escaping settings

escaping_strategy: false

}}}

(We are looking into ways to support either setting so that Apostrophe does not force you to choose

the same strategy for your own Symfony code. If we can do that without too much overhead, we

will.)

Now, load the fixtures for a basic site. Every site begins with a home

page with all other pages being added as descendants of the home page:

{{{

Page 22: Apostrophe Manual

22 | P a g e

./symfony doctrine:build --all

./symfony doctrine:data-load

}}}

Note: if you are adding apostrophePlugin to an existin site you probably don't want to delete your

other tables and start over! You can do:

{{{

./symfony doctrine:build --all-classes --sql

}}}

To build the model classes and to build the SQL code that would be sufficient to recreate your

database. Then look in data/sql/schema.sql for the CREATE TABLE statements for tables beginning

with the `a_` prefix.

Ideally you would use Doctrine's migration schema diff task instead. Unfortunately there is currently

a bug in Symfony and Doctrine that prevents successful use of this feature with tables that use

inheritance. See [http://trac.symfony-project.org/ticket/7272 Symfony bug 7272 ] for more

information and patches if you are interested in pursuing that approach. It's probably easier to just

locate the CREATE TABLE statements.

You will also want to load the fixtures for the apostrophePlugin or, depending on your needs, edit

them to suit your purposes better first. You can do so without clobbering your existing database:

{{{

./symfony doctrine:data-load --append plugins/apostrophePlugin/data/fixtures/seed_data.yml

}}}

In particular you must have a home page and the hidden `/admin` and `/admin/media` engine pages.

Page 23: Apostrophe Manual

23 | P a g e

Also see the fixtures provided with the sandbox project for strongly recommended (but not

mandatory) permissions and groups settings.

==== .htaccess Rules For Media ====

The media module of Apostrophe uses carefully designed URLs to allow images to be

served as static files after they are generated for the first time. This

is done at the Apache level to maximize performance: PHP (and therefore

Symfony) don't have to get involved at all after the first time an image

is rendered at a particular size.

The following special .htaccess rules are required to enable this. These

should be copied to your .htaccess file after the

`RewriteBase /` rule, if you are using one, but before any other rules.

{{{

###### BEGIN special handling for the media module's cached scaled images

# If it exists, just deliver it

RewriteCond %{REQUEST_URI} ^/uploads/media_items/.+$

RewriteCond %{REQUEST_FILENAME} -f

RewriteRule .* - [L]

# If it doesn't exist, render it via the front end controller

RewriteCond %{REQUEST_URI} ^/uploads/media_items/.+$

RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^(.*)$ index.php [QSA,L]

###### END special handling for the media module's cached scaled images

}}}

==== Routing Rules ====

Page 24: Apostrophe Manual

24 | P a g e

By default Apostrophe will map CMS pages to URLs beginning with `/cms`:

{{{

/cms/pagename

}}}

And leave all other URLs alone. This is appropriate if the CMS is a minor part of your site. If the CMS

is the main purpose of your site, shut off the automatic registration of the route above in app.yml:

{{{

a:

routes_register: false

}}}

And register these as the LAST rules in your application's `routing.yml` file instead:

{{{

# A default rule that gets us to actions outside of the CMS.

# Note that you can't have regular CMS pages with a slug beginning with /admin

# on an Apostrophe site. At some point we'll make this reserved prefix more

# configurable.

}}}

{{{

default:

url: /admin/:module/:action/*

}}}

Page 25: Apostrophe Manual

25 | P a g e

{{{

# A homepage rule is expected by a and various other plugins,

# so be sure to have one

}}}

{{{

homepage:

url: /

param: { module: a, action: show, slug: / }

}}}

{{{

# Put any routing rules for other modules and actions HERE,

# before the catch-all rule that routes URLs to the

# CMS by default.

}}}

{{{

# Must be the last rule

}}}

{{{

a_page:

url: /:slug

param: { module: a, action: show }

requirements: { slug: .* }

}}}

Page 26: Apostrophe Manual

26 | P a g e

Thanks to Stephen Ostrow for his help with these rules.

'''You can change the a_page rule, prefixing the page slug with anything you wish, but you must have

such a rule and it must be named `a_page`. Otherwise Apostrophe engines, including the built-in

media repository functionality and media slots, will not work.'''

==== layout.php Requirements ====

Apostrophe's non-CMS pages, such as the "reorganize" feature, the user admin feature and the

media repository, expect a layout that offers certain overridable Symfony slots, in particular the `a-

subnav` slot. The media repository overrides this slot to offer media navigation links instead of a list

of subpages. If your application layout does not offer such a slot to override, the media repository

will be missing some of its interface.

Take a close look at `modules/a/templates/layout.php` and check out the way we've provided the

ability for page templates to override various parts of the layout via Symfony slots. If you experience

missing bits of the UI with your own layout, make sure you offer the same slots at appropriate

points.

** Prior to Apostrophe 1.0.7 ** the media repository in particular defaults to using a built-in layout

that is not really suitable for its current markup. You can fix this with an `app.yml` setting which is

also found in the sandbox project:

{{{

all:

aMedia:

use_bundled_layout: false

}}}

Beginning in Apostrophe 1.0.7 this is no longer necessary.

Page 27: Apostrophe Manual

27 | P a g e

==== Styling Requirements ====

The user interface will be essentially complete at this point, but you may notice a few areas in the

media repository where the look and feel is a bit too austere unless you borrow elements from the

sandbox project's `main.css` file or apply some styles of your own. In fact in Apostrophe 1.1 we will

be migrating toward moving more of the hard-to-override, site-design-specific CSS from `a.css` to

the `main.css` file of the sandbox project. So you may want to refer to that file as a reference.

==== Set Permissions ====

In Symfony, the `cache` and `web/uploads` folders are always world-writable so that the web server

can store files. Apostrophe adds one more such folder, `data/a_writable`. This folder is used for files

that should be writable by the web server but not directly accessible via a URL. This folder is used for

search indexes and other data that does not live in the database.

You can set up this folder yourself, but it's more convenient to use Symfony's `project:permissions`

task, which apostrophePlugin enhances to handle this folder:

{{{

./symfony project:permissions

}}}

==== Performance Optimization ====

Sites with many pages sometimes require some tuning for good performance.

By default, Apostrophe is configured to provide good navigation and reasonable performance

without running out of memory, even if your site has thousands of pages. Previously a separate

setting was provided to specify that your site has many pages, but this setting has been removed as

we've found the code for the "worst case" scenario is acceptably fast on small sites too.

Page 28: Apostrophe Manual

28 | P a g e

However, there is an issue with searches on sites with thousands of pages. Searches for common

words can exhaust the memory available.

You can address this by setting a hard limit on the number of pages Apostrophe should consider

when generating search results. Note that this is not the same thing as the number of results the

user will see, because some results may be locked pages the user is not eligible to see. That filtering

takes place after the hard limit, otherwise it does not save memory. So the limit should be set

generously.

To set a hard limit on the memory required for a search, set this `app.yml` option:

{{{

all:

a:

search_hard_limit: 1000

}}}

1000 is a good choice because it will not exhaust a realistically chosen PHP `memory_limit` and

almost certainly will not exclude any worthwhile results either.

==== Clear the Cache and Get to Work! ====

No Symfony developer will be shocked to hear that you need to clear your Symfony cache at this

point:

{{{

./symfony cc

}}}

==== Scheduled Tasks: Enabling Linked Accounts ====

Page 29: Apostrophe Manual

29 | P a g e

'''Version 1.5 and above:''' if you wish to take advantage of the "Linked Accounts" feature, which

automatically brings new media into the media repository from Youtube and other media services

specified by the site administrator, you will need to schedule the following command line task to run

on a scheduled basis. We suggest using a cron job to run this task every 20 minutes. Change the `--

env` setting as appropriate to your environment:

{{{

./symfony apostrophe:update-linked-accounts --env=prod

}}}

==== Conclusion ====

We're done installing Apostrophe! On to the [ManualEditorsGuide editor's guide], which briefly

explains how to edit content with Apostrophe- briefly because it is so much more fun to simply use

it.

[ManualEditorsGuide Continue to the Editor's Guide]

[ManualOverview Up to the Overview]

Page 30: Apostrophe Manual

30 | P a g e

= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Upgrading Apostrophe ==

=== Overview ===

From time to time we issue upgrades for Apostrophe, more specifically for the open source Symfony

plugins `apostrophePlugin` and `apostropheBlogPlugin`. For the more part this process is all but

automatic within the Apostrophe 1.x series. However, sometimes a major change does justify a few

manual steps to allow new features to be implemented. We'll add notes on these issues to this page

over time to ensure that an upgrade path is available, just as we did during the transition from

`pkContextCMS` to `apostrophePlugin`.

=== Standard Upgrade Steps ===

* If you are installing Apostrophe via the `plugin:install` task or downloading Symfony plugin tarballs

directly from the Symfony plugins site, then of course you'll need to download the new version. If

you used `plugin:install` you should be able to use `plugin:upgrade` to install the latest version of

`apostrophePlugin`. Remember that you should upgrade the blog plugin at the same time.

* Always run the `symfony cc` task after upgrading.

* Always run the `apostrophe:migrate` task after upgrading. For instance, on your development

computer, you would type:

`./symfony apostrophe:migrate --env=dev`

This task upgrades database tables as needed. If the blog plugin is present it will also upgrade its

database tables. For the most part we avoid unnecessary schema changes but sometimes additions

are necessary to deliver new features. We generally avoid these in patchlevel releases (1.4.2,

Page 31: Apostrophe Manual

31 | P a g e

1.4.3...) but they may be necessary to address bugs in rare cases. They are more common in minor

version changes (1.4, 1.5).

If you are not using MySQL, then the `apostrophe:migrate` task will not work directly for you.

However the source code of the task remains a valuable resource for determining the necessary

changes.

=== Version-Specific Upgrade Notes ===

Be sure to consult these when migrating from an older version up to or beyond the version

mentioned.

==== Version 1.5 ====

'''First make sure that apostropheBlogPlugin is enabled AFTER apostrophePlugin in your

config/ProjectConfiguration.class.php file.''' Do NOT use `enableAllPluginsExcept` for this purpose.

Enable the specific plugins you want, with the blog plugin listed after the main Apostrophe plugin.

Otherwise certain class files will not be found beginning in version 1.5.

Developers: if you have created custom slot types, either on your own or by using the

`apostrophe:generate-slot-type` task, you will need to make a small change to the `normalView`

partial of your slot.

Beginning in version 1.5, the `slot` parameter must be added to the list of parameters to the

`simpleEditButton` partial. For example:

{{{

<?php include_partial('a/simpleEditButton', array('pageid' => $page->id, 'name' => $name, 'permid'

=> $permid, 'slot' => $slot)) ?>

}}}

Page 32: Apostrophe Manual

32 | P a g e

If you are using `simpleEditWithVariants` you won't need to change anything. If you are not using a

standard edit view at all (many slots, such as our slideshow slot, do not need one), then you don't

have to worry about this change.

Page 33: Apostrophe Manual

33 | P a g e

= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Editor's Guide ==

This section is devoted to users who will participate in editing, maintaining and administering the

content of the site. All of these tasks are performed through your web browser. No command line

skills are required.

Access your new Apostrophe site's URL to see the home page.

Click "log in" and log in as the admin user (username admin, password demo)

to see the editing controls. Notice that editing controls are added to the normal experience of the

site. This greatly reduces the learning curve for editors.

=== Managing Pages ===

When a user has appropriate privileges on a page, they are able to make

the following changes by clicking the "This Page" button, then interacting with a simple "breadcrumb

trail" that appears at the top of the page:

* Rename the page by clicking on the page title

* Add a child page beneath the current page

* Open the page management settings dialog via "This Page," then the "gear" icon for less frequent

changes

Apostrophe emphasizes "unpublishing" pages as the preferred way

of "almost" deleting them because it is not permanent. Anonymous users, and users who do not

have editing privileges on the page, will see the usual 404 Not Found error.

But users with suitable editing privileges will see the page with its title

Page 34: Apostrophe Manual

34 | P a g e

"struck through" and will be able to undelete the page if they desire. This prevents the loss of

content.

You can also delete a page permanently via the small X in the lower right

corner of the page settings dialog. Most of the time that's a shortsighted

thing to do, but it is useful when you create an unnecessary page by accident.

The side navigation column also offers an editing tool: users with

editing privileges can change the order of child pages listed there

by dragging and dropping them. (If a page has no children, the side

navigation displays its peers instead, including itself.) You can do the same thing with the tabs at the

top of the page. Also check out the "Reorganize" button, which is discussed in more detail later.

=== Editing Slots ===

What about the actual content of the page? The editable content

of a page is stored in "slots" (_note to developers: not the same thing as Symfony slots_).

CMS slots can be of several types:

* Plaintext slots (single line or multiline)

* Rich text slots (edited via FCK)

* Feed slots (RSS or Atom feeds, such as Twitter feeds, inserted into a page)

* Raw HTML slots (best avoided in good designs, but useful when you must paste raw embed codes)

* Media slots (image, slideshow, video, PDF, button)

* Custom slots (of any type, implemented as described in the developer's guide section)

Once you have logged in, you'll note that each editable slot has an "Edit" button or, in the case of

the media slots, "Select Image" and similar buttons.

Page 35: Apostrophe Manual

35 | P a g e

Every slot also offers version control. The arrow-in-a-circle icon

accesses a dropdown list of all changes that have been made to that slot,

labeled by date, time and author and including a short summary of the change

to help you remember what's different about that version. Pick any version to preview it. Click "Save

as current version" to permanently revert to that version.

=== Editing Areas ===

In addition to single slots, apostrophePlugin also supports

"areas." Areas are vertical columns containing more than one slot.

Editing users are able to add and remove slots from an area at any time,

selecting from a list of slots approved for use in that area. The slots

can also be reordered via up and down arrow buttons (used here instead

of drag and drop to avoid possible browser bugs when dragging and dropping

complex HTML, and because drag and drop is not actually much fun to use

when a column spans multiple pages).

The usefulness of areas becomes clear when rich text slots are interleaved with media slots. Media

slots provide a robust way to manage photos and videos without wrecking the layout of the site. You

can upload photos and select and embed videos freely without worrying about their size and format.

=== Revising History ===

Did you make a mistake? Not a problem! Both slots and areas allow you to roll back to any previous

edit. Just click "History," preview versions by clicking on them, and click "Save As Current Revision"

when you find the version you want.

=== Editing Media ===

Click "Add Slot," then "Image," then "Select Image." You will be taken to the media area of the site.

Here you can select any of the images that have already been uploaded to the site, or upload new

images. This media repository provides a clean way to keep track of the media you have available.

Page 36: Apostrophe Manual

36 | P a g e

You can do much the same thing with video. YouTube is tightly integrated with Apostrophe, and you

can easily search YouTube from within the media interface. You can also paste embed codes from

other sites and add thumbnail images for those videos manually.

You can organize media with categories and tags. Add new categories using the "Manage

Categories" button at the left, in the media browser area.

It's possible to add a media page to the public-facing part of the site that displays only media in

certain categories. To do that, go to the home page, add a new page and give it a title that relates to

a media category. When the new page appears, click "This Page," then the gear icon. When the page

settings dialog appears, select "Media" from the "Page Engine" menu.

Once you select Media, a category selector will appear. Pick the appropriate category, then click

Save. Note that you can select more than one category.

The page will now refresh and display a media browser that anyone on your site can use to see

media in that category, most recent first, with all of the browsing features that are standard in the

media area.

=== Reorganizing the Site ===

Apostrophe offers drag-and-drop reordering of the children of

any page via the navigation links on the left-hand side. You can also reorder the tabs at the top of

any page by dragging and dropping.

However, there are times when you want to do something less linear, like moving a page up or down

in the page tree.

To do that, click on the "Reorganize" button at the top of any page. This will take you to the

reorganize tool, a page where you can drag and drop pages to any position in the page tree for quick

and painless reorganization of the site.

This tool is only available to site administrators such as the `admin` user.

Page 37: Apostrophe Manual

37 | P a g e

[ManualDesignersGuide Continue to the Designer's Guide]

[ManualOverview Up to the Overview]

Page 38: Apostrophe Manual

38 | P a g e

= Apostrophe Manual =

[ManualOverview Up to Overview]

== Designer's Guide ==

This section explains how to go about customizing the appearance and behavior of the site without

writing new code (apart from simple calls to insert slots and otherwise use PHP as a templating

language). It is intended to be read by front end designers (also known as front end developers).

Some familiarity with Symfony is expected. You will need to edit files like

`apps/frontend/config/app.yml` to adjust settings, and you will be creating `.php` files although full

PHP programming skills are not required.

=== Title Prefix ===

By default, the title element of each page will contain the title of that page.

In many cases you'll wish to specify a prefix for the title as well.

You can do so by setting `app_a_title_prefix` in `app.yml`. This option supports

optional internationalization:

{{{

all:

a:

# You can do it this way...

title_prefix:

en: 'Our Company : '

fr: 'French Prefix : '

# OR this way for a single-culture site

title_prefix: 'Our Company'

Page 39: Apostrophe Manual

39 | P a g e

}}}

Note that all changes to `app.yml` should be followed by a `symfony cc` command:

{{{

./symfony cc

}}}

=== Creating and Managing Page Templates and Layouts ===

Where do slots appear in a page? And how do you insert them?

Slots can be inserted in two places: in your site's <tt>layout.php</tt>

file, which decorates all pages, and in page template files, which can

be assigned to individual pages.

=== How to Customize the Layout ===

By default, the CMS will use the

<tt>layout.php</tt> file bundled with it. If you wish, you can turn this

off via app.yml:

{{{

all:

a:

use_bundled_layout: false

}}}

CMS pages will then use your application's default layout. One strategy

Page 40: Apostrophe Manual

40 | P a g e

is to copy our <tt>layout.php</tt> to your application's template folder

and customize it there after turning off use_bundled_layout.

=== How to Customize the Page Templates ===

The layout is a good place for global elements that should appear on

every page. But elements specific to certain types of pages are better

kept in page templates. These are standard Symfony template files with

a special naming convention.

Page template files live in the templates folder of the a module.

We provide these templates "out of the box:"

* homeTemplate.php

* defaultTemplate.php

homeTemplate.php is used by our default home page, and defaultTemplate.php

is the default template if no other template is chosen.

You can change the template used by a page by using the

template dropdown in the breadcrumb trail. This does not delete

the slots used by the previous template, so you can switch back

without losing your work.

How do you create your own template files? ''Don't'' alter the templates

folder of the plugin. As always with Symfony modules, you should

instead create your own a/templates folder within your

application's modules folder:

Page 41: Apostrophe Manual

41 | P a g e

{{{

mkdir -p apps/frontend/modules/a/templates

}}}

Now you can copy homeTemplate.php and defaultTemplate.php to this folder,

or just start over from scratch. You can also copy _login.php if you don't

like the way we present the login and logout options. We ''do not recommend'' altering the rest of

the templates unless

you have a clear understanding of their purpose and function and are

willing to make ongoing changes when new releases are made. In general,

if you can use CSS to match the behavior of our HTML to your needs,

that will be more forwards-compatible with new releases of the CMS.

If you add additional template files, you'll need to adjust

the `app_a_templates` setting in `app.yml` so that your

new templates also appear in the dropdown menu:

{{{

all:

a:

templates:

home:

Home Page

default:

Default Page

mytemplate:

My Template

Page 42: Apostrophe Manual

42 | P a g e

}}}

=== Custom Navigation in Templates and Layouts ===

Apostrophe supports three types of navigation components. These can be found in the aNavigation

module. Including these navigation elements is as easy as including a component in your template

or in `layout.php`. The three types of navigation elements are:

* An accordion tree

* A tabbed or vertical menu of links to child pages

* A breadcrumb trail from the home page to the current page.

The navigation element, once used, will no longer require any additional SQL queries no matter how

many similar navigation elements are included in a template or layout.

==== Accordion Navigation ====

Accordion-style navigation can be easily included in your template by doing the following. Accordion

navigation is a compromise between showing the entire page tree and showing just the breadcrumb.

Accordion navigation includes everything that would be included in a breadcrumb trail, plus the

peers of all of those pages. This makes it easy to navigate to related pages at any level without being

overwhelmed by a comprehensive list of all pages on the site. These links are rendered as nested `ul`

elements.

{{{

<?php include_component('aNavigation', 'accordion', array('root' => '/', 'active' => $page->slug,

'name' => 'normal')) ?>

}}}

Parameters

* root (required) - This is the slug of the root page that the menu should start from. Often the home

page

Page 43: Apostrophe Manual

43 | P a g e

* active (required) - The page to build the navigation down to, will also recieve a css class of current

* name (required) - The unique name of the navigation element for your template. This influences

CSS IDs and can be used for styling

==== Tabbed Navigation ====

The tabbed navigation provides a navigation element that displays all of the children of a page. Note

that depending on your CSS you can render it as a vertical menu. For an example, see the "subnav"

area of the `asandbox` project; see `a/templates/_subnav.php).

{{{

<?php include_component('aNavigation', 'tabs', array('root' => '/', 'active' => $page->slug, 'name'

=> 'tabs')) ?>

}}}

Parameters

* root (required) - This is the parent slug of the tabs that are displayed.

* active (required) - This is the page that if in the navigation will recieve a current css class of current

* name (required) - The unique name of the navigation element for your template

* extras (optional) - Additional links to present at the beginning of the list

You can also inject additional links at the beginning of the list:

{{{

<?php include_component('aNavigation', 'tabs', array('root' => '/', 'active' => $page->slug, 'name'

=> 'tabs', 'extras' => array('/page/slug/or/absolute/url' => 'Label'))) ?>

}}}

If you wish to link to a Symfony action on the site as an additional tab, you'll need to use `url_for` to

create an absolute URL to it:

Page 44: Apostrophe Manual

44 | P a g e

{{{

'extras' => array(url_for('module/action?my=parameter', true) => 'Label')

}}}

==== Breadcrumb ====

{{{

<?php include_component('aNavigation', 'breadcrumb', array('root' => '/', 'active' => $page->slug,

'name' => 'bread')) ?>

}}}

Parameters

* root (required) - This is the root slug of the page the breadcrumb should begin with. Usually the

home page

* active (required) - This is the last element that the navigation should descend to. Usually the

current page

* separator (optional) - The separator to use between items, defaults to " > "

* name (required) - the unique name of the navigation element for your template.

=== Inserting Slots in Layouts and Templates === #slots

Of course, creating layouts and templates does you little good if you

can't insert user-edited content into them. This is where the

CMS slot helpers come in.

Here's how to insert a slot into a layout or page template:

{{{

<?php # Once at the top of the file ?>

Page 45: Apostrophe Manual

45 | P a g e

<?php use_helper('a') ?>

}}}

{{{

<?php # Anywhere you want a particular slot ?>

<?php a_slot('body', 'aRichText') ?>

}}}

Notice that two arguments are passed to the <tt>a_slot</tt>

helper. The first argument is the name of the slot, which distinguishes

it from other slots on the same page. *Slot names should contain only

characters that are allowed in HTML ID and NAME attributes*. We recommend

that you use only letters, digits, underscores and dashes in slot names.

The slot name will never be seen by the user. It is a useful label

such as `body` or `sidebar` or `subtitle`.

The second argument is the type of the slot. "Out of the box,"

apostrophePlugin offers a useful array of slot types:

* aText (plaintext)

* aRichText (allows WYSIWYG formatting)

* aFeed (brings any RSS or Atom feed into the page)

* aImage (still images from the media repository)

* aSlideshow (a series of images from the media repository)

* aButton (an image from the media repository, and an editor-configurable link)

* aVideo (video from !YouTube and other providers)

* aPDF (PDF documents from the media repository)

Page 46: Apostrophe Manual

46 | P a g e

* aRawHTML (Unfiltered HTML code)

The use of these slots is largely self-explanatory and we encourage you to play with the demo and

try them out.

You can add additional slot types of your own and release and

distribute them as plugins as explained in the developers' section in this document.

==== aText Slots ====

aText slots contain only plaintext (actually, valid HTML text, which can include valid HTML and UTF-8

entities). URLs are automatically rendered as links, email addresses are rendered as obfuscated

`mailto:` links, and newlines are rendered as line breaks.

Note that the special slot name `title` is reserved for the title of the page

and is always of the type `aText`. While you don't really need to provide an additional editing

interface for the title, you might also want to insert it elsewhere in your

page layout or template as a design element:

{{{

<?php a_slot('title', 'aText') ?>

}}}

The behavior of most slot types can be influenced by passing options to them from the template or

layout. You do this by passing an array of options as a third argument

to the helper, like this:

{{{

<?php a_slot('aboutus', 'aText', array('multiline' => true)) ?>

}}}

Page 47: Apostrophe Manual

47 | P a g e

The `multiline` option specifies that a plaintext slot should permit multiple-line text input.

==== aRichText Slots ====

Here is a simple example of a rich text slot:

{{{

<?php a_slot('ourproducts', 'aRichText') ?>

}}}

The rich text slot allows users to edit content using a rich text editor. The HTML they enter is filtered

for correctness and to ensure it does not damage your design. Read on for more information about

ways to adjust these filters.

The `tool` option is one of the most useful options for use with rich text slots:

{{{

<?php a_slot('subtitle', 'aRichText',

array('tool' => 'basic')) ?>

}}}

Here we create a subtitle rich text slot on the page which is editable, but only

with the limited palette of options provided in FCK's `basic` toolbar (bold, italic and links).

Note that you can create your own custom toolbars for the FCK rich text editor. Add these to

`web/js/fckextraconfig.js` (at the project level, not in the plugin) and they will be found

automatically. Here is an example:

{{{

Page 48: Apostrophe Manual

48 | P a g e

FCKConfig.ToolbarSets["Sidebar"] = [

['Bold','Italic']

] ;

}}}

For more complete examples of what can be included here, see

`apostrophePlugin/web/js/fckeditor/fckconfig.js`. (You do not need to, and should not, duplicate this

entire file in `fckextraconfig.js`. Just add and override things as needed.)

Other notable options to `aRichText` slots include:

* allowed-tags is a list of HTML elements to be permitted inside the rich text. By default Apostrophe

filters out most HTML elements to prevent pasted content from Microsoft Word and the like from

wrecking page layouts. You can pass an array of HTML element names (without angle brackets), or a

string like that accepted by the PHP `strip_tags` function.

This allows us to write a better version of the `subtitle` slot above that filters the HTML to make sure

it's suitable to appear in a particular context:

<?php a_slot('subtitle', 'aRichText',

array('tool' => 'basic', 'allowed-tags' => '<b><i><strong><em><a>')) ?>

Note that we list both the `b` tag and the `strong` tag here. We do this because different browsers

submit slightly different rich text.

The default list of allowed tags is:

`h3, h4, h5, h6, blockquote, p, a, ul, ol, nl, li, b, i, strong, em, strike, code, hr, br, div, table, thead,

caption, tbody, tr, th, td, pre`

Page 49: Apostrophe Manual

49 | P a g e

* allowed-attributes is a list of HTML attributes to be accepted, on a per-element basis. Unlike

`strip_tags` Apostrophe's robust, high-performance HTML filter, `aHtml::simplify`, removes all

inappropriate HTML attributes. Here is the default list of allowed attributes:

{{{

array(

"a" => array("href", "name", "target"),

"img" => array("src")

);

}}}

* allowed-styles is a list of CSS style names to be permitted, again on a per-element basis. By

default, no styles are permitted in rich text editor content. You can alter this to allow more creative

table styling, at the cost that a truly persistent user might manage to wreck the page layout. The

format is the same as that used above for allowed attributes.

If you want to change the default settings for allowed tags, attributes and styles, you may find it

more convenient to use `app.yml` for this purpose (default values are shown, '''be sure to keep

them''' unless you specifically want to disable things that are normally considered essential):

{{{

all:

aToolkit:

allowed_tags: [h3, h4, h5, h6, blockquote, p, a, ul, ol, nl, li, b, i, strong, em, strike, code, hr, br, div,

table, thead, caption, tbody, tr, th, td, pre]

allowed_attributes:

a: [ href, name, target ]

img: [ src ]

allowed_styles: ~

}}}

Page 50: Apostrophe Manual

50 | P a g e

==== aFeed Slots ====

The aFeed slot allows editors to insert an RSS feed into the page:

{{{

<?php a_slot('subtitle', 'aFeed') ?>

}}}

When the user clicks "Edit," they are invited to paste an RSS feed URL. When they click "Save," the

five most recent posts in that feed appear. The title of each feed post links to the original article on

the site of origin as specified by the feed.

You can change this behavior with the following options. The defaults are shown as examples:

* `'links' => true` determines whether links to the original posts are provided.

* `'dateFormat' => 'Y m d'` lets you specify your own date and time format (see the PHP date()

function). 'Y m d' is not actually our default, as our default is more subtle than can be output with

the date function alone (we include the year only when necessary and so on). Our default is very

nice, but very American, so feel free to override it.

* `'markup' => '<strong><em><p><br><ul><li>'` changes the list of HTML elements we let through in

markup. We apply our usual aHtml::simplify() method, which is smarter than PHP's `strip_tags`

alone.

* `'interval' => 300` specifies how long we hold on to a feed before we fetch it again. This is

important to avoid slowing down your site or getting banned by feed hosts. We rock Symfony's

caching classes to implement this. Check out aFeed::getCachedFeed if you're into that sort of thing.

* `'posts' => 5` determines how many posts are shown.

==== aImage Slots ====

Image slots are used to insert single still images from Apostrophe's built-in media repository:

{{{

<?php a_slot('landscape', 'aImage') ?>

Page 51: Apostrophe Manual

51 | P a g e

}}}

Options are available to control many aspects of image slots:

* `'width' => 440` determines the width of the image, in pixels. Images are never rendered larger

than actual size as upsampling always looks awful.

* `'height' => 330` determines the height of the image, in pixels. It is ignored if `flexHeight' => true is

also present.

* `'resizeType' => 's'` determines the scaling and cropping style. The `s` style scales the image down

if needed but never changes the aspect ratio, so the image is surrounded with white bars if

necessary (see `flexHeight` for a way to avoid this). The `c` style crops the largest part of the center

of the image that matches the requested aspect ratio.

* `'flexHeight' => false` determines whether the height of the image is scaled along with the

requested width to maintain the aspect ratio of the original. Set this option to `true` to scale the

height.

* `'defaultImage' => false` allows you to specify the URL of an image to be displayed if no image has

been selected by the editor yet. Otherwise no image is displayed until an editor chooses one.

* `'title' => false` specifies whether the title associated with the image in the media repository

should be shown on the page. CSS can be used to display this title in a variety of interesting ways.

* `'description' => false` specifies whether the rich text description associated with the image in the

media repository should be shown on the page.

* `'link' => false` specifies a URL that the image should link to when clicked upon. Note that in most

cases you probably want to use an aButton slot for this sort of thing.

These options suffice for most purposes. There is one more option, `constraints`, which takes an

array of constraints that can be used to limit which images the user can select from the media

repository. By default no constraints are applied. The possible constraints are:

* 'aspect-width' and 'aspect-height' specify an aspect ratio. The media browser will show only media

that match this aspect ratio. Both must be specified. For instance:

{{{

'constraints' => array('aspect-width' => 4, 'aspect-height' => 3)

Page 52: Apostrophe Manual

52 | P a g e

}}}

* 'minimum-width' and 'minimum-height' specify minimum width and height in pixels. Only media

meeting these minimum criteria will be shown. You are not required to use both options although it

usually makes sense to specify both.

* 'width' and 'height' specify that only images of that '''exact''' width and/or height should be

selectable for this image slot. It is not mandatory to specify both, and if you are using `flexHeight`

you might not want to specify a height constraint.

Here are three examples:

This slot will render the image 400 pixels wide. The original must be at least 400 pixels wide. The

height will scale along with the width.

{{{

<?php a_slot('landscape', 'aImage', array('width' => 400, 'flexHeight' => true, 'constraints' =>

array('minimum-width' => 400))) ?>

}}}

This slot will be render images at 640x480 pixels. The original must be at least 640 pixels wide. The

aspect ratio must be 4x3.

{{{

<?php a_slot('landscape', 'aImage', array('width' => 640, 'height' => 480, 'constraints' =>

array('minimum-width' => 640, 'aspect-width' => 4, 'aspect-height' => 3))) ?>

}}}

This slot will crop the largest portion of the center of the selected image with a 400x200 aspect ratio

and scale it to 400x200 pixels. The selected image must be at least 400 pixels wide.

Page 53: Apostrophe Manual

53 | P a g e

{{{

<?php a_slot('landscape', 'aImage', array('width' => 400, 'height' => 200, 'resizeType' => 'c',

'constraints' => array('minimum-width' => 400))) ?>

}}}

==== aSlideshow Slots ====

Slideshow slots allow editors to select one or more images and display them as a slideshow. The

exact behavior of the slideshow can be controlled by various options.

Here is a simple example:

{{{

<?php a_slot('travel', 'aSlideshow') ?>

}}}

By default users can select any images they wish and add them to the slideshow, and the slideshow

will advance when they click on an image. Left and right arrows to move back and forward in the

slideshow appear above the image unless turned off by the `arrows` option.

Slideshows with images of varying dimensions can be confusing to look at unless the aspect ratio is

the same. Fortunately slideshows support all of the sizing, cropping and constraint options that are

available for the aImage slot (see above). We recommend either using `resizeType => 'c'` to crop to a

consistent size (and therefore aspect ratio) or using the `aspect-width` and `aspect-height`

parameters to the `constraints` option to ensure a consistent aspect ratio.

'''In addition to all of the options supported by the aImage slot''' (see above), slideshow slots support

the following additional options:

* `'random' => false` determines whether the slideshow is shown in the order selected by the editor

or in a random order. Set this option to `true` for a random order.

Page 54: Apostrophe Manual

54 | P a g e

* `'arrows' => true` indicates that arrows to move forward and backward in the slideshow should

appear above the image. By default they do appear. You can turn this off by passing `false`.

* `'interval' => false` sets the interval for automatic advance to the next image in the slideshow. By

default the slideshow does not advance automatically. For automatic advance every ten seconds

pass `'interval' => 10`. Slideshows automatically repeat once they have advanced to the final image.

If the user clicks on the slideshow, auto-advance stops, and the user can navigate manually by

clicking to advance or using the arrows if present.

* `'credit' => false` determines whether the credit field from the media repository is shown with

each image.

* `'itemTemplate' => slideshowItem` allows you to specify a different slideshow item template at

the project level. The default template is bundled with the slot and is named `_slideshowItem.php`.

==== aButton Slots ====

Button slots are similar to image slots. However, they also allow the editor to specify a target URL

and a plaintext title. When they click on the button, they are taken to the URL.

Here is an example:

{{{

<?php a_slot('logo', 'aButton') ?>

}}}

Button slots support all of the options supported by the aImage slot (see above). In particular you

might want to specify a default image.

If the title and/or description options are turned on, they are rendered as links to the button's

destination URL.

Page 55: Apostrophe Manual

55 | P a g e

==== aVideo Slots ====

The aVideo slot allows video to be embedded in a page.

Apostrophe's media repository does not store video directly. Instead Apostrophe manages videos

hosted on external services such as !YouTube, Viddler and Vimeo. !YouTube is tightly integrated into

Apostrophe, so editors can search !YouTube directly from the repository in order to easily add

videos. Embed codes for other video services can also be added to the media repository. Either way,

Apostrophe wrangles the necessary embed codes and ensures that they render at the desired size.

Here is an example:

{{{

<?php a_slot('screencast', 'aVideo') ?>

}}}

Note that you can disable support for embed codes from non-!YouTube services if you wish:

{{{

all:

aMedia:

embed_codes: false

}}}

Note that the plugin itself ships with this feature disabled by default, but the `app.yml` file of our

sandbox project does turn it on.

''' The video slot supports all of the options supported by the image slot''' (described above), with

the following differences and exceptions:

Page 56: Apostrophe Manual

56 | P a g e

* The `width` option defaults to 320 pixels.

* The `height` option defaults to 240 pixels.

* The `resizeType` option is ignored. As a general rule video hosting services do not permit cropping

and choose their own approaches to letterboxing. We recommend using the `flexHeight` option if

there is any doubt about the dimensions of the video.

* The `constraints` option is supported. The constraints will be applied based on the dimensions of

the original video (if from !YouTube). For videos from other hosting services (pasted as embed codes

in the media repository) constraints are based on the dimensions of the thumbnail manually

uploaded by the editor.

* The `link` and `defaultImage` options are not supported.

==== aPDF Slots ====

The aPDF slot allows PDF documents to be embedded in a page. If ghostscript and netpbm are

installed on the server, thumbnail previews of the first page are automatically displayed in PDF slots,

and clicking on them launches the PDF. If they are not available, a clickable PDF icon is still displayed

as a way of launching the PDF.

aPDF slots support the following options, which behave as they do for the aImage slot (described

above):

* The `width` option defaults to 170 pixels.

* The `height` option defaults to 220 pixels.

* The `flexHeight` option is available to scale the thumbnail based on the page size, although this is

only relevant if netpbm and ghostscript are available.

* The `defaultImage` option is available. The default image appears when no PDF has been selected

yet.

* The `constraints` option is available, and can be used to select only PDFs with a certain aspect

ratio, such as 8.5x11 (use the aspect-width and aspect-height parameters). This option should not be

used when netpbm and ghostscript are not available as the true size of the PDF is not known in this

case.

==== aRawHTML Slots: When You Must Have Raw HTML ====

Page 57: Apostrophe Manual

57 | P a g e

Honestly, we're not big fans of raw HTML slots. Editors tend to paste code that breaks page layouts

on a fairly regular basis. That's why our rich text slots filter it so carefully.

However there are times when you must have the embed code for a mailing list signup form service

or similar third-party website feature.

In these situations the aRawHTML slot is useful.

Here is an example:

{{{

<?php a_slot('mailinglist', 'aRawHTML') ?>

}}}

This slot displays a multiline text entry form where the editor can paste in HTML code directly. This

code is not validated in any way, so you should think carefully before offering this feature to less

experienced editors.

There is a way to recover if you paste bad HTML that breaks the page layout. Visit the page with the

following appended to the URL:

{{{

?safemode=1

}}}

When this parameter is present in the URL, raw HTML slots will escape their contents so that you see

the source code, safely defanged. You can then click "Edit" and make corrections as needed.

That's it for the standard slots included with Apostrophe! Of course there will be more. And you can

add more yourself. See the developer's section of this document.

Page 58: Apostrophe Manual

58 | P a g e

=== Slot Variants: More Mileage From Your Templates === #variants

It's possible to specify different options to each slot in different page templates. And for many

designs this is the way to go.

But sometimes you'd like to have a more flexible setup in which the same template or layout can

serve the needs of more pages.

One solution would be to let users specify slot options or even CSS classes directly, but this has a

tendency to be confusing. So we've provided a better solution: slot variants.

If you define variants like this in `app.yml`:

{{{

slot_variants:

aSlideshow:

normal:

label: Normal

options:

interval: 0

title: false

arrows: true

compact:

label: Compact

options:

interval: 0

title: true

arrows: true

itemTemplate: slideshowItemCompact

autoplay:

Page 59: Apostrophe Manual

59 | P a g e

label: Auto Play

options:

interval: 4

title: true

arrows: false

itemTemplate: slideshowItemCompact

}}}

And you have allowed these variants, either by not specifying the `app_a_allowed_slot_variants`

option at all (note that it is set but empty in our sandbox project) or by setting it up as follows:

{{{

allowed_slot_variants:

aSlideshow:

- normal

- compact

- autoplay

}}}

Then the user will be presented with a choice of these three variations on the slideshow slot in an

"Options" menu to the right of the "Choose Images" button, provided that we have actually allowed

those variants.

How does this work? Each variant has a label and an optional set of options. When the user switches

between variants, the slot is refreshed with the new options in effect.

Sometimes you will want to define variants on a slot that are only suitable for use in a particular

place. For instance, you wouldn't want to allow an "extra-wide" image slot in a sidebar. Beginning in

version 1.04, you can address this by passing the `allowed_variants` option when inserting an area or

slot:

Page 60: Apostrophe Manual

60 | P a g e

<?php a_slot('aImage', array('allowed_variants' => array('narrow'))) ?>

Note that this means that you can lock a slot down to a single variant that would not otherwise be

the default. If you allow only one variant, the options menu does not appear, since there are no

other choices.

You can do the same thing for an area by adding `allowed_variants` to the `type_options` for the slot

type in question.

'''Prior to version 1.4.1/svn commit 1569 the `app_a_allowed_slot_variants` option did not exist''', so

it was necessary to specifically allow only the appropriate variants in every `a_slot` or `a_area` call if

any of the variants were template-specific and a poor choice for general use. This new option makes

it easy to set up a more restrictive list of variants that are allowed when `allowed_variants` is not

specified at the slot level.

'''In addition to changing options, variants also set a CSS class''' on the slot's outermost container.

Specifically, when the `compact` option is in use, the CSS class `compact` will be set on that slot's

outermost container. This is very useful for styling purposes.

'''Prior to version 1.04 slot variant options must fully contradict each other''' for consistent results.

That is, if you set the `interval` option for one variant, you must set a value for it for every variant

(even if that value is false). This requirement is removed in version 1.04.

What about newly added slots? If there are variants for a slot, the first variant in the list is used for

new slots of that type (in areas) or for slots whose variant has never been set (for standalone slots).

If an `allowed_variants` option is present for a particular slot or area, or there is a global

`app_a_allowed_slot_variants` option, then the first allowed variant is used.

=== Inserting Areas: Unlimited Slots in a Vertical Column === #areas

Slots are great on their own. But when you want to mix paragraphs of text with

elements inserted by custom slots, it is necessary to create a separate

Page 61: Apostrophe Manual

61 | P a g e

template file for every page. This is tedious and requires the

involvement of an HTML-savvy person on a regular basis.

Fortunately apostrophePlugin also offers "areas." An

area is a continuous vertical column containing multiple slots which

can be managed on the fly without the need for template changes.

You insert an area by calling a_area($name) rather than

a_slot($name):

{{{

<?php a_area("sidebar") ?>

}}}

When you insert an area you are presented with a slightly different

editing interface. At first there are no editable slots in the area.

Click "Insert Slot" to add the first one. You can now edit that

first slot and save it.

Add more slots and you'll find that you are also able to delete them

and reorder them at will.

By default new slots appear at the top of an area. If you don't like this,

you can change it for your entire site via `app.yml`:

{{{

all:

Page 62: Apostrophe Manual

62 | P a g e

a:

new_slots_top: false

}}}

An area has just one version control button for the entire area. This

is because creating, deleting, and reordering slots are themselves

actions that can be undone through version control.

In a project with many custom slot types, you may find it is

inappropriate to use certain slot types in certain areas. You can

specify a list of allowed slot types like this:

{{{

<?php a_area("sidebar",

array("allowed_types" => array("aText", "myCustomType"))) ?>

}}}

Notice that the second argument to `a_area` is an

associative array of options. The `allowed_types` option allows us to

specify a list of slot types that are allowed in this particular area.

In addition, you can pass options to the slots of each type, much as

you would when inserting a single slot:

{{{

a_area("sidebar",

array("allowed_types" => array("aText", "myCustomType"),

Page 63: Apostrophe Manual

63 | P a g e

"type_options" => array(

"aText" => array("multiline" => 1))));

}}}

Here the `multiline` option specifies that all

`aText` slots in the area should have the

`multiline` option set.

=== Global Slots and Virtual Pages ===

Most of the time, you want the content of a slot to be specific to a page.

After all, if the content was the same on every page, you wouldn't need

more than one page.

However, it is sometimes useful to have editable content that appears

on more than one page. For instance, an editable page footer or page

subtitle might be consistent throughout the site, or at least throughout

a portion of the site.

The quickest way to do this is by adding a "global slot" to your page template or layout.

Just set the `global` option to `true` when inserting the slot:

{{{

<?php a_slot('footer', 'aRichText',

array('toolbar' => 'basic', 'global' => true)) ?>

}}}

The content of the resulting slot is shared by all pages that include

Page 64: Apostrophe Manual

64 | P a g e

it with the `global` option.

Note that you can use the `global` flag with areas as well as slots:

{{{

<?php a_area('footer', array(

'allowed_types' => array('aRichText', 'aImage'),

'global' => true

)) ?>

}}}

By default, global slots can be edited only by users with editing

privileges throughout the site. Otherwise users with control only over

a subpage could edit a footer displayed on all pages. See below for more information about

how to override this rule where appropriate.

==== Virtual Pages: When Global Slots Are Not Enough ====

Global slots will do the job for most situations that front end developers will encounter. If you're not

a PHP developer looking to use slots and areas to manage dynamic content related to your own

code, it's probably safe to skip this section, although it is sometimes useful to apply these techniques

if you would otherwise have hundreds of global slots in your design.

Conceptually, all global slots reside together on a virtual page with the slug `global`. Since there is no

leading `/`, this page can never be navigated to. The `global` virtual page resides outside of the site's

organizational tree and is used only as a storehouse of shared content.

For headers and footers, this works very well. But if you have a larger amount of shared content, the

model begins to break down. Fortunately there's a solution: group your content into separate virtual

pages.

Page 65: Apostrophe Manual

65 | P a g e

When you include a slot this way:

{{{

<?php a_slot('biography', 'aRichText', array('toolbar' => 'basic', 'slug' => "bio-$id")) ?>

}}}

You are fetching that slot from a separate virtual page with the slug `bio-$id`, where `$id` might

identify a particular user in the `sfGuardUser` table.

Apostrophe will automatically create the needed virtual page object in the database the first time

the slot is used. This technique can be used for areas as well.

But why is this better than using `'global' => true`? Two reasons: performance and access control.

==== Performance, Global Slots and Virtual Pages ====

Yes, you could manage dynamic content like biographies with just the global flag, if you chose

dynamically generated names for your slots and areas. But since Apostrophe loads all current global

slots into memory the first time a global slot is requested on a page, the CMS would be forced to

load all of your biographies on just about every page of the site. That's clearly not acceptable. For

that reason it's important to use a separate virtual page for each individual's biography, etc.

As a rule of thumb, add database IDs to virtual page slugs, not area or slot names.

==== Access Control For Global Slots and Virtual Pages ====

By default, only sitewide admins can edit slots included from other virtual pages. This is fine for

headers and footers seen throughout the site, but doesn't work well for biography slots. In other

words, users should be able to write their own autobiographies.

You can address this problem by passing `'edit' => true` as an option to the slot or area. This

overrides the normal slot editing privilege checks, so you should do this only if the user ought to

have the privilege according to your own judgment. For instance, you might do something like this:

Page 66: Apostrophe Manual

66 | P a g e

{{{

<?php $myid = sfContext::getInstance()->getUser()->getGuardUser()->id ?>

<?php a_slot('biography', 'aRichText', array('toolbar' => 'basic', 'slug' => "bio-$id", 'edit' => $id ===

$myid)) ?>

}}}

Once again, you can do this with areas as well. Also note that setting 'edit' => false will also disable

editing of the slot or areas for admin users as well, this is the behavior in the trunk and will be

present in 1.1

==== Including Slots From Other "Normal" Pages ====

It's possible to use the `slug` option to include a slot from another normal, navigable page on the

site. However, if you do so, keep in mind that you don't want to create a situation where the same

slot is included twice on the page itself.

Also keep in mind that normal pages can be moved around on the site, which will break templates

that contain explicit slugs pointing at their old locations. You can avoid this by determining the slug

option dynamically, or by using a virtual page instead (no leading / on the slug).

=== CSS: Styling Apostrophe ===

By default, `apostrophePlugin` provides two stylesheets which are

automatically added to your pages. There's a lot happening there,

particularly with regard to the editing interface, and we recommend

that you keep these and override them as needed in a separate

stylesheet of your own. However, you can turn them off if you wish

in `app.yml`:

{{{

all:

Page 67: Apostrophe Manual

67 | P a g e

a:

use_bundled_stylesheet: false

}}}

If you do keep our stylesheets and further override them, you'll

want to specify `position: last` for the stylesheets that should

override them. This is automatically done for the stylesheet

`web/css/main.css` in our sandbox project, and we recommend you

make your customizations there.

==== The Golden Rule: Use Classes, Not IDs ====

You may want to style individual areas and slots without introducing

wrapper divs to your templates. To do that, pay attention to the CSS

classes we output on the outermost wrappers of each area. You'll note

that standalone slots still have an area wrapper in order to implement

their editing controls and make it easier to write consistent CSS:

{{{

<div id="a-area-12-body" class="a-area a-area-body">

}}}

You may be tempted to use the `id` attribute. ''Don't do that.'' The id attribute contains the page ID,

which differs from page to page and should never be used in CSS.

Instead, take advantage of the classes on this div:

{{{

.a-area-body

Page 68: Apostrophe Manual

68 | P a g e

{

css rules specific to the area or slot named 'body'

}

}}}

Note that if you need different behavior on different pages in a way that can't be achieved by adding

different slots and using different variants of each slot, you should use a separate template for each

type of page.

==== Why So Many Wrappers? ====

The wrapper divs output by Apostrophe are necessary to implement inline editing. We've found over

time that it's best to keep them in place even when the page is rendered in a non-editing mode in

order to allow a single set of CSS to render the page consistently.

We recommend embracing our wrapper divs and styling them to suit your needs rather than

attempting to remove or replace them or put unnecessary wrappers around them. See the provided

`a/templates/defaultTemplate.php` for an example of how to float one to the left of another. The

use of CSS floating makes it straightforward to lay slots and areas out as you see fit.

==== Styling Slot Variants ====

Slot variants (choices on the "Options" menu for a given slot) also set CSS classes. These classes have

the same name as the option. This is very useful for styling purposes, especially if you wish to display

the rich text description of a media image to the right or left of the image itself. See "Slot Variants:

More Mileage From Your Templates" for details.

=== CSS: Apostrophe UI ===

Apostrophe UI colors can be easily changed by overriding a small set of CSS styles bundled with the

plugin.

Located at the bottom of a.css you will find this css:

Page 69: Apostrophe Manual

69 | P a g e

{{{

/* 34. aUI & Admin Colors - Default

-------------------------------------*/

.a-btn,

.a-submit,

.a-cancel,

#a-global-toolbar #the-apostrophe

{ /* Apostrophe */

background-color: rgb(255,150,0);

background-color: rgba(255,150,0,0.75);

}

.a-btn.alt,

.a-submit.alt,

.a-cancel.alt,

.a-history-browser,

ul.a-controls .a-variant-options

{ /* Border Color */

border-color: rgb(255,150,0);

}

.a-admin .a-admin-content a:link,

.a-admin .a-admin-content a:visited,

#a-global-toolbar #a-logged-in-as span,

#a-personal-settings-heading span

{ /* Text Color */

Page 70: Apostrophe Manual

70 | P a g e

color: rgb(255,150,0);

}

}}}

You can simply copy this section from a.css into your site's CSS file and change the values to colors

that work for your design.

==== aUI() ====

There is a javascript function `aUI();` that decorates the buttons in a cross-browser compatible way

on Dom Ready. It lives in `aUI.js`. By default it touches every button the page and applies functional

and aesthetic changes to the buttons. The `aUI()` call can be scoped using a CSS selector or a jQuery

object by simply passing it into the function call.

`aUI('.a-area-body');` will only affect buttons that are children of this CSS selector. We use this for

updating buttons that are returned via AJAX, missing out on the Dom Ready call.

==== aOverrides() ====

This function is hooked into `aUI()` To use it, define the function `aOverrides()` in a newly created a

project level `site.js` file. If it exists, `aUI()` will call it whenever `aUI()` is executed. This is helpful

when working with technologies such as `Cufon` that need to be run on Dom Ready.

=== CSS: Apostrophe Buttons === #abtn

'''.a-btn'''

* The base class for all buttons in the Apostrophe UI. When used alone, it will create

a simple button out of an any element.

* examples

* ''<A href="" class="a-btn">..text..</A>''

* ''<INPUT type="SUBMIT" value="..text.." class="a-btn"/>''

Page 71: Apostrophe Manual

71 | P a g e

* ''<LI class="a-btn">...text...</LI>''

* '''.icon'''

* Alters the button to make space for a sprite on the left side

* '''.a-icon-name'''

* A second class is necessary for the .icon class to make sense. A

sprite is defined by the icon name class in the a.css file

* example

* CSS

* ''.a-edit { background-image:

url(/apostrophePlugin/images/a-icon-edit.png); }''

* HTML

* ''<A href="" class="a-btn icon a-edit">..text..</A>''

* '''.alt'''

* chooses the alternate CSS sprite and button color scheme for the button.

* some site designs work better with white icons instead of black and using

the .alt class in some situations helps improve the buttons legibility on a case-by-case basis. ''Note:

The .alt class can be applied to the <BODY> to globally change all buttons across the site to use the

alternate sprite.''

* '''.nobg'''

* removes background and border

* example

* ''<A href="" class="a-btn nobg">..text..</A>''

* '''.no-label'''

* Hides the text label of the button

* '''.mini'''

* Small button style with 10px type size

* '''.big'''

* Large button style with 18px type size

* example

Page 72: Apostrophe Manual

72 | P a g e

* ''<A href="" class="a-btn big">..text..</A>''

* '''.flag'''

* Hides the text label and displays it upon :hover

* '''.flag-left'''

* Hides the text label and displays it as a tooltip to the left of the

button

* '''.flag-right'''

* Hides the text label and displays it as a tooltip to the right of the

button

* example

* ''<A href="" class="a-btn flag flag-right">..text..</A>''

=== Access Control: Who Can Edit What? ===

By default, an unconfigured Apostrophe site that was not copied from our sandbox follows these

security rules:

* Anyone can view any page without being authenticated.

* Any authenticated (logged-in) user can edit any page,

and add and delete pages.

This is often sufficient for simple sites. But Apostrophe can

also handle more complex security needs.

=== Requiring Login to Access All Pages ===

To require that the user log in before they view any page

in the CMS, use the following setting in `app.yml`:

Page 73: Apostrophe Manual

73 | P a g e

{{{

all:

a:

view_login_required: true

}}}

=== Requiring Login to Access Some Pages ===

To require the user to log in before accessing a particular page,

just navigate to that page as a user with editing privileges

and click on the "lock" icon.

By default, locked pages are only accessible to logged-in users. Of course, on some sites this is too

permissive, especially when users are allowed to create their own accounts without further

approval. In such situations you can set up different credentials to access the pages. To require the

`view_locked` credential to view locked pages, use the following app.yml setting:

{{{

all:

a:

view_locked_sufficient_credentials: view_locked

}}}

Then grant the `view_locked` permission to the appropriate sfGuard groups, and you'll be able to

distinguish user-created accounts from invited guests.

=== Requiring Special Credentials to Edit Pages ===

Editing rights can be controlled in several ways. It may seem a bit confusing, so keep in mind that the

default set of permissions, groups and `app.yml` settings in our sandbox project works well and

allows the admin to assign editing and managing privileges anywhere in the CMS page tree as they

see fit. Editing privileges allow users to edit a page, while managing privileges allow them to also

create and delete pages.

Page 74: Apostrophe Manual

74 | P a g e

Read on if you believe you may need to override this configuration. You will need to do that if you

are not using our sandbox project as a starting point, as the default behavior of the plugin is to allow

any logged-in user to edit as they see fit (often quite adequate for small sites without unprivileged

user accounts).

Editing and managing privileges are granted as follows:

1) Any user with the `cms_admin` credential can always

carry out any action in the CMS, regardless of all other settings. Note that the

sfGuard "superadmin" user always has all credentials.

2) Any user with `edit_sufficient_credentials` can always edit

pages (but not necessarily add or delete them) anywhere on the site. For instance,

if you add such users to the `executive_editors` sfGuardGroup and grant that

group the `edit` permission, then you can give them

full editing privileges with these settings:

{{{

all:

a:

edit_sufficient_credentials: edit

}}}

''If you do not specify any editing credentials at all, then any logged-in user can edit anywhere.'' This

is useful for small sites.

Similarly, any user with `manage_sufficient_credentials` can always

add or delete pages anywhere on the site, in addition to editing content. So

Page 75: Apostrophe Manual

75 | P a g e

complete settings might be:

{{{

all:

a:

edit_sufficient_credentials: edit

manage_sufficient_credentials: manage

}}}

''Note that if you do not specify `manage_sufficient_credentials` any logged-in user can manage

pages anywhere.''

For convenience, you can also grant these privileges directly via a group name:

{{{

all:

a:

edit_sufficient_group: executive_editors

manage_sufficient_group: executive_editors

}}}

3) Any user who is a member of the group specified by

`app_a_edit_candidate_group` can ''potentially'' be made an

editor in particular parts of the site. If

`app_a_edit_group` is not set, all logged-in users are potential editors.

{{{

all:

Page 76: Apostrophe Manual

76 | P a g e

a:

edit_candidate_group: editors

}}}

Similarly, any user who is a member of the group specified by

`app_a_manage_candidate_group` can potentially be given the ability to add

and delete pages in a particular part of the site. So a

common setup might be:

{{{

all:

a:

edit_candidate_group: editors

manage_candidate_group: editors

}}}

Why is this feature useful? Two reasons: because checking their membership

in one group is faster than checking their access privileges in the

entire chain of ancestor pages, and because when an administrator is

managing the list of users permitted to edit a page the list of users in the

editors group is much easier to read than a list of all users (especially

in a large system with many non-editing users).

4) Editing privileges for any specific page and its descendants

can be granted to any member of the group specified by

`app_a_edit_candidate_group` (if that option is set), or to

any user if `app_a_edit_candidate_group` is not set.

Page 77: Apostrophe Manual

77 | P a g e

When a user with the right to manage a page opens the page settings,

they are given the option of assigning editors for that page.

The same principle applies to "managing" (adding and deleting)

pages, with the candidate group being indicated by

your `app_a_manage_candidate_group` setting.

Note that the pulldown list of possible editors can be quite long

if there are thousands of people with accounts on your site! This

is why we recommend setting up groups as described above.

=== Publishing Pages, by Choice and By Default ===

Apostrophe offers a "published/unpublished" toggle under "manage page settings."

Pages that are unpublished are completely invisible to users who do not

have at least the candidate credentials to be an editor; a user without appropriate privileges

gets a 404 not found error just as if the page did not exist. In most cases

you should use this in preference to actually deleting the page because the

content is still available if you choose to bring it back later.

By default all new a pages are in the "published" state.

If you need to approach the matter more conservatively, you can easily

change this with the following `app.yml` setting:

{{{

all:

a:

default_on: false

}}}

Page 78: Apostrophe Manual

78 | P a g e

=== Limiting The Depth of the Page Tree ===

By default, editors can create pages nested as deeply as they wish. This makes sense for a site built

by a few skilled people, but when there are many cooks in the kitchen you may want to impose

some discipline. You can do that in `app.yml` by setting the `app_a_max_page_levels` option:

{{{

all:

a:

# Allows tabs, grandchildren, and great-grandchildren

max_page_levels: 3

}}}

A setting of `3` allows tabs, grandchildren (children of tabs), and great-grandchildren. This is often a

good choice. For a very simple site with no breadcrumb trail in the layout, you might want to use

`max_page_levels: 1`, which only permits tabs, or `max_page_levels: 2`, which only permits tabs and

grandchildren.

=== Limiting the Number of Children of Any One Page ===

Similarly, you can limit the number of child pages that any given page can have. This helps to avoid

unwieldy side navigation and impose a bit more structure.

Set the `max_children_per_page` option to do this:

{{{

all:

a:

# No page, including the home page, may have more than 8 direct children

max_children_per_page: 8

}}}

Page 79: Apostrophe Manual

79 | P a g e

Note that only the immediate children of a page are counted against this limit.

=== Deferring Search Engine Updates ===

By default pages are reindexed for search purposes at the time

edits are made. This makes installation simple, but for performance reasons

you might be happier deferring this to a cron job that runs every few minutes. If you want to take

this

approach, set up a cron job like this:

{{{

0,10,20,30,40,50 '' '' '' '' /path/to/your/project/symfony apostrophe:update-search-index --

env=env

}}}

Note the `--env` option. There is a separate index for each

environment. On a development workstation, specify `--env=env`. In

a production environment you might specify `--env=prod`.

Then turn on the feature in app.yml:

{{{

all:

a:

defer_search_updates: true

}}}

This speeds up editing a bit. But if you don't like cron, you don't have to enable it.

Page 80: Apostrophe Manual

80 | P a g e

You can also change the word count of search summaries:

{{{

all:

a:

search_summary_wordcount: 50

}}}

=== Rebuilding the Search Index ===

When you deploy from development to production, you will need to build a new search engine index

for production. This is normally a one-time operation:

{{{

./symfony apostrophe:rebuild-search-index --env=prod

}}}

If you choose to sync your content to or from staging and production servers with

sfSyncContentPlugin, you'll need to run this task with the appropriate environment parameter for

the host in question after syncing content to it.

=== Conclusion ===

That's it for the front end designer oriented section of the manual. Next we'll move on to

information for PHP developers who want to extend Apostrophe with new capabilities and integrate

it more tightly into their own websites.

[ManualDevelopersGuide Continue to Developer's Guide]

Page 81: Apostrophe Manual

81 | P a g e

[ManualOverview Up to Overview]

Page 82: Apostrophe Manual

82 | P a g e

= Apostrophe Manual =

[ManualOverview 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 [http://www.symfony-

project.org/jobeet/1_4/Doctrine/en/ Symfony tutorial] before attempting to add new features to

Apostrophe. We'll assume familiarity with Symfony development in this section.

=== 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:

Page 83: Apostrophe Manual

83 | P a g e

{{{

./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:

{{{

all:

a:

slot_types:

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.

Page 84: Apostrophe Manual

84 | P a g e

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()

{

Page 85: Apostrophe Manual

85 | P a g e

// Must be at the start of both view components

$this->setup();

// 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:

{{{

Page 86: Apostrophe Manual

86 | P a g e

<?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.

==== 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;

parent::__construct();

$this->setDefaults($defaults);

}

public function configure()

{

$this->setWidgets(array('url' => new sfWidgetFormInputText(array('label' => 'RSS Feed URL'))));

Page 87: Apostrophe Manual

87 | P a g e

$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]');

$this->widgetSchema->setFormFormatterName('aAdmin');

}

}

}}}

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)

{

Page 88: Apostrophe Manual

88 | P a g e

$this->editSetup();

$value = $this->getRequestParameter('slotform-' . $this->id);

$this->form = new aFeedForm($this->id, array());

$this->form->bind($value);

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

$this->slot->setArrayValue($this->form->getValues());

return $this->editSave();

}

else

{

// 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.

Page 89: Apostrophe Manual

89 | P a g e

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.

Page 90: Apostrophe Manual

90 | P a g e

==== 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') . "?" .

http_build_query(

array(

"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 ====

Page 91: Apostrophe Manual

91 | P a g e

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:

{{{

aFeedSlot:

# 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

options:

symfony:

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

Page 92: Apostrophe Manual

92 | P a g e

# a single table

inheritance:

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

Page 93: Apostrophe Manual

93 | P a g e

`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:

{{{

all:

a:

extra_admin_buttons:

users:

label: Users

action: 'aUserAdmin/index'

class: 'a-users'

reorganize:

label: Reorganize

action: a/reorganize

class: a-reorganize

Page 94: Apostrophe Manual

94 | P a g e

}}}

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`:

{{{

all:

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.

Page 95: Apostrophe Manual

95 | P a g e

==== 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()

{

aTools::addGlobalButtons(array(

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,

Page 96: Apostrophe Manual

96 | P a g e

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'))

{

aTools::addGlobalButtons(array(

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.

Page 97: Apostrophe Manual

97 | P a g e

$this->dispatcher->connect('a.getGlobalButtons',

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.

Page 98: Apostrophe Manual

98 | P a g e

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()

{

aEngineTools::preExecute($this);

parent::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`:

{{{

Page 99: Apostrophe Manual

99 | P a g e

# Engine rules must precede any catch-all rules

enginetest_index:

url: /

param: { module: enginetest, action: index }

class: aRoute

enginetest_foo:

url: /foo

param: { module: enginetest, action: foo }

class: aRoute

enginetest_bar:

url: /bar

param: { module: enginetest, action: bar }

class: aRoute

enginetest_baz:

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:

{{{

enginetest_action:

url: /:action

param: { module: enginetest }

class: aRoute

Page 100: Apostrophe Manual

100 | P a g e

}}}

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

aDoctrineRoute:

{{{

a_event_show:

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`:

{{{

department:

class: aDoctrineRouteCollection

options:

model: Department

module: department

prefix_path: ''

column: id

with_wildcard_routes: true

}}}

Page 101: Apostrophe Manual

101 | P a g e

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:

{{{

/test1

}}}

And the user requests the following URL:

/test1/foo

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:

{{{

/foo

}}}

To the appropriate rule.

Page 102: Apostrophe Manual

102 | P a g e

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 the following to `app.yml`:

{{{

all:

a:

engines:

'': 'Template-Based'

enginetest: 'Engine Test'

}}}

Substitute the name of your module for `enginetest`. Be sure to keep the "template-based" entry in

place, as otherwise normal CMS pages are not permitted on your site.

Page 103: Apostrophe Manual

103 | P a g e

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.

Page 104: Apostrophe Manual

104 | P a g e

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') ?>

}}}

Page 105: Apostrophe Manual

105 | P a g e

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 `push`ing 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.

Page 106: Apostrophe Manual

106 | P a g e

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->useFields();

$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)));

Page 107: Apostrophe Manual

107 | P a g e

$this->widgetSchema->setNameFormat('enginesettings[%s]');

$this->widgetSchema->setFormFormatterName('aAdmin');

$this->widgetSchema->getFormFormatter()->setTranslationCatalogue('apostrophe');

}

}

}}}

The corresponding schema is:

{{{

aMediaCategory:

tableName: a_media_category

actAs:

Timestampable: ~

Sluggable: ~

columns:

id:

type: integer(4)

primary: true

autoincrement: true

name:

type: string(255)

unique: true

description:

type: string

relations:

Page 108: Apostrophe Manual

108 | P a g e

MediaItems:

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

Pages:

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.

Page 109: Apostrophe Manual

109 | P a g e

You can create your own subnavigation within your engine page. We suggest overriding appropriate

portions of your page layout via Symfony slots.

=== Internationalization ===

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.

Page 110: Apostrophe Manual

110 | P a g e

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

template that displays the results (a/searchSuccess), and bring in more results for

other types of data via a Symfony component of your own. You can grab

the search query string from $sf_data->getRaw('q').

This is Zend Lucene search, so Lucene syntax is allowed:

http://framework.zend.com/manual/en/zend.search.lucene.query-language.html

You can use Lucene to index your own data and take advantage of that

fully. Keep in mind that the entire Zend library is available to you

already since it's one of Apostrophe's requirements.

Following this approach you don't need to know much about Apostrophe's

internals at all.

Page 111: Apostrophe Manual

111 | P a g e

The second supported approach is 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).

apostropheBlogPlugin uses this 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:

{{{

@a_blog_search_redirect?id=50

}}}

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.

The third supported way is to interleave your own results with the

page search results, based on their search ranking. This makes sense

only if the results are somewhat reasonable to compare - for instance, articles and web pages are

reasonably similar and Zend will probably

produce search rankings that mix reasonably well.

Fortunately, Apostrophe's a/search action is designed to be extended

in this way. All you have to do is implement the searchAddResults

method 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:

{{{

Page 112: Apostrophe Manual

112 | P a g e

// This is at the application level,

apps/frontend/modules/a/actions/actions.class.php

class aActions extends BaseaActions

{

protected function searchAddResults(&$values, $q)

{

// $values is the set of results so far, passed by reference so

you can append more.

// $q is the Zend query the user typed.

//

// Override me! Add more items to the $values array here (note

that it was passed by reference).

// Example: $values[] = array('title' => 'Hi there', 'summary' =>

'I like my blog',

// 'link' => 'http://thissite/wherever', 'class' => 'blog_post',

'score' => 0.8)

//

// 'class' is used to set a CSS class (see searchSuccess.php) to

distinguish result types.

//

// Best when used with results from a

aZendSearch::searchLuceneWithValues call.

//

// IF YOU CHANGE THE ARRAY you must return true, otherwise it will

not be sorted by score.

// return true;

Page 113: Apostrophe Manual

113 | P a g e

}

}

}}}

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:

{{{

Page 114: Apostrophe Manual

114 | P a g e

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($this, array(

'type' => $this->getType(),

'title' => $this->getTitle(),

'description' => $this->getDescription(),

'credit' => $this->getCredit(),

'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

Page 115: Apostrophe Manual

115 | P a g e

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)

So the complete call to aZendSearch::updateLuceneIndex might be:

{{{

aZendSearch::updateLuceneIndex($this,

array('text' => $this->getFullSearchText()),

null, // Or perhaps $this->getCulture() depending on your needs

array('title' => $this->title,

'summary' => $this->getShortSummary(),

'url' => 'mymodule/show?id=' . $this->id,

'view_is_secure' => false));

}}}

Note the second argument, which is the culture for this object. You can

Page 116: Apostrophe Manual

116 | P a g e

pass null to use the current user's culture which often makes sense if

they have just edited the object. Apostrophe search returns only results for your current culture.

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);

}

}}}

Page 117: Apostrophe Manual

117 | P a g e

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.

=== Manipulating Slots Programmatically ===

==== 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.

Page 118: Apostrophe Manual

118 | P a g e

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->getArea('body');

}}}

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

{{{

if (count($page->getArea()) == 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.

Page 119: Apostrophe Manual

119 | P a g e

==== 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');

Page 120: Apostrophe Manual

120 | P a g e

$slot->value = $title;

$slot->save();

$page->newAreaVersion('title', 'update',

array(

'permid' => 1,

'slot' => $slot));

}}}

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, 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 121: Apostrophe Manual

121 | P a g e

{{{

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

'top' => false));

}}}

The default is to add the slot at the top.

==== Looping Over All Slots in a Page ====

Looping over slots is dangerous, depending on your goals, because

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 ($this->Areas as $area)

{

$areaVersion = $area->AreaVersions[0];

foreach ($areaVersion->AreaVersionSlots as $areaVersionSlot)

{

$slot = $areaVersionSlot->Slot;

$permid = $areaVersionSlot->permid;

// Now you can do things with $slot

}

}

Page 122: Apostrophe Manual

122 | P a g e

}}}

Recall that areas can contain multiple slots (as a result of the "add

slot" 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.

=== 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.

Page 123: Apostrophe Manual

123 | P a g e

Although you can start from scratch, we recommend copying the aYoutube or aVimeo class as a

starting point, as it makes it easier to ensure you are returning data in the right format.

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

`app.yml`:

{{{

all:

aMedia:

embed_services:

- class: aYoutube

media_type: video

- class: aVimeo

media_type: video

- class: aMyservice

media_type: video

}}}

Here the standard YouTube and Vimeo services have been kept in place. You can choose to remove

them if you wish by not including them in your settings.

(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 the trunk and

soon to version 1.5 if you are interested in adding support for custom services.)

=== Conclusion ===

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 [http://trac.apostrophenow.org/ Apostrophe Trac] to

Page 124: Apostrophe Manual

124 | P a g e

submit your reports, requests and concerns. And also be sure to join the

[http://groups.google.com/group/apostrophenow apostrophenow Google group] to share your

experiences with other Apostrophe developers.

[wiki:ManualI18N Continue to Internationalizing Apostrophe]

[ManualOverview Up to Overview]

Page 125: Apostrophe Manual

125 | P a g e

= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Requirements ==

Your project must contain the svn trunk version of apostrophePlugin, or version 1.4 stable. The 1.0

stable release of apostrophePlugin is missing necessary supporting features for

apostropheBlogPlugin.

The '''quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the the

sandbox project]''', which includes the blog plugin, in which case you can just install that according

to [wiki:ManualInstallation#CheckItOutFromSubversion the directions].

== Installation ==

In a new project, creating the database tables for the blog plugin is a natural part of the usual

command:

{{{

./symfony doctrine:build --all --and-load

}}}

Again, '''the quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the

1.4 stable branch] (or the trunk) of the sandbox project''', which includes the blog plugin, in which

case you can just install that according to the directions. Then you can just do the usual

doctrine:build command and you're good to go.

However, the blog plugin can be added to an existing Apostrophe Symfony project. To do so you'll

need to install the plugin via `plugin:install` or svn externals (we recommend the latter), enable it

correctly, and add the relevant tables to your database without, of course, overwriting all of your

existing data.

Page 126: Apostrophe Manual

126 | P a g e

'''First make sure that apostropheBlogPlugin is enabled AFTER apostrophePlugin in your

config/ProjectConfiguration.class.php file.''' Do NOT use `enableAllPluginsExcept` for this purpose.

Enable the specific plugins you want, with the blog plugin listed after the main Apostrophe plugin.

Otherwise certain class files will not be found.

Once you have enabled the plugin, you need to add the appropriate database tables. Fortunately the

`apostrophe:migrate` task is automatically extended to support this when the blog plugin is present.

Make sure you have built your model, form and filter classes and cleared your cache, BACK UP YOUR

DATABASE, and then run the `apostrophe:migrate` task, taking care to specify the correct

environment:

{{{

./symfony doctrine:build --all-classes

./symfony cc

./symfony apostrophe:migrate --env=dev

}}}

This task requires MySQL. If you are not using MySQL, use doctrine:build-sql to generate SQL

commands in data/sql/schema.sql and review that file for tables beginning with `a_blog`.

=== Adding Permissions For Blog Post Editors ===

Those who are adding the blog plugin to older Apostrophe projects might not have the `blog_admin`

and `blog_author` permissions in their `sf_guard_permission` table, or the corresponding entries in

`sf_guard_user_group_permission`. For security reasons we do NOT automatically add these via the

`apostrophe:migrate` task. However, you can add them after you have performed the above steps

by visiting the permissions and groups dashboards while logged in as the superuser. Be sure to

create `blog_admin` and `blog_author` permissions and add them to your `admin` and `editor`

groups. To locate the dashboards, log in as the superuser, click on "Users," and then click on

"Permissions Dashboard" and "Groups Dashboard" at the left.

=== Creating Your Engine Pages ===

Page 127: Apostrophe Manual

127 | P a g e

You're almost there, but you still need to create "engine pages" where your blog posts and events

will be displayed. Although there is a separate back end for editing and managing blog posts,

creating and naming the front end pages is up to you.

The distinction between the back end blog administration page and the front end blog engine page

can be a bit confusing. However it offers substantial benefits. The back end administration page has

many navigational and editing tools that would be overwhelming if we tried to present them in the

context of the front end. That's why we use separate pages for this purpose.

To create your front end engine pages for blog posts and upcoming events:

1. Navigate to the home page

2. Log in as admin

3. Click "This Page," then "Add New Page," giving it the name "News" or "Blog" depending on your

preference

4. When the new page appears, click "This Page," then click the "Gear" icon

5. Set "Page Engine" to "Blog"

6. Click Save.

Now repeat these steps for the "Events" engine.

Note that if you do not want your blog and events engines pages to be public you can unpublish

them via the page settings menu, accessed via the gear. If you anticipate inserting blog post slots

into pages but don't want an "actual blog" that presents navigation to access all blog posts, this

might be right for your needs.

== Posting to the Blog ==

To post to the blog, click the "Blog" icon at the top of any page. Then click "New Post."

Page 128: Apostrophe Manual

128 | P a g e

Give your post a title, then start adding slots, exactly as you would if you were creating a regular

Apostrophe page. The body of your blog post is a full-fledged Apostrophe "area," so it can contain

rich text, slideshows, video and other Apostrophe features.

== Blog Post Categories ==

Notice that blog posts can be organized into categories. This feature is critical because it allows you

to insert blog posts on the same subject via blog post slots elsewhere on the site. You can also create

additional blog engine pages which are locked to a single category. To lock a blog engine page to a

single category, first create the page, then switch it to the blog engine, and then select one or more

blog post categories from the category selector that appears.

== Blog Post Slots ==

apostropheBlogPlugin provides two good ways to insert blog posts into your regular pages. You can

insert a single blog post with the "Blog Post" slot, or insert multiple recent blog posts with the "Blog

Posts" slot.

The "Blog Post" slot invites you to search by title. Since all blog posts must have unique titles this is a

very effective way to pick a post.

The "Blog Posts" slot asks how many of the most recent blog posts you wish to display, and also

allows you to select specific categories. The use of categories allows you to present relevant content

on any page without foreknowledge of what that content will be.

=== Blog Post Excerpt Options ===

By default, blog post slots display a short excerpt from the post along with the first image, if any,

found in the post. You can adjust this behavior with the following options to the `aBlog` or

`aBlogSingle` slot:

* `maxImages` determines the number of images extracted from the blog post. If it is greater than

one, the images are presented as a click-to-advance slideshow.

Page 129: Apostrophe Manual

129 | P a g e

* `slideshowOptions` can be used to override the behavior of the slideshow, using exactly the same

options available to slideshow slots (see ManualDesignersGuide).

* `excerptLength` determines the maximum word count of the excerpt.

These options are often enough. But you can do more, including displaying the entire blog post as

part of a regular CMS page. Read on for more possibilities.

== Customizing Blog Post Templates ==

All blog posts have a blog post template that determines their basic structure. For instance, "out of

the box" the blog plugin allows for one-column and two-column blog posts. The two-column posts

have a narrow second column which is great for associated images and videos.

You can add additional templates to your project by overriding the `aBlog` settings in `app.yml`.

Note that you must begin by copying this entire section into `app.yml` (of course you should merge

this under your existing `all` heading):

{{{

all:

aBlog:

templates:

singleColumnTemplate:

name: Single Column

areas: ['blog-body']

twoColumnTemplate:

name: Two Column

areas: ['blog-body', 'blog-sidebar']

comments: false

# add_this: punkave # Username for AddThis -- http://addthis.com

aEvent:

Page 130: Apostrophe Manual

130 | P a g e

templates:

singleColumnTemplate:

name: Single Column

areas: ['blog-body']

}}}

Once you have copied these settings to your own `app.yml`, you can add additional templates in

addition to the one-column and two-column templates, or replace them entirely. The templates you

specify will be the choices on the "Template" dropdown when creating a blog post. Please do not

remove templates that are already in use for posts on your site. If you need to do that, first add your

new templates and change the template setting for each existing post.

Note that you need to specify an array of Apostrophe area names that appear in your templates (the

`areas` setting for each template seen above in this `app.yml` excerpt). This is used to implement the

`getText` convenience method, which returns just the text of an entire blog post. This is not

currently used in the core of the blog plugin but does come in handy in application-level code at

times, so we recommend specifying a list of area names for each template.

You can customize the appearance of these templates when the blog post is seen in on the blog

engine page, and even change the Apostrophe areas that make up the blog post, by overriding the

`aBlog/singleColumnTemplate` and `aBlog/twoColumnTemplate` partials. And you can customize the

appearance of blog posts when inserted as slots by overriding `aBlog/singleColumnTemplate_slot`

and `aBlog/twoColumnTemplate_slot`. And you can override the templates for the RSS feed by

overriding `aBlog/singleColumnTemplate_rss` and `aBlog/twoColumnTemplate_rss`.

Often these options are enough. However, if you are inserting blog posts on pages with different

page templates and different amounts of space available, or you wish to use teasers in some cases

and full-fledged blog posts in others, you'll want the `subtemplate` option.

When you insert a blog post slot in a page template like this:

{{{

<?php a_slot('blogpost', 'aBlogSingle', array('subtemplate' => 'inMyTemplate')) ?>

Page 131: Apostrophe Manual

131 | P a g e

}}}

The blog plugin will append `_inMyTemplate` (note the underscore) to the blog post's template

name, instead of the usual `_slot`.

That is, if the blog post is a single column post, `_singleColumnTemplate_inMyTemplate.php` will be

used, and if the blog post is a two column post, `_twoColumnTemplate_inMyTemplate.php` will be

used.

You can also specify subtemplates for specific blog post templates:

{{{

<?php a_slot('blogpost', 'aBlogSingle', array('template_options' => array('singleColumnTemplate' =>

array('subtemplate' => 'inMyTemplate')))) ?>

}}}

There are two more ways to change the template used for a blog post. You can simply force the blog

slot to use the same template that the blog engine page would use:

{{{

<?php a_slot('blogpost', 'aBlogSingle', array('full' => true)) ?>

}}}

In this case, `_singleColumnTemplate.php` is used, exactly as it would be on the engine page.

Note that this won't work well with our out of the box blog templates, which are designed to display

wide posts on the blog engine page. But it may work well with your own overrides of those

templates.

The last option is to simply force the use of a specific blog post template:

Page 132: Apostrophe Manual

132 | P a g e

{{{

<?php a_slot('blogpost', 'aBlogSingle', array('template' => 'myTemplate') ?>

}}}

This forces the use of `_myTemplate_slot.php` (which still must be in your application level override

of the `aBlog/templates` folder). `_slot` is still appended because were are simply requiring the blog

plugin to act as if this post's template was `myTemplate`. The usual rules for what comes next still

apply.

The `template` option is often useful if your blog post templates are always designed in such a way

that the most important area always has the same name. This is true for our standard blog post

templates: the single column template and the two column template both have an area named

`blog-body`, so you can safely substitute the former for the latter if you don't mind giving up the

presumably less important information in the second column.

== Working With Events ==

Events are slightly different from blog posts. A blog post is an article, a piece of news or other

content that should be published now or on a particular future date. An event, on the other hand,

has a fixed start time and end time during which it will take place.

Blog posts are traditionally presented in reverse chronological order (newest first). Events, by

contrast, are typically presented in calendar order (today's events first).

apostropheBlogPlugin provides separate back end administration and front end engine pages for

events, and separate slots for inserting single events and groups of upcoming events. The same

features that are available for blog posts can also be used to customize the appearance and behavior

of events.

As with blog posts, you must first create an engine page for events before you can add them to your

site. Once you've done that, you can begin adding upcoming events to the system.

Page 133: Apostrophe Manual

133 | P a g e

When you visit the events engine page, which typically becomes the public-facing "calendar of

upcoming events" for your project, you'll see upcoming events sorted by start date and time. Again,

this is different from blog posts, which are presented in reverse chronological order.

Users can still navigate to past events using the provided links to browse by year, month and day.

== Managing Editing Privileges in the Blog Plugin ==

By default, those who are members of the `editors` group will be able to write blog posts, and delete

and edit their own posts.

Those who are members of the `admin` group will be able to edit and delete the blog posts of

others, and also determine which editors are permitted to assign which categories to blog posts.

Admins can also grant editors the right to edit specific posts written by others.

Under the hood, the blog plugin looks for the same permissions that are used to determine which

users are potential webpage editors. That is, if you are a potential webpage editor, you are a

potential blog author.

== Apostrophe Search and the Blog Plugin ==

Good news: published blog posts and events are automatically integrated into Apostrophe's sitewide

search feature. If you search for something that appears in a post or event, a link to that event will

be included in the results.

== Adding Blog Comments with Disqus ==

You may wonder why we didn't implement our own system for commenting on blog posts.

In a nutshell: it's easy to implement comments badly. It's not so easy to authenticate users using

their system of choice and filter spam effectively. Third party services like Disqus do it brilliantly for

free. So rather than reinvent the wheel we've provided the hooks for you to take advantage of them.

Page 134: Apostrophe Manual

134 | P a g e

(At a later date we may implement our own comment system, for instance to meet the needs of

those who are blogging for an intranet audience where Disqus is not an option. Disqus will probably

always be a desirable solution for most.)

In Apostrophe 1.5 we've made it incredibly easy to implement disqus in your apostrophe (1.5+) site.

All you have to do is add this block to your project app.yml:

aBlog:

disqus_enabled: true

disqus_shortname: yourdisqusshortname

Just change the disqus shortname to the shortname you created when you signed up for disqus.

(if you started with our sandbox, this is probably already in your project app.yml, in that case just un-

comment it and change the shortname)

This will enable comments in the show success of your blog posts. You will still need to edit some of

your blog templates if you want to do something like show the comment count on blog post slots.

There are instructions for that in the 1.4 instructions below, as well as in the Disqus help documents.

If you're using the 1.4 version of apostrophe, you will need to override a few blog templates to

enable disqus.

Here's how to set up Disqus with the Apostrophe 1.4 blog plugin:

1. [http://disqus.com/ Visit Disqus and set up an account].

Page 135: Apostrophe Manual

135 | P a g e

2. On the "Choose Install Instructions" page, click "Universal Code."

3. Override the `aBlog/showSuccess` template at the app level. Copy the existing

`aBlog/showSuccess`, and add the following inside this `if ... endif` block found at the end:

{{{

<?php if($aBlogPost['allow_comments']): ?><?php endif ?>

}}}

Should become:

{{{

<?php if($aBlogPost['allow_comments']): ?>

<?php include_partial('aBlog/disqus') ?>

<?php endif ?>

}}}

We'll be making this easier for you soon by adding an empty `aBlog/postFooter` partial that you can

override as needed.

4. Optionally, in your blog post template overrides (`aBlog/singleColumnTemplate` and

`aBlog/twoColumnTemplate`, as well as any blog templates you have added), add the

`#disqus_thread` suffix to blog post permalinks so they can display comment counts. Change this:

{{{

<h3 class="a-blog-item-title">

<?php echo link_to($a_blog_post->getTitle(), 'a_blog_post', $a_blog_post) ?>

</h3>

}}}

Page 136: Apostrophe Manual

136 | P a g e

To this:

{{{

<h3 class="a-blog-item-title">

<?php $url = url_for('a_blog_post', $a_blog_post) . '#disqus_comments' ?>

<a href="<?php echo $url ?>"><?php echo $a_blog_post->getTitle() ?></a>

</h3>

}}}

5. Paste the "embed code" provided by Disqus into your `aBlog/disqus` partial (this code is provided

on the Disqus site).

6. Provide yourself a convenient way to access the Disqus comment administration page for each

blog post by overriding the `aBlogAdmin/list_actions` partial. This is a Symfony admin generator

partial. You can fetch it from your Symfony cache, but we've reproduced it here for your

convenience. The new code is the second line. Note that you must replace

"YOURDISQUSSHORTCODE" with '''your''' disqus short code as provided by Disqus:

{{{

<?php echo $helper->linkToNew(array( 'params' => array( ), 'class_suffix' => 'new', 'label' =>

'New',)) ?>

<li><?php echo link_to('Comments', 'http://YOURDISQUSSHORTCODE.disqus.com', array('class' =>

'a-btn big alt', )) ?></li>

}}}

7. Add the Disqus "comment count code" just before the closing `</body>` tag of your `layout.php`.

Again, this code is provided on the Disqus "universal code" installation instructions page, so you

should copy the latest code given there.

Optionally, you can also put Disqus into "developer mode" by pasting this code just before the

closing `</head>` tag of your `layout.php`:

Page 137: Apostrophe Manual

137 | P a g e

{{{

<script type="text/javascript">

var disqus_developer = 1; // this would set it to developer mode

</script>

}}}

8. Turn on support for allowing comments in `app.yml`. Note that '''if you have not yet overridden

any of the `aBlog` settings you'll need to copy the entirety of

`plugins/apostropheBlogPlugin/config/app.yml` into the `all` section of your application-level

`app.yml` file.''' Then you can make this change:

{{{

all:

aBlog:

# Be sure to copy the rest of the aBlog settings from the plugin app.yml too!

# Now change the comments setting

comments: true

}}}

As with all changes to `app.yml`, don't forget the `symfony cc` command.

Once you have done this, blog authors and editors will be able to check the "allow comments" box

for individual posts, which will then show Disqus comments.

After following these steps you should immediately have working Disqus comments on your site.

Follow the documentation on the Disqus site if you wish to style Disqus to more closely match your

site's style.

Page 138: Apostrophe Manual

138 | P a g e

= Apostrophe Manual =

[ManualDevelopersGuide Back to the Developer's Guide]

[ManualOverview Up to the Overview]

== Internationalization ==

Apostrophe supports internationalization and localization of sites in two critical ways: translation of

the administrative interface, which your clients see, and translation of the actual site content, which

end users of the site will see.

This section assumes familiarity with earlier sections of the manual (the Developer's Guide is

optional).

=== What Languages Are Available? ===

Apostrophe supports UTF-8, so you can potentially translate your content into any language. At this

time the user interface used by those editing content on the site supports French, Spanish, German

and English, with more languages on the way. If you are editing site content in a language for which

our administrative interface has not yet been translated, you'll see an administrative interface in

English.

=== UTF-8 and Apostrophe ===

Apostrophe stores content in UTF-8 for compatibility with nearly all languages. However, the

Apostrophe 1.0.x series has issues with generating slugs for items with non-Latin1 characters in their

names. Support for this is already committed in the trunk and will be standard in the 1.1 series of

Apostrophe. So those who wish to use Greek and other non-Latin character sets today should use

the svn trunk of the plugin and migrate to the 1.1 stable branch when it appears.

Page 139: Apostrophe Manual

139 | P a g e

In any case, for good results with non-ASCII characters you must ensure that your MySQL database is

configured to store and collate information in UTF-8. See the `databases.yml.sample` file provided

with Apostrophe for the required settings. Note the attribute settings below:

{{{

all:

doctrine:

class: sfDoctrineDatabase

param:

dsn: mysql:dbname=demo;host=localhost

username: root

password: root

encoding: utf8

attributes:

DEFAULT_TABLE_TYPE: INNODB

DEFAULT_TABLE_CHARSET: utf8

DEFAULT_TABLE_COLLATE: utf8_general_ci

}}}

If you already have a database with the Latin1 character encoding or collation you will need to use

ALTER TABLE statements to migrate it to UTF-8. See the MySQL documentation for more information

on this issue.

To use UTF8 characters reliably in URLs with Symfony you must also modify your `frontend_dev.php`

and `index.php` controllers to ignore `PATH_INFO`. Apache helpfully decodes UTF-8 URLs in

`PATH_INFO`, unfortunately trashing them in the process. However Symfony is perfectly capable of

getting by with just the `REQUEST_URI` environment variable, which it uses when there is no explicit

PHP script name in the URL. So add this code to the top of your front end controllers (after the `<?`

line of course) and you'll be ready to go with non-Latin URLs:

{{{

Page 140: Apostrophe Manual

140 | P a g e

unset($_SERVER['PATH_INFO']);

}}}

=== Allowing Users to Switch Languages ===

By default, there is no user interface to switch languages in Apostrophe. You can turn this on easily

with two `app.yml` settings: one to turn on the user interface and another to specify which

languages are available. A third setting is required to fully internationalize search indexing, a step

you may wish to skip if your site is used only in Latin languages because Zend Search is not able to be

clever in its handling of plural versus singular words and the like when in strict UTF-8 mode.

Here is an example. Note that these steps have already been carried out in our current sandbox

project.

{{{

all:

a:

# If true, there will be a language switcher next to the login/logout button

i18n_switch: true

i18n_languages: [en, fr, de, es]

}}}

You must also turn on support for internationalization in `settings.yml`, and may change the default

language from English to another language there as well:

{{{

all:

.settings:

i18n: on

default_culture: en

Page 141: Apostrophe Manual

141 | P a g e

}}}

Be sure to `symfony cc` after this and any change to `app.yml`.

Then copy the `apps/frontend/i18n` folder from our sandbox project to your own project. This folder

contains the `apostrophe.fr.xml`, `apostrophe.de.xml`, etc. files used to provide translation on the

fly.

The language switcher can be used by both end users of the site and client staff who need to edit the

site in a user interface that speaks their preferred language.

(Developers: if you don't care for the user interface of the language switcher, consider overriding the

`a/login` partial at the application level. It is also a good candidate for progressive enhancement via

jQuery.)

When you switch languages you will immediately notice that the home page has no content. Is this a

bug? Not at all. The content has not been translated into the new language yet. That's the topic of

the next section.

=== Translating Your Site Content ===

When you switch languages, you find yourself looking at a blank page. That's because the content

has not yet been written for that page in that language. Just start adding slots!

Of course, you might want to use the content in another language as a starting point. We agree that

this could be more convenient, however a handy workaround is to open a separate browser (Firefox

if you use Chrome, and vice versa) and leave that browser logged into that site in the original

language for reference. We'll be investigating ways to make this process easier, but the two-

browsers method works very well in practice.

=== Adding the Culture to the URL ===

Page 142: Apostrophe Manual

142 | P a g e

In order for Google to index content in several languages you will need distinct URLs for several

languages. You can do this by changing your `a_page` route (beginning with Apostrophe 1.5):

{{{

a_page:

url: /:sf_culture/:slug

param: { module: a, action: show }

requirements: { slug: .* }

}}}

This allows Google to index separate pages for separate languages and allows links to language-

specific pages to be shared by users.

This leads to the question, "how does Google ever see the home page in a language other than

English?" Our default language switcher just changes the user's culture and redirects to the home

page without a culture in the URL. But you can also access culture-specific homepages at `/de/`,

`/en/`, `/fr/` etc.

Consider removing the standard "homepage" route and replacing it with an action at the project

level that redirects to the home page URL for the default language. Also consider adding direct links

to the homepages for the other languages you support on your site. This will allow Google to find

them.

=== Translating the User Interface Into a New Language ===

As with any Symfony project, internationalization of the user interface is accomplished via XLIFF files.

And you can find complete translations in the `apps/frontend/i18n` folder of our sandbox project for

several languages, including French, German, and Spanish. Install the sandbox project to see these in

action right away. You can of course copy them to your own project's `apps/frontend/i18n` folder.

To add a new translation, you can take two approaches:

Page 143: Apostrophe Manual

143 | P a g e

1. [http://groups.google.com/group/apostrophenow Join the Apostrophe Now Google Group] and

express interest in translating the user interface. Our team will give you access to a convenient back

end interface for translating the content, and your work will benefit the entire Apostrophe open

source community.

2. Alternatively, just copy one of the existing XLIFF files in `apps/frontend/i18n` in our sandbox

project, changing the language code to the appropriate 2-letter ISO code for your language, and get

started translating. Those who are comfortable with XLIFF files may prefer this approach. Of course,

we hope you will decide to share the results with the rest of the community.

[wiki:ManualImportGuide Continue to Import & Migration]

[ManualOverview Up to Overview]

Page 144: Apostrophe Manual

144 | P a g e

= Apostrophe Manual =

[wiki:ManualI18N Back to the Internationalization Guide]

[ManualOverview Up to the Overview]

Apostrophe includes an import task that is capable of importing a site from a valid xml document.

The task can be run using

{{{

./symfony apostrophe:import-site

}}}

The task expects xml files to be located in sfRoot/data/a

A sample document is shown below.

{{{

<?xml version="1.0" encoding="UTF-8" ?>

<site>

<Page slug="/" title="Home" template="home" >

<Area name="body">

<Slot type="aRichText">

<value>This is my body text</value>

</Slot>

</Area>

<Area name="header_image">

Page 145: Apostrophe Manual

145 | P a g e

<Slot type="aImage">

<MediaItem src="header.jpg" alt="Header"/>

</Slot>

</Area>

<Page slug="/about" title="About" file-id="2" template="default" />

<Page slug="/people" title="People" file-id="3" template="default" >

<Page slug="/people/students" title="Students" id="4" template="default" />

<Page slug="/people/teachers" title="Teachers" id="4" template="default" />

</Page>

</Page>

</site>

}}}

An example of an external file used to include slot info.

data/pages/3.xml

{{{

<?xml version="1.0" encoding="UTF-8"?>

<Page>

<Area name="body">

<Slot type="foreignHtml">

<value><![CDATA[

<b>This is blod text</b><img src="food-fast.gif">More regular text. <a

href="http://www.punkave.com"><img

src="http://farm5.static.flickr.com/4090/5073384290_63ea8ab19d_b.jpg"></a> And another

<b><i>box</i></b>

]]>

</value>

</Slot>

Page 146: Apostrophe Manual

146 | P a g e

</Area>

</Page>

}}}

Page elements have 4 attributes, currently both the slug and title are required. Template is optional

and will default to default. The file-id is used to specify an external file where information about

areas and slots can be found, this is useful for particularly large sites where loading one XML file is

not feasible. A file-id of 2 will result in the importer looking for the file data/pages/2.xml. If file-id is

not specified the importer will not look for areas in the main xml file.

Currently 3 slot types are supported by the importer: aRichText, aImage, and foreignHtml. '''The

foreignHtml content type is used to import rich text with embedded images''', the import process

will create multiple rich text and imageSlots.

MediaItem src attribute can either be a http:// format url or a relative url. If relative importer will

expect file to be located at data/a/images.

xsd schema is attached to this page.

[ManualOverview Up to the Overview]

Page 147: Apostrophe Manual

147 | P a g e

The following is our internal process for deploying new Apostrophe client sites. We share it with you

to better document useful tools like `apostrophe:deploy` and `project:sync-content`. Please note

that it is not mandatory at all to use those tools with Apostrophe. But we like and recommend them

and also feel this document would benefit from third-party feedback.

- The team at P'unk Avenue

== P'unk Avenue Deployment Process ==

==== A guide for client system administrators and other technical staff ====

Our process is to develop sites on Macs, using svn for version control and local copies of Apache and

PHP to interact with the site before it is deployed. Completed code is then deployed via rsync to a

staging server for approval by the client and then deployed from there to the actual production

server. In some cases a staging server is not available, in which case we deploy directly to

production, although our preference is naturally to use a staging server.

Note that we do not use svn for final deployment. Instead we use svn to keep the code up to date on

development Macs and use various Symfony tasks (based on rsync, mysqldump and related

commands) to deploy code and content as needed.

So deploy the site to production, it is necessary to sync the code first from a development Mac to

the staging server and then on to the production server. This assures that each change has been

properly reviewed. It also means that "hotfixes" are never made directly to a server and such

changes will be overwritten by future deployments that do follow the process. Contributing directly

at the code level requires svn access to the project.

=== Setting Up config/databases.yml and config/properties.ini for production use ===

The `config/databases.yml` file must have database settings for the production server as well as for

the staging server. Examine this file to see how the staging settings are set up (it is a human-

readable configuration file; take care to preserve the indentation as it is significant). If you prefer, for

security reasons, not to keep this file in svn where developers can see it, we can take it off the list of

files that are synced to to the production server, and you can manually maintain a special production

Page 148: Apostrophe Manual

148 | P a g e

copy there. This is common on sites where P'unk Avenue does not have direct access to the

production server.

PLEASE NOTE: do not make direct changes to the database without consulting with the rest of the

developers involved. In particular the various tables that make up the Apostrophe content

management model should be manipulated through Apostrophe's Doctrine-based model layer and

never directly with SQL statements. Otherwise the page tree and versioning system can be easily

damaged. There are rare exceptions but it is best to discuss them with P'unk Avenue first.

The `config/properties.ini` file must contain ssh credentials (but no password) for the production

server. Examine this file to see how the staging settings are set up. Since this file does not contain

the password there is no security risk in adding the production settings to this file.

=== Syncing For The First Time ===

When syncing code to the production server for the first time, you will receive some errors when

running the `apostrophe:deploy` task. This is normal and due to the fact that the `web/index.php`

file has not been set up on that server yet. This file is not synced because it is specific to each server.

(Note to experienced Symfony developers: this is a departure from the default "frontend_dev.php"

approach that we prefer because it simplifies debugging and deployment. `index.php` acts as the

sole "switch" that determines which settings should be used on a particular host.)

After the first code sync, copy `web/index.php` from staging to production manually, and edit the

file so that it enables the prod environment rather than the staging environment:

{{{

ProjectConfiguration::getApplicationConfiguration('frontend', 'staging', true);

}}}

=== Syncing Code ===

To sync code from one server to another, we use the `apostrophe:deploy` Symfony task. The

following command will push the current code from the staging server or a development Mac to the

Page 149: Apostrophe Manual

149 | P a g e

production server. Note that this does not affect the content on the site, only the code. This is the

way to deploy fixes to the code, or to initially deploy the site to production:

{{{

./symfony apostrophe:deploy production prod

}}}

You will be prompted several times for the ssh password. This command can take a long time and

sometimes runs quietly. It carries out several Symfony commands:

{{{

[Locally]

project:permissions

project:deploy

apostrophe:fix-remote-permissions

[Remotely]

cc

doctrine:migrate

apostrophe:migrate

}}}

Also, the `project:deploy` task is given specific rsync arguments that ensure rsync is not fooled by

overlapping timestamps if two different developers have deployed recently. This is important to

avoid surprising results especially with the APC cache.

Note that the `apostrophe:fix-remote-permissions` task uses a password set in `config/properties.ini`

to request that the remote website adjust permissions on files that must be writable by both

command line tasks and the webserver. The relevant section of `properties.ini` is:

{{{

Page 150: Apostrophe Manual

150 | P a g e

[sync]

password=agoodveryrandompassword

}}}

Permissions and deployment are a source of great frustration in Symfony development: files created

by Apache are usually not writable by cron jobs and vice versa. If everything is in the database you're

OK, but if you need to manage files you have a problem on your hands. '''The best way to address

this issue is to run command line tasks and Apache as the same user''', which does not introduce any

great new security risk, since if PHP is compromised it is still possible to call `system()` even when

the Apache user has no shell. However, if you are not comfortable with this, `apostrophe:fix-remote-

permissions` is a useful workaround. it invokes the `aSync/fixPermissions` action, which carries out

the same steps as the `project:permissions` task, but does so as Apache.

To deploy new code from a development Mac to the staging server (which we recommend doing

first before deploying anything to the production server), use this command:

{{{

./symfony apostrophe:deploy staging staging

}}}

You can then run:

{{{

./symfony apostrophe:deploy production prod

}}}

Directly on the staging server to deploy the final step to production after the client has approved the

changes. If P'unk Avenue does not have direct access to the production server then this command is

carried out by the client system administrator.

=== Syncing Content ===

Page 151: Apostrophe Manual

151 | P a g e

Pushing code to the production server is normal. But pushing content to another server should be

done with great care and caution. Always think about what machine you are syncing from and what

machine you are syncing to. There is NO way to undo this operation, unless proper backups of the

database and the web/uploads folder are being made. Measure twice, cut once.

==== One-Time Content Sync TO Production At Launch ====

The following command, typed on the staging server, will sync content FROM the staging server TO

the production server. This should only be done ONCE when the production server is first set up, and

then once more after content has been frozen on staging in anticipation of launch:

ALMOST CERTAINLY A BAD IDEA (except the very first time production is set up):

{{{

./symfony project:sync-content frontend staging to prod@production

}}}

You will be prompted several times for the ssh password. This command can take a long time and

sometimes runs quietly.

Note that the `project:sync-content` task copies both the MySQL database and the `web/uploads`

and `data/a_writable` folders, as well as any other data folders specified in `app.yml` (usually these

are the only ones).

==== Periodic Sync Back From Production for Better Testing ====

The following command, typed on the staging server, will sync content FROM the production server

back down TO the staging server. This command is quite useful for making sure the staging server’s

content is a realistic test of what will happen when new code changes are eventually pushed to

production. Content "lives" in production (after launch), so it makes sense to periodically refresh the

content on staging with the current content of production:

Page 152: Apostrophe Manual

152 | P a g e

{{{

./symfony project:sync-content frontend staging from prod@production

}}}

Note the use of “from” rather than “to” above. Always proofread this command carefully.

The following command, typed on a development Mac, will sync content FROM the staging server

back down TO the development Mac for realistic testing:

{{{

./symfony project:sync-content frontend dev from staging@staging

}}}

=== After Syncing Content: Rebuilding the Search Index ===

After syncing content TO production, you would run this command ON production to rebuild the

search index:

{{{

./symfony apostrophe:rebuild-search-index --env=prod

}}}

Similarly, you would run this command on staging and use `--env=staging` if you synced content back

down to staging. Otherwise searches will not return results.

Rebuilding the search index on a development Mac is an optional step if you are not testing search-

related issues.

Page 153: Apostrophe Manual

153 | P a g e

Rebuilding the search index does not take the site down in the meantime.

=== Frequently Asked Questions ===

“Why ‘production prod’ and not just production?” It’s possible for Symfony sites to have several

environments on one server although we don’t recommend that practice or use it on our client

projects.

“Why frontend?” Symfony projects can contain several sub-applications. Our projects typically

contain only one "application" because we believe in progressively enhancing the user's experience

to include admin features rather than creating a typically less user-friendly "back end" application

that is often neglected in the design process.

"I made a change to the code on the staging or production server and someone deployed and now

the change is gone. How do I make changes that stick?" You need svn access to the project so you

can participate in version control and avoid conflicts with other developers on the project. Code

changes are typically made on a development laptop and then committed with 'svn commit,' never

by hotfixing files on servers.

=== Further Reading ===

Complete developer documentation for Apostrophe, our content management system, is available

in the [ManualOverview Apostrophe manual]. For client technical staff interested in contributing at a

designer or developer level, that is the right place to start reading.