| 5 | | ## Before You Begin ## |
| 6 | | |
| 7 | | This is a beta release of a. Although the |
| 8 | | CMS already works quite well and we have released production sites |
| 9 | | based on it, certain aspects are still maturing. |
| 10 | | |
| 11 | | We strongly recommend that you follow the instructions in this |
| 12 | | document which permit you to check out or copy a complete project from svn |
| 13 | | rather than wrestling with the pear package dependencies yourself. You can check |
| 14 | | out our cmstest project directly as a sandbox, or copy it with our |
| 15 | | svnforeigncopy script to start your own version-controlled project. |
| 16 | | *This is how we start new sites of our own*, so you will get maximum |
| 17 | | support following this approach. |
| 18 | | |
| 19 | | ## Overview ## |
| 20 | | |
| 21 | | apostrophePlugin is the core of a suite of plugins that make up the [Apostrophe Content Management System](http://www.apostrophenow.com/). The philosophy of Apostrophe is that editing should be done "in context" whenever possible. |
| 22 | | |
| 23 | | apostrophePlugin is heavily inspired by and borrows a little code and numerous ideas from sfSimpleCMSPlugin. The changes here are dramatic enough that a separate plugin makes more sense, but we felt it important to acknowledge this up front. sfSimpleCMSPlugin allowed users to double-click text frames and edit them in place. We have expanded this idea to allow for a number of content slots to be edited without leaving the page. |
| 24 | | |
| 25 | | Standard features of apostrophePlugin include version control for all content slots, locking pages for authenticated users only, and in-context addition/deletion/reordering/retitling of pages. When a user is logged in with appropriate privileges the breadcrumb trail and sub-navigation areas become editing tools, neatly extending the metaphors they already implement rather than requiring a second interface solely for editing purposes. |
| 26 | | |
| 27 | | apostrophePlugin 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. |
| 28 | | |
| 29 | | ## Supported Browsers ## |
| 30 | | |
| 31 | | Editing works 100% in Firefox, Safari and Internet Explorer 7+. |
| 32 | | Editing is expressly not supported in Internet Explorer 6. Of course, |
| 33 | | browsing the site as a user works just fine in Internet Explorer 6. |
| 34 | | |
| 35 | | ## Requirements ## |
| 36 | | |
| 37 | | apostrophePlugin requires the following. Note that virtually all of the |
| 38 | | requirements are included in the cmstest project which you can easily |
| 39 | | check out or copy from svn as described below. We *strongly recommend* |
| 40 | | starting out in this way. |
| 41 | | |
| 42 | | ### System Requirements ### |
| 43 | | |
| 44 | | The following must be installed on your system: |
| 45 | | |
| 46 | | * PHP 5.2.4 or better, with a PDO driver for MySQL |
| 47 | | * MySQL (tested), SQLite (tested), or another relational database supported by PDO and Doctrine |
| 48 | | * For the media features: netpbm (recommended), or GD support in PHP |
| 49 | | * Optional, for PDF slots: ghostscript |
| 50 | | |
| 51 | | See apostrophePlugin/README and apostrophePlugin/README for more |
| 52 | | information about installing netpbm and ghostscript. A few truly excellent hosting |
| 53 | | companies may already have these in place. If you have a Virtual Private Server |
| 54 | | (and you should, shared hosting is very insecure), you can most likely install |
| 55 | | netpbm and ghostscript with a few simple commands like `sudo apt-get install netpbm` |
| 56 | | and `sudo apt-get install ghotscript`. |
| 57 | | |
| 58 | | 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. |
| 59 | | |
| 60 | | ### PHP Libraries ### |
| 61 | | |
| 62 | | * Symfony 1.2, 1.3 or 1.4 (provided with cmstest) |
| 63 | | * sfJqueryReloadedPlugin (provided with cmstest) |
| 64 | | * sfDoctrineGuardPlugin (provided with cmstest) |
| 65 | | * apostrophePlugin (provided with cmstest) |
| 66 | | * Doctrine (standard with Symfony 1.2) |
| 67 | | * The Zend framework, for search (provided with cmstest) |
| 68 | | |
| 69 | | Note that all of the components "provided with cmstest" are provided as svn externals, which means they update automatically when you type `svn update`. Wherever possible we've pinned these to stable branches, not development versions. |
| 70 | | |
| 71 | | Most users will also want apostrophePlugin and apostrophePlugin, which are also included in cmstest. |
| 72 | | |
| 73 | | Optional: apostrophePlugin is compatible with sfShibbolethPlugin (the 1.2 trunk version) and sfDoctrineApplyPlugin. |
| 74 | | |
| 75 | | Mac users can most easily meet the PHP requirements by installing |
| 76 | | the latest version of [MAMP](http://www.mamp.info/). Note that MAMP's PHP |
| 77 | | must be your command line version of PHP, not Apple's default install of PHP. |
| 78 | | To fix that, add this line to the `.profile` file in your home directory: |
| 79 | | |
| 80 | | export PATH="/Applications/MAMP/Library/bin:/Applications/MAMP/bin/php5/bin:$PATH" |
| 81 | | |
| 82 | | Of course your production server will ultimately need to meet the same requirements with regard to PHP and PDO. |
| 83 | | |
| 84 | | The use of Microsoft Windows as a hosting environment has not been tested but may work reasonably well now that netpbm is no longer mandatory for media slots. |
| 85 | | |
| 86 | | ## Installation ## |
| 87 | | |
| 88 | | For the time being, we recommend that you check out our |
| 89 | | sample project via Subversion. You don't need to be an svn expert |
| 90 | | to use this command: |
| 91 | | |
| 92 | | FOR SYMFONY 1.4 (recommended for new projects): |
| 93 | | |
| 94 | | svn co http://svn.symfony-project.com/plugins/apostrophePlugin/sandbox/trunk cmstest |
| 95 | | |
| 96 | | FOR SYMFONY 1.3 (recommended only if you are stuck with other 1.2 code that isn't fully 1.4-ready yet): |
| 97 | | |
| 98 | | svn co http://svn.symfony-project.com/plugins/apostrophePlugin/sandbox/branches/1.3 cmstest |
| 99 | | |
| 100 | | FOR SYMFONY 1.2 (*not recommended for new projects*): |
| 101 | | |
| 102 | | svn co http://svn.symfony-project.com/plugins/apostrophePlugin/cmstest cmstest |
| 103 | | |
| 104 | | Better yet, copy it to your own repository with [svnforeigncopy](https://sourceforge.net/projects/svnforeigncopy/) each time you want to start a new site. That's what we do. With `svnforeigncopy` you get a copy of the cmstest 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. |
| 105 | | |
| 106 | | This will give you all of the necessary plugins and the ability to |
| 107 | | `svn update` the whole shebang with one command. |
| 108 | | |
| 109 | | Next light up the folder `cmstest/web` as a virtualhost named `cmstest` via MAMP, `httpd.conf` or whatever your testing 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. |
| 110 | | |
| 111 | | Now create the `config/databases.yml` file, which must contain |
| 112 | | database settings appropriate to *your* system. Copy the file |
| 113 | | `config/databases.yml.sample` as a starting point: |
| 114 | | |
| 115 | | cp config/databases.yml.sample config/databases.yml |
| 116 | | |
| 117 | | If you are testing with MAMP the default settings |
| 118 | | (username root, password root, database name cmstest) may work |
| 119 | | just fine for you. If you are testing on a staging server you will |
| 120 | | need to change these credentials. |
| 121 | | |
| 122 | | Also create your `properties.ini` file: |
| 123 | | |
| 124 | | cp config/properties.ini.sample config/properties.ini |
| 125 | | |
| 126 | | In Symfony `properties.ini` contains information about hosts that a project can be synced to, in addition |
| 127 | | to the name of the project. The sample properties.ini file just defines the name of the project. You'll |
| 128 | | add information there when and if you choose to sync the project to a production server via project:deploy. |
| 129 | | See the Symfony documentation for more information about that technique. |
| 130 | | |
| 131 | | At this point you're ready to use the checkout of Symfony's 1.2.x branch |
| 132 | | that is included in the project. If you want to use a different installation |
| 133 | | of Symfony, such as a shared install for many sites (note that only 1.2.x is likely to work), copy |
| 134 | | config/require-core.php.example to config/require-core.php and edit the |
| 135 | | paths in that file. |
| 136 | | |
| 137 | | Next, cd to the `cmstest` folder and run these commands: |
| 138 | | |
| 139 | | (For Symfony 1.3 and 1.4) |
| 140 | | |
| 141 | | ./symfony doctrine:build --all |
| 142 | | ./symfony doctrine:data-load |
| 143 | | |
| 144 | | (For Symfony 1.2) |
| 145 | | |
| 146 | | ./symfony doctrine:build-all-reload |
| 147 | | |
| 148 | | This will create a sample database from the fixtures files. |
| 149 | | |
| 150 | | Now set the permissions of data folders so that they are writable by the web |
| 151 | | server. Note that svn does NOT store permissions so you can NOT assume they are |
| 152 | | already correct: |
| 153 | | |
| 154 | | ./symfony project:permissions |
| 155 | | |
| 156 | | Our apostrophePlugin extends project:permissions for you to include the |
| 157 | | data/writable folder in addition to the standard web/uploads, cache and log folders. Handy, isn't it? |
| 158 | | |
| 159 | | If you prefer you can do this manually: |
| 160 | | |
| 161 | | chmod -R 777 data/a_writable |
| 162 | | chmod -R 777 web/uploads |
| 163 | | chmod -R 777 cache |
| 164 | | chmod -R 777 log |
| 165 | | |
| 166 | | More subtle permissions are possible. However be aware that most |
| 167 | | "shared hosting" environments are inherently insecure for a variety |
| 168 | | of reasons. Before criticizing the "777 approach," be sure |
| 169 | | to [read this article on shared hosting and Symfony](http://trac.symfony-project.org/wiki/SharedHostingNotSecure). |
| 170 | | |
| 171 | | Next, build the site's search index for the first time (yes, search is included). It doesn't live in |
| 172 | | the database so it needs to be done separately. After this, you won't |
| 173 | | need to run this command again unless you are deploying to a new |
| 174 | | environment such as a staging or production server: |
| 175 | | |
| 176 | | ./symfony aToolkit:rebuild-search-index --env=dev |
| 177 | | |
| 178 | | (You can specify staging or prod instead to build the search indexes |
| 179 | | for environments by those names.) |
| 180 | | |
| 181 | | You can now log in as `admin` with the password `admin` to see how the |
| 182 | | site behaves when you're logged in. Start adding subpages, editing |
| 183 | | slots, adding slots to the multiple-slot content area... have a ball with it. |
| 184 | | |
| 185 | | OPTIONAL: by default pages are reindexed for search purposes at the time |
| 186 | | edits are made. For performance reasons you might be happier deferring |
| 187 | | this to a cron job that runs every few minutes. If you want to take this |
| 188 | | approach, set up a cron job like this: |
| 189 | | |
| 190 | | 0,10,20,30,40,50 * * * * /path/to/your/project/symfony a:update-lucene --env=env |
| 191 | | |
| 192 | | Note the `--env` option. There is a separate index for each |
| 193 | | environment. On a development workstation, specify `--env=env`. In |
| 194 | | a production environment you might specify `--env=prod`. |
| 195 | | |
| 196 | | Then turn on the feature in app.yml: |
| 197 | | |
| 198 | | all: |
| 199 | | a: |
| 200 | | defer_search_updates: true |
| 201 | | |
| 202 | | This speeds up editing a bit. But if you don't like cron, you don't have to enable it. |
| 203 | | |
| 204 | | You can also change the word count of search summaries: |
| 205 | | |
| 206 | | all: |
| 207 | | a: |
| 208 | | search_summary_wordcount: 50 |
| 209 | | |
| 210 | | That's the easy way to configure the CMS. The notes that follow assume you're doing it the hard way, without starting from the cmstest project. |
| 211 | | |
| 212 | | * * * |
| 213 | | |
| 214 | | Install the above plug-ins in your Symfony 1.2 project. We strongly encourage you to do so using svn externals. If you are using that approach 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. For best results use |
| 215 | | a relative path: |
| 216 | | |
| 217 | | cd web |
| 218 | | ln -s ../plugins/apostrophePlugin/web apostrophePlugin |
| 219 | | # Similar for other plugins required |
| 220 | | |
| 221 | | The search features of the plugin rely on Zend Search, so you must |
| 222 | | install the Zend framework. |
| 223 | | [The latest version of the minimal Zend framework is sufficient.](http://framework.zend.com/download/latest) If you choose to install this system-wide |
| 224 | | where all PHP code can easily find it with a `require` statement, great. |
| 225 | | If you prefer to install it in your Symfony project's |
| 226 | | `lib/vendor` folder, you'll need to modify your `ProjectConfiguration` class |
| 227 | | to ensure that `require` statements can easily find files there: |
| 228 | | |
| 229 | | class ProjectConfiguration extends sfProjectConfiguration |
| 230 | | { |
| 231 | | public function setup() |
| 232 | | { |
| 233 | | // We do this here because we chose to put Zend in lib/vendor/Zend. |
| 234 | | // If it is installed system-wide then this isn't necessary to |
| 235 | | // enable Zend Search |
| 236 | | set_include_path( |
| 237 | | sfConfig::get('sf_lib_dir') . |
| 238 | | '/vendor' . PATH_SEPARATOR . get_include_path()); |
| 239 | | // for compatibility / remove and enable only the plugins you want |
| 240 | | $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin')); |
| 241 | | } |
| 242 | | } |
| 243 | | |
| 244 | | Create an application in your project. Then create a |
| 245 | | module folder named a as a home for your page templates |
| 246 | | and layouts (and possibly other customizations): |
| 247 | | |
| 248 | | mkdir -p apps/frontend/modules/a/templates |
| 249 | | |
| 250 | | The CMS provides convenient login and logout links. By default these |
| 251 | | are mapped to sfGuardAuth's signin and signout actions. If you are |
| 252 | | using sfShibbolethPlugin to extend sfDoctrineGuardPlugin, you'll |
| 253 | | want to change these actions in apps/frontend/config/app.yml: |
| 254 | | |
| 255 | | all: |
| 256 | | sfShibboleth: |
| 257 | | domain: duke.edu |
| 258 | | a: |
| 259 | | actions_logout: "sfShibbolethAuth/logout" |
| 260 | | actions_login: "sfShibbolethAuth/login" |
| 261 | | |
| 262 | | You can also log in by going directly to `/login`. If you don't want to display the login link |
| 263 | | (for instance, because your site is edited only you), just shut that feature off: |
| 264 | | |
| 265 | | all: |
| 266 | | a: |
| 267 | | login_link: false |
| 268 | | |
| 269 | | You will also need to enable the a modules in |
| 270 | | your application's `settings.yml` file. Of course you may need |
| 271 | | other modules as well based on your application's needs: |
| 272 | | |
| 273 | | enabled_modules: [a, aText, aRichText, sfGuardAuth] |
| 274 | | |
| 275 | | a edits rich text content via the FCK editor. A recent version of FCK is |
| 276 | | included in downloads of apostrophePlugin, which is among the requirements already stated |
| 277 | | above. However you'll need to enable FCK in your settings.yml file, as follows: |
| 278 | | |
| 279 | | all: |
| 280 | | rich_text_fck_js_dir: apostrophePlugin/js/fckeditor |
| 281 | | |
| 282 | | Load the fixtures for the "stub" site. Every site begins with a home |
| 283 | | page with all other pages being added as descendants of the home page: |
| 284 | | |
| 285 | | ./symfony doctrine:build-all-load |
| 286 | | |
| 287 | | By default a will establish routes which map your modules and |
| 288 | | actions to: |
| 289 | | |
| 290 | | /cms/modulename/actionname |
| 291 | | |
| 292 | | And check all other URLs to see whether they match a slug (i.e. a path) |
| 293 | | in the CMS, offering up that page via the a/show action if so. |
| 294 | | |
| 295 | | If this is not what you want, you will need to shut off the built-in |
| 296 | | routes via app.yml and write your own: |
| 297 | | |
| 298 | | a_routes_register: false |
| 299 | | |
| 300 | | In any case, it is up to you to provide a route for the homepage of |
| 301 | | your site. If you want the homepage to be the root page of the CMS, |
| 302 | | just use this route in your routing.yml file: |
| 303 | | |
| 304 | | homepage: |
| 305 | | url: / |
| 306 | | param: { module: a, action: show, slug: / } |
| 307 | | |
| 308 | | ### Enhanced Form Controls ### |
| 309 | | |
| 310 | | To get the benefit of the progressively enhanced form controls |
| 311 | | featured in our admin tools, you'll need to add apostrophePlugin's |
| 312 | | aControls.js to your view.yml file: |
| 313 | | |
| 314 | | default: |
| 315 | | ... Other things ... |
| 316 | | javascripts: [/apostrophePlugin/js/aControls.js] |
| 317 | | |
| 318 | | ### Title Prefix ### |
| 319 | | |
| 320 | | By default, the title element of each page will contain the title of that page. |
| 321 | | In many cases you'll wish to specify a prefix for the title as well. |
| 322 | | |
| 323 | | You can do so by setting `app_a_title_prefix` in `app.yml`. This option supports |
| 324 | | optional internationalization: |
| 325 | | |
| 326 | | all: |
| 327 | | a: |
| 328 | | # You can do it this way... |
| 329 | | title_prefix: |
| 330 | | en: 'Our Company : ' |
| 331 | | fr: 'French Prefix : ' |
| 332 | | # OR this way for a single-culture site |
| 333 | | title_prefix: 'Our Company' |
| 334 | | |
| 335 | | ## Routing Rules ## |
| 336 | | |
| 337 | | For compatibility reasons, by default your CMS pages will appear in the /cms "folder" |
| 338 | | of your site. That is, the "home page" is `http://yoursite.com/cms`. Administrative |
| 339 | | actions of the CMS can then use the default Symfony routing rules. The home page URL ("/") is not overridden |
| 340 | | by default and still goes wherever you'd like it to go in your application. |
| 341 | | |
| 342 | | Of course, this probably isn't what most people want in practice. You can easily |
| 343 | | customize it via your `routing.yml` file so that URLs are presumed to be CMS pages if they do |
| 344 | | not expressly match a routing rule. And it's not hard to create a catch-all "folder" to contain URLs mapped to Symfony actions rather than CMS pages. |
| 345 | | |
| 346 | | These rules are used by our `cmstest` project. Note that the routing rule for pages must be named `a_page` so that the plugin can use it explicitly to generate short paths to pages, even in the presence of the default rule which would otherwise match first: |
| 347 | | |
| 348 | | # This default routing rule handles all non-CMS actions that are not |
| 349 | | # explicitly routed by another rule |
| 350 | | |
| 351 | | default: |
| 352 | | url: /admin/:module/:action/* |
| 353 | | |
| 354 | | # A homepage rule is expected by a and various other plugins, |
| 355 | | # so be sure to have one |
| 356 | | |
| 357 | | homepage: |
| 358 | | url: / |
| 359 | | param: { module: a, action: show, slug: / } |
| 360 | | |
| 361 | | # Put any additional routing rules for other modules and actions HERE, |
| 362 | | # before the catch-all rule that routes URLs to the |
| 363 | | # CMS by default. |
| 364 | | |
| 365 | | # Must be the last rule, and must have this name |
| 366 | | |
| 367 | | a_page: |
| 368 | | url: /:slug |
| 369 | | param: { module: a, action: show } |
| 370 | | requirements: { slug: .* } |
| 371 | | |
| 372 | | When using this technique you will also need to turn off the default routing rules of the CMS |
| 373 | | as we've done in the `cmstest` project: |
| 374 | | |
| 375 | | all: |
| 376 | | a: |
| 377 | | routes_register: false |
| 378 | | |
| 379 | | Thanks to Stephen Ostrow for his analysis of this issue. |
| 380 | | |
| 381 | | ## Customizing Your CMS Site ## |
| 382 | | |
| 383 | | You now have a CMS site. Access your Symfony site's URL to see the |
| 384 | | home page. |
| 385 | | |
| 386 | | Click "log in" and log in as the sfDoctrineGuard superuser |
| 387 | | (admin/admin) to see |
| 388 | | the editing controls. |
| 389 | | |
| 390 | | ### Managing Pages ### |
| 391 | | |
| 392 | | When a user has appropriate privileges on a page, they are able to make |
| 393 | | the following changes via the breadcrumb trail, which becomes an |
| 394 | | interactive site management tool when logged in: |
| 395 | | |
| 396 | | * Rename the page by clicking on the page title |
| 397 | | * Add a child page beneath the current page |
| 398 | | * Open the page management settings dialog via the "gear" icon |
| 399 | | for less frequent changes |
| 400 | | |
| 401 | | Note that the breadcrumb trail appears on the home page only when |
| 402 | | logged in with editing privileges. On sub-pages the breadcrumb |
| 403 | | trail always appears. |
| 404 | | |
| 405 | | a emphasizes "turning off" pages as the preferred way |
| 406 | | of "almost" deleting them because it is not permanent. |
| 407 | | Anonymous users, and users who do not |
| 408 | | have editing privileges on the page, will see the usual 404 Not Found error. |
| 409 | | But users with editing privileges will see the page with its title |
| 410 | | "struck through" and will be able to undelete the page if they desire. |
| 411 | | You can also delete a page permanently via the small X in the lower right |
| 412 | | corner of the management dialog. Most of the time that's a shortsighted |
| 413 | | thing to do but it is useful when you create an unnecessary |
| 414 | | page by accident. |
| 415 | | |
| 416 | | The side navigation column also offers an editing tool: users with |
| 417 | | editing privileges can change the order of child pages listed there |
| 418 | | by dragging and dropping them. (If a page has no children, the side |
| 419 | | navigation displays its peers instead, including itself.) |
| 420 | | |
| 421 | | ### Editing Content: Editing Slots ### |
| 422 | | |
| 423 | | What about the actual content of the page? The editable content |
| 424 | | of a page is stored in "CMS slots" (not the same thing as Symfony slots). |
| 425 | | CMS slots can be of several types: |
| 426 | | |
| 427 | | * Plaintext slots (single line or multiline) |
| 428 | | * Rich text slots (edited via FCK) |
| 429 | | * Custom slots (of any type, implemented as described later) |
| 430 | | |
| 431 | | Once you have logged in, you'll see each editable slot outlined |
| 432 | | with a box. If the slot is currently empty, there will also be a |
| 433 | | hint to double-click the slot to begin editing it. Double-click it to |
| 434 | | edit it with the appropriate editor (an input, textarea or rich |
| 435 | | text edit control). Click "Save" to save your changes. |
| 436 | | |
| 437 | | Every slot also offers version control. The arrow-in-a-circle icon |
| 438 | | accesses a dropdown list of all changes that have been made to that slot, |
| 439 | | labeled by date, time and author and including a short summary of the change |
| 440 | | to help you remember what's different about that version. Pick any version and |
| 441 | | click "Preview" to redisplay that version of the slot. To revert to an old version, |
| 442 | | copying it to create a new version of the slot, click "Revert." Click |
| 443 | | "Cancel" if you decide not to switch versions. |
| 444 | | |
| 445 | | ### Editing Content: Editing Areas ### |
| 446 | | |
| 447 | | In addition to single slots, apostrophePlugin also supports |
| 448 | | "areas." Areas are vertical columns containing more than one slot. |
| 449 | | Editing users are able to add and remove slots from an area at any time, |
| 450 | | selecting from a list of slots approved for use in that area. The slots |
| 451 | | can also be reordered via up and down arrow buttons (used here instead |
| 452 | | of drag and drop to avoid possible browser bugs when dragging and dropping |
| 453 | | complex HTML, and because drag and drop is not actually much fun to use |
| 454 | | when a column spans multiple pages). |
| 455 | | |
| 456 | | The usefulness of areas may not be entirely clear when only |
| 457 | | plaintext and rich text slots are in use on a site. Their usefulness |
| 458 | | can be better understood when custom slot types that implement |
| 459 | | multimedia or connect to your project-specific resources come into play. |
| 460 | | You want to be able to interleave these with blocks of text without |
| 461 | | creating a new custom page template for each one. Areas give you that |
| 462 | | capability. |
| 463 | | |
| 464 | | ## Creating and Managing Page Templates and Layouts ## |
| 465 | | |
| 466 | | Where do slots appear in a page? And how do you insert them? |
| 467 | | |
| 468 | | Slots can be inserted in two places: in your site's <tt>layout.php</tt> |
| 469 | | file, which decorates all pages, and in page template files, which can |
| 470 | | be assigned to individual pages. |
| 471 | | |
| 472 | | ### How to Customize the Layout ### |
| 473 | | |
| 474 | | By default, the CMS will use the |
| 475 | | <tt>layout.php</tt> file bundled with it. If you wish, you can turn this |
| 476 | | off via app.yml: |
| 477 | | |
| 478 | | all: |
| 479 | | a: |
| 480 | | use_bundled_layout: false |
| 481 | | |
| 482 | | CMS pages will then use your application's default layout. One strategy |
| 483 | | is to copy our <tt>layout.php</tt> to your application's template folder |
| 484 | | and customize it there after turning off use_bundled_layout. |
| 485 | | |
| 486 | | ### Turning Off the Home Page Tab ### |
| 487 | | |
| 488 | | By default a generates navigation tabs for the direct |
| 489 | | children of the home page, and for the home page itself. If you don't |
| 490 | | want a tab for the home page itself, just change this setting |
| 491 | | in `app.yml`: |
| 492 | | |
| 493 | | all: |
| 494 | | a: |
| 495 | | home_as_tab: false |
| 496 | | |
| 497 | | ### Reordering Children of the Home Page ### |
| 498 | | |
| 499 | | a offers drag-and-drop reordering of the children of |
| 500 | | any page via the navigation links on the left-hand side. But the home |
| 501 | | page, by default, doesn't display those links. So how can you reorder |
| 502 | | its children? |
| 503 | | |
| 504 | | One way is to add the subnav component back into your |
| 505 | | local copy of homeTemplate.php: |
| 506 | | |
| 507 | | <?php include_component('a', 'subnav') # Left Side Navigation ?> |
| 508 | | |
| 509 | | To avoid wrecking the layout of your home page, you could choose to |
| 510 | | insert it only when an admin is logged in: |
| 511 | | |
| 512 | | <?php if ($sf_user->hasCredential('cms_admin')): ?> |
| 513 | | <?php include_component('a', 'subnav') # Left Side Navigation ?> |
| 514 | | <?php endif ?> |
| 515 | | |
| 516 | | On our "TODO" list: allow the left side navigation controls to be collapsed |
| 517 | | by default for more convenient use on pages with a layout that doesn't |
| 518 | | really accommodate them. |
| 519 | | |
| 520 | | ### How to Customize the Page Templates ### |
| 521 | | |
| 522 | | The layout is a good place for global elements that should appear on |
| 523 | | every page. But elements specific to certain types of pages are better |
| 524 | | kept in page templates. These are standard Symfony template files with |
| 525 | | a special naming convention. |
| 526 | | |
| 527 | | Page template files live in the templates folder of the a module. |
| 528 | | We provide these templates "out of the box:" |
| 529 | | |
| 530 | | * homeTemplate.php |
| 531 | | * defaultTemplate.php |
| 532 | | |
| 533 | | homeTemplate.php is used by our default home page, and defaultTemplate.php |
| 534 | | is the default template if no other template is chosen. |
| 535 | | |
| 536 | | You can change the template used by a page by using the |
| 537 | | template dropdown in the breadcrumb trail. This does not delete |
| 538 | | the slots used by the previous template, so you can switch back |
| 539 | | without losing your work. |
| 540 | | |
| 541 | | How do you create your own template files? *Don't* alter the templates |
| 542 | | folder of the plugin. As always with Symfony modules, you chould |
| 543 | | instead create your own a/templates folder within your |
| 544 | | application's modules folder: |
| 545 | | |
| 546 | | mkdir -p apps/frontend/modules/a/templates |
| 547 | | |
| 548 | | Now you can copy homeTemplate.php and defaultTemplate.php to this folder, |
| 549 | | or just start over from scratch. You can also copy _login.php if you don't |
| 550 | | like the way we present the login and logout options. The same applies |
| 551 | | to _tabs.php and _subnav.php. We *do not recommend* altering the rest of the templates unless |
| 552 | | you have a clear understanding of their purpose and function and are |
| 553 | | willing to make ongoing changes when new releases are made. In general, |
| 554 | | if you can use CSS to match the behavior of our HTML to your needs, |
| 555 | | that will be more forwards-compatible with new releases of the CMS. |
| 556 | | |
| 557 | | If you add additional template files, you'll need to adjust |
| 558 | | the `app_a_templates` setting in `app.yml` so that your |
| 559 | | new templates also appear in the dropdown menu: |
| 560 | | |
| 561 | | all: |
| 562 | | a: |
| 563 | | templates: |
| 564 | | home: |
| 565 | | Home Page |
| 566 | | default: |
| 567 | | Default Page |
| 568 | | mytemplate: |
| 569 | | My Template |
| 570 | | |
| 571 | | ### Inserting Slots in Layouts and Templates ### |
| 572 | | |
| 573 | | Of course, creating layouts and templates does you little good if you |
| 574 | | can't insert user-edited content into them. This is where the |
| 575 | | CMS slot helpers come in. |
| 576 | | |
| 577 | | Here's how to insert a slot into a layout or page template: |
| 578 | | |
| 579 | | <?php # Once at the top of the file ?> |
| 580 | | <?php use_helper('a') ?> |
| 581 | | |
| 582 | | <?php # Anywhere you want a particular slot ?> |
| 583 | | <?php a_slot('body', 'aRichText') ?> |
| 584 | | |
| 585 | | Notice that two arguments are passed to the <tt>a_slot</tt> |
| 586 | | helper. The first argument is the name of the slot, which distinguishes |
| 587 | | it from other slots on the same page. *Slot names should contain only |
| 588 | | characters that are allowed in HTML ID and NAME attributes*. We recommend |
| 589 | | that you use only letters, digits, underscores and dashes in slot names. |
| 590 | | The slot name will never be seen by the user. It is a useful label |
| 591 | | such as `body` or `sidebar` or `subtitle`. |
| 592 | | |
| 593 | | The second argument is the type of the slot. "Out of the box," |
| 594 | | apostrophePlugin offers two slot types: |
| 595 | | |
| 596 | | * aRichText |
| 597 | | * aText |
| 598 | | |
| 599 | | You can add additional slot types of your own and release and |
| 600 | | distribute them as plugins as explained later in this document. |
| 601 | | |
| 602 | | The special slot name `title` is reserved for the title of the page |
| 603 | | and is always of the type `aText`. |
| 604 | | While you don't need to provide an additional editing interface |
| 605 | | for the title, you might want to insert it as an `h1` somewhere in your |
| 606 | | page layout or template as a design element: |
| 607 | | |
| 608 | | <h1> |
| 609 | | <?php a_slot('title', 'aText', |
| 610 | | array('tool' => 'basic')) ?> |
| 611 | | </h1> |
| 612 | | |
| 613 | | The behavior of most slot types can be influenced by passing |
| 614 | | options to them from the template or layout. |
| 615 | | You can also pass an array of options as a third argument |
| 616 | | to the helper, like this: |
| 617 | | |
| 618 | | <h2> |
| 619 | | <?php a_slot('subtitle', 'aRichText', |
| 620 | | array('tool' => 'basic')) ?> |
| 621 | | </h2> |
| 622 | | |
| 623 | | Here we create a subtitle area on the page which is editable, but only |
| 624 | | with the limited palette of options provided in FCK's `basic` toolbar. |
| 625 | | This works because the `aRichText` slot implementation calls |
| 626 | | the `textarea_tag` helper, passing the options array along. (These |
| 627 | | options are also available to the slot implementation at the time |
| 628 | | the slot is saved, which will be discussed in more detail later.) |
| 629 | | |
| 630 | | Incidentally, you can create your own custom toolbars for FCK as part |
| 631 | | of your FCK configuration. See the FCK documentation. Note that this |
| 632 | | does not actually prevent the user from submitting other HTML, often |
| 633 | | by copying and pasting from Microsoft Word, etc. We plan to include |
| 634 | | connectors to an HTML tidying package in a future release of the CMS to reduce |
| 635 | | the severity of this problem. |
| 636 | | |
| 637 | | In addition to passing familiar options along to the |
| 638 | | `input_tag` and `textarea_tag` helper functions, you can |
| 639 | | also pass the `multiline` option when inserting a slot |
| 640 | | of the type `aText`. When `multiline` is true, the |
| 641 | | plaintext slot is rendered with `textarea_tag` rather than |
| 642 | | `input_tag`. |
| 643 | | |
| 644 | | ### Inserting Areas: Unlimited Slots in a Vertical Column ### |
| 645 | | |
| 646 | | Slots are great on their own. But when you want to mix paragraphs of text with |
| 647 | | elements inserted by custom slots, it is necessary to create a separate |
| 648 | | template file for every page. This is tedious and requires the |
| 649 | | involvement of an HTML-savvy person on a regular basis. |
| 650 | | |
| 651 | | Fortunately apostrophePlugin also offers "areas." An |
| 652 | | area is a continuous vertical column containing multiple slots which |
| 653 | | can be managed on the fly without the need for template changes. |
| 654 | | |
| 655 | | You insert an area by calling a_include_area($name) rather than |
| 656 | | a_include_slot($name): |
| 657 | | |
| 658 | | <?php a_include_area("sidebar") ?> |
| 659 | | |
| 660 | | When you insert an area you are presented with a slightly different |
| 661 | | editing interface. At first there are no editable slots in the area. |
| 662 | | Click "Insert Slot" to add the first one. You can now edit that |
| 663 | | first slot and save it. |
| 664 | | |
| 665 | | Add more slots and you'll find that you are also able to delete them |
| 666 | | and reorder them at will. |
| 667 | | |
| 668 | | By default new slots appear at the top of an area. If you don't like this, |
| 669 | | you can change it for your entire site via `app.yml`: |
| 670 | | |
| 671 | | all: |
| 672 | | a: |
| 673 | | new_slots_top: false |
| 674 | | |
| 675 | | An area has just one version control button for the entire area. This |
| 676 | | is because creating, deleting, and reordering slots are themselves |
| 677 | | actions that can be undone through version control. |
| 678 | | |
| 679 | | In a project with many custom slot types, you may find it is |
| 680 | | inappropriate to use certain slot types in certain areas. You can |
| 681 | | specify a list of allowed slot types like this: |
| 682 | | |
| 683 | | <?php a_include_area("sidebar", |
| 684 | | array("allowed_types" => array("aText", "myCustomType"))) ?> |
| 685 | | |
| 686 | | Notice that the second argument to `a_include_area` is an |
| 687 | | associative array of options. The `allowed_types` option allows us to |
| 688 | | specify a list of slot types that are allowed in this particular area. |
| 689 | | |
| 690 | | In addition, you can pass options to the slots of each type, much as |
| 691 | | you would when inserting a single slot: |
| 692 | | |
| 693 | | a_include_area("sidebar", |
| 694 | | array("allowed_types" => array("aText", "myCustomType"), |
| 695 | | "type_options" => array( |
| 696 | | "aText" => array("multiline" => 1)))); |
| 697 | | |
| 698 | | Here the `multiline` option specifies that all |
| 699 | | `aText` slots in the area should have the |
| 700 | | `multiline` option set. |
| 701 | | |
| 702 | | ### Inserting the Breadcrumb Trail and Side Navigation ### |
| 703 | | |
| 704 | | Your layout, or possibly your page templates if you wish to handle |
| 705 | | it differently on some pages, will need to insert the breadcrumb |
| 706 | | trail and side navigation elements on some or all pages. |
| 707 | | |
| 708 | | The breadcrumb trail is inserted into a page template or layout |
| 709 | | by the following code: |
| 710 | | |
| 711 | | <?php include_component('a', 'breadcrumb') ?> |
| 712 | | |
| 713 | | Sometimes, such as on the home page, you do not want to display the breadcrumb |
| 714 | | trail at all, when the user is not logged in. But you still need it when you |
| 715 | | are are logged in so that you can manage the page. You can handle |
| 716 | | this situation like so: |
| 717 | | |
| 718 | | <?php if (aTools::getCurrentPage()->userHasPrivilege('edit')): ?> |
| 719 | | <?php include_component('a', 'breadcrumb') ?> |
| 720 | | ?> |
| 721 | | |
| 722 | | The side navigation column ("subnavigation") is inserted similarly: |
| 723 | | |
| 724 | | <?php include_component('a', 'subnav') ?> |
| 725 | | |
| 726 | | ## Global Slots ## |
| 727 | | |
| 728 | | Most of the time, you want the content of a slot to be specific to a page. |
| 729 | | After all, if the content was the same on every page, you wouldn't need |
| 730 | | more than one page. |
| 731 | | |
| 732 | | However, it is sometimes useful to have editable content that appears |
| 733 | | on more than one page. For instance, an editable page footer or page |
| 734 | | subtitle might be consistent throughout the site, or at least throughout |
| 735 | | a portion of the site. |
| 736 | | |
| 737 | | You can do this by adding a "global slot" to your page template or layout. |
| 738 | | Just set the `global` option to `true` when inserting the slot: |
| 739 | | |
| 740 | | <h4> |
| 741 | | <?php a_slot('footer', 'aRichText', |
| 742 | | array('toolbar' => 'basic', 'global' => true)) ?> |
| 743 | | </h4> |
| 744 | | |
| 745 | | The content of the resulting slot is shared by all pages that include |
| 746 | | it with the `global` option. |
| 747 | | |
| 748 | | Note that global slots can be edited only by users with editing |
| 749 | | privileges throughout the site. Otherwise users with control only over |
| 750 | | a subpage could edit a footer displayed on all pages. |
| 751 | | |
| 752 | | Have a need for a slot that is shared by some pages, but not all pages? |
| 753 | | Just name the slot appropriately. For instance, the slot name |
| 754 | | `chemistry-blurb` might be suitable for all pages in the |
| 755 | | chemistry department of an educational institution's website. |
| 756 | | |
| 757 | | ## Overriding the Stylesheets ## |
| 758 | | |
| 759 | | By default, `apostrophePlugin` provides two stylesheets which are |
| 760 | | automatically added to your pages. There's a lot happening there, |
| 761 | | particularly with regard to the editing interface, and we recommend |
| 762 | | that you keep these and override them as needed in a separate |
| 763 | | stylesheet of your own. However, you can turn them off if you wish |
| 764 | | in `app.yml`: |
| 765 | | |
| 766 | | all: |
| 767 | | a: |
| 768 | | use_bundled_stylesheet: false |
| 769 | | |
| 770 | | If you do keep our stylesheets and further override them, you'll |
| 771 | | want to specify `position: last` for the stylesheets that should |
| 772 | | override them. |
| 773 | | |
| 774 | | ## Access Control ## |
| 775 | | |
| 776 | | By default, a a site follows these security rules: |
| 777 | | |
| 778 | | * Anyone can view any page without being authenticated. |
| 779 | | * Any authenticated (logged-in) user can edit any page, |
| 780 | | and add and delete pages. |
| 781 | | |
| 782 | | This is often sufficient for simple sites. But a can |
| 783 | | also handle more complex security needs. |
| 784 | | |
| 785 | | ### Requiring Login to Access All Pages ### |
| 786 | | |
| 787 | | To require that the user log in before they view any page |
| 788 | | in the CMS, use the following setting in `app.yml`: |
| 789 | | |
| 790 | | all: |
| 791 | | a: |
| 792 | | view_login_required: true |
| 793 | | |
| 794 | | ### Requiring Login to Access Some Pages ### |
| 795 | | |
| 796 | | To require the user to log in before accessing a particular page, |
| 797 | | just navigate to that page as a user with editing privileges |
| 798 | | and click on the "lock" icon. |
| 799 | | |
| 800 | | 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: |
| 801 | | |
| 802 | | all: |
| 803 | | a: |
| 804 | | view_locked_sufficient_credentials: view_locked |
| 805 | | |
| 806 | | Then grant the `view_locked` permission to the appropriate sfGuard groups, and you'll be able to distinguish user-created accounts from invited guests. |
| 807 | | |
| 808 | | ### Requiring Special Credentials to Edit Pages ### |
| 809 | | |
| 810 | | Editing rights can be controlled in several ways: |
| 811 | | |
| 812 | | 1) Any user with the `cms_admin` credential can always |
| 813 | | edit, regardless of all other settings. Note that the |
| 814 | | sfGuard "superadmin" user always has all credentials. |
| 815 | | |
| 816 | | 2) Any user with `edit_sufficient_credentials` can always edit |
| 817 | | pages (but not add or delete them) anywhere on the site. For instance, |
| 818 | | if you add such users to the `executive_editors` sfGuardGroup and grant that |
| 819 | | group the `executive_editors` permission, then you can give them |
| 820 | | full editing privileges with these settings: |
| 821 | | |
| 822 | | all: |
| 823 | | a: |
| 824 | | edit_sufficient_credentials: executive_editors |
| 825 | | |
| 826 | | Similarly, any user with `manage_sufficient_credentials` can always |
| 827 | | add or delete pages anywhere on the site, in addition to editing content. So |
| 828 | | a more complete setting for a typical setup would be: |
| 829 | | |
| 830 | | all: |
| 831 | | a: |
| 832 | | edit_sufficient_credentials: executive_editors |
| 833 | | manage_sufficient_credentials: executive_editors |
| 834 | | |
| 835 | | 3) Any user who is a member of the group specified by |
| 836 | | `app_a_edit_group` can *potentially* be made an |
| 837 | | editor in particular parts of the site. If |
| 838 | | `app_a_edit_group` is not set, all users are |
| 839 | | potential editors: |
| 840 | | |
| 841 | | all: |
| 842 | | a: |
| 843 | | edit_group: editors |
| 844 | | |
| 845 | | Similarly, any user who is a member of the group specified by |
| 846 | | `app_a_manage_group` can potentially be given the ability to add |
| 847 | | and delete pages in a particular part of the site. So a |
| 848 | | common setup might be: |
| 849 | | |
| 850 | | all: |
| 851 | | a: |
| 852 | | edit_group: editors |
| 853 | | manage_group: editors |
| 854 | | |
| 855 | | Why is this feature useful? Two reasons: because checking their membership |
| 856 | | in one group is faster than checking their access privileges in the |
| 857 | | entire chain of ancestor pages, and because when an administrator is |
| 858 | | managing the list of users permitted to edit a page the list of users in the |
| 859 | | editors group is much easier to read than a list of all users (especially |
| 860 | | in a large system with many non-editing users). |
| 861 | | |
| 862 | | 4) Editing privileges for any specific page and its descendants |
| 863 | | can be granted to any member of the group specified by |
| 864 | | `app_a_edit_group` (if that option is set), or to |
| 865 | | any user if `app_a_edit_group` is not set. |
| 866 | | When a user with the `cms_admin` credential clicks on the "lock" icon, |
| 867 | | they are given the option of assigning editors for that page. |
| 868 | | The same principle applies to "managing" (adding and deleting) |
| 869 | | pages, with the group being indicated by |
| 870 | | your `app_a_manage_group` setting. |
| 871 | | |
| 872 | | Note that the pulldown list of possible editors can be quite long |
| 873 | | if there are thousands of people with accounts on your site! This |
| 874 | | is why we recommend setting up groups as described above. |
| 875 | | |
| 876 | | ### PublishingPages, by Choice and By Default ### |
| 877 | | |
| 878 | | a offers a "published/unpublished" toggle under "manage page settings." |
| 879 | | Pages that are unpublished are completely invisible to users who do not |
| 880 | | have at least the candidate credentials to be an editor; a user without appropriate privileges |
| 881 | | gets a 404 not found error just as if the page did not exist. In most cases |
| 882 | | you should use this in preference to actually deleting the page because the |
| 883 | | content is still available if you choose to bring it back later. |
| 884 | | |
| 885 | | By default all new a pages are in the "published" state. |
| 886 | | If you need to approach the matter more conservatively, you can easily |
| 887 | | change this with the following `app.yml` setting: |
| 888 | | |
| 889 | | all: |
| 890 | | a: |
| 891 | | default_on: false |
| 892 | | |
| 893 | | ### Refreshing Slots ### |
| 894 | | |
| 895 | | Most slots are self-contained: they contain all of the information needed to render them, with no references to an external site. However, for efficiency reasons, some slots do contain metadata such as static image URLs pointing to an external site and do not attempt to refresh that data on every single page load. Our media slots are a good example of this. |
| 896 | | |
| 897 | | Such slots always contain enough information to update themselves in response to the a:refresh task: |
| 898 | | |
| 899 | | ./symfony a:refresh --env=prod --application=frontend |
| 900 | | |
| 901 | | It's a good idea to run that task every day or so via a cron job if you are using our CMS with our media plugin and its supporting slots. |
| 902 | | |
| 903 | | For performance reasons this task only looks at the latest version of each slot. If you are carrying out a one-time transition such as moving from development to production and you know that all of your frontend controller URLs have changed as a result, you can use the `--allversions` option to specify that older versions of slots should be refreshed as well: |
| 904 | | |
| 905 | | ./symfony a:refresh --env=prod --application=frontend --allversions |
| 906 | | |
| 907 | | If you are implementing custom slot types that have a need to participate in the refresh task (see below for more information about custom slot types), just implement a public `refreshSlot` method in those slot classes. This method takes no arguments and returns nothing. |
| 908 | | |
| 909 | | IMPORTANT: command line Symfony tasks have no idea what the hostname of the site is, which is a problem for tasks like a:refresh. If you are using this task with our media slots, you will need to tell Symfony what the hostname of your site is, or what the hostname of the media plugin site is (if they are different). In most cases the CMS site and the media plugin site are one and the same. |
| 910 | | |
| 911 | | As far as our plugins are concerned, the simplest way to tell Symfony the hostname of the site is to set: |
| 912 | | |
| 913 | | all: |
| 914 | | cli: |
| 915 | | host: www.mysite.com |
| 916 | | |
| 917 | | (You can specify different settings for different environments of course.) |
| 918 | | |
| 919 | | If you want to specify the location of the media server only, you can do that this way: |
| 920 | | |
| 921 | | all: |
| 922 | | aMedia: |
| 923 | | client_site: media.mysite.com |
| 924 | | |
| 925 | | (See the media plugin README for more information about setting up a media server on a separate site.) |
| 926 | | |
| 927 | | ## Creating Custom Slot Types ## |
| 928 | | |
| 929 | | You are not limited to the two slot types provided with |
| 930 | | apostrophePlugin! Anyone can create new slot types by taking |
| 931 | | advantage of normal Symfony features: modules, components, |
| 932 | | actions, templates and Doctrine model classes. |
| 933 | | |
| 934 | | Let's look at `aText` to understand how this works. |
| 935 | | |
| 936 | | The `aText` slot type is implemented as follows: |
| 937 | | |
| 938 | | 1) *The model class.* Every slot must be stored in the database. |
| 939 | | All slot types have a model class which inherits from |
| 940 | | `aSlot`. The `aText` model class is |
| 941 | | `aTextSlot`. |
| 942 | | |
| 943 | | This is done using a feature of Doctrine called |
| 944 | | *column aggregation inheritance*. Column aggregation inheritance |
| 945 | | allows you to gain the benefits of object-oriented inheritance |
| 946 | | while still storing all of the slot types in the same database table. |
| 947 | | Doctrine manages this automatically for you. |
| 948 | | |
| 949 | | The relevant portion of `config/doctrine/schema.yml` for |
| 950 | | `aText` looks like this: |
| 951 | | |
| 952 | | aTextSlot: |
| 953 | | inheritance: |
| 954 | | extends: aSlot |
| 955 | | type: column_aggregation |
| 956 | | keyField: type |
| 957 | | keyValue: 'aText' |
| 958 | | |
| 959 | | The `extends` keyword specifies the class we are inheriting from, while |
| 960 | | the `keyValue` field must contain the name of the type. Doctrine uses |
| 961 | | this to figure out what class of object to create when loading a |
| 962 | | record from the `a_slot` table. The slot type name is |
| 963 | | recorded in the `type` column. You don't need to worry |
| 964 | | about the details, but for more information about them, |
| 965 | | see the excellent Doctrine documentation. |
| 966 | | |
| 967 | | *Note that the keyValue setting does not include the word Slot.* |
| 968 | | |
| 969 | | *Yes, you can have custom columns in the database for your type.* |
| 970 | | A plaintext slot doesn't need them because the `value` column works |
| 971 | | well for storing its contents. But you can add whatever columns |
| 972 | | suit your purposes. For example, a `aContextYoutube` slot type might |
| 973 | | have a model class schema like this: |
| 974 | | |
| 975 | | aContextYoutubeSlot: |
| 976 | | inheritance: |
| 977 | | extends: aSlot |
| 978 | | type: column_aggregation |
| 979 | | keyField: type |
| 980 | | keyValue: 'aYoutube' |
| 981 | | columns: |
| 982 | | youtube_url: string(300) |
| 983 | | youtube_auto_repeat: boolean |
| 984 | | |
| 985 | | Note that I have prefixed the extra columns with `youtube_`. This is |
| 986 | | required if you intend to release your slot type as a plugin because |
| 987 | | collisions between column names intended for use in different slot types |
| 988 | | can cause problems for other users. If your slot type is strictly for |
| 989 | | use in your own project, then you can get by without a prefix. It would |
| 990 | | be nice if Doctrine did this automatically so that the field names |
| 991 | | of subclasses were never really in conflict, but so far it does not. |
| 992 | | |
| 993 | | (You may be able to work around this issue with less typing by using |
| 994 | | Doctrine's "name: phpname as sqlname" syntax. But I have not yet tested this |
| 995 | | to see how it interacts with column aggregation inheritance. |
| 996 | | TODO: find out! If you try it, let me know.) |
| 997 | | |
| 998 | | Any columns that you add to your custom slot type are automatically |
| 999 | | included in the definition of `a_slot` when the |
| 1000 | | SQL for your database is generated by the `doctrine:build-all` |
| 1001 | | or `doctrine:build-sql` task. |
| 1002 | | |
| 1003 | | 2) *The module.* Every slot type is implemented by a Symfony module of |
| 1004 | | the same name. The `aText` slot type is implemented by the |
| 1005 | | `aText` module. Create your own modules for your own |
| 1006 | | custom slot types. However, you'll set up your actions, components and |
| 1007 | | templates in a specific way as described below. |
| 1008 | | |
| 1009 | | 3) *The components class.* The `aText` slot type's |
| 1010 | | component clas is called `aTextComponents`. Unlike |
| 1011 | | typical components classes it inherits from |
| 1012 | | `aBaseComponents`: |
| 1013 | | |
| 1014 | | class aTextComponents extends aBaseComponents |
| 1015 | | |
| 1016 | | This class typically contains two components, although the |
| 1017 | | base class versions inherited from aBaseComponents are |
| 1018 | | sometimes sufficient to do the job, depending on the behavior you want: |
| 1019 | | |
| 1020 | | * The `normalView` component. The `normalView` component displays |
| 1021 | | your slot as a normal user, not a user with editing privileges, |
| 1022 | | would see it. The default implementation simply outputs the |
| 1023 | | contents of of the `value` column of the slot as HTML, which works for |
| 1024 | | both `aRichText` and `aText` (the latter stores |
| 1025 | | its "plaintext" pre-escaped to be displayed as HTML). |
| 1026 | | |
| 1027 | | If you choose to change this behavior, you'll need to code the |
| 1028 | | `execute` method like so. Note the use of the |
| 1029 | | `$this->setup()` method, which automatically sets up |
| 1030 | | the slot object for you as well as various other conveniences: |
| 1031 | | |
| 1032 | | public function executeNormalView() |
| 1033 | | { |
| 1034 | | // Sets up $this->slot, $this->name, $this->id, etc. automatically |
| 1035 | | $this->setup(); |
| 1036 | | // Examine options with $this->getOption('optionname'), set |
| 1037 | | // various member variables for convenience in the partial, etc. |
| 1038 | | } |
| 1039 | | |
| 1040 | | The corresponding partial, `_normalView.php`, could look like this: |
| 1041 | | |
| 1042 | | <?php if (!strlen($value)): ?> |
| 1043 | | <?php if ($editable): ?> |
| 1044 | | Double-click to edit. |
| 1045 | | <?php endif ?> |
| 1046 | | <?php else: ?> |
| 1047 | | <?php echo $value ?> |
| 1048 | | <?php endif ?> |
| 1049 | | |
| 1050 | | Even though this is the non-editing view (displayed even to editors until |
| 1051 | | they double-click), the `editable` parameter is set as a convenience so that |
| 1052 | | you can determine that the user does have editing privileges. |
| 1053 | | |
| 1054 | | * The `editView` component. The `editView` component displays your |
| 1055 | | slot with appropriate editing controls. You can use Symfony 1.2+-style |
| 1056 | | form classes, but you are not required to. |
| 1057 | | |
| 1058 | | The `executeEditView` method for `aText' looks like this: |
| 1059 | | |
| 1060 | | public function executeEditView() |
| 1061 | | { |
| 1062 | | $this->setup(); |
| 1063 | | $this->multiline = $this->getOption('multiline'); |
| 1064 | | // The rest of the options array is passed as HTML |
| 1065 | | // options to the helper function, but this |
| 1066 | | // should not be |
| 1067 | | unset($this->options['multiline']); |
| 1068 | | } |
| 1069 | | |
| 1070 | | And the corresponding template, `_editView.php`, looks like this: |
| 1071 | | |
| 1072 | | <?php if ($multiline): ?> |
| 1073 | | <?php echo textarea_tag("value", |
| 1074 | | $value, |
| 1075 | | array_merge(array("id" => "$id-value"), $options)) ?> |
| 1076 | | <?php else: ?> |
| 1077 | | <?php echo input_tag("value", |
| 1078 | | $value, |
| 1079 | | array_merge(array("id" => "$id-value"), $options)) ?> |
| 1080 | | <?php endif ?> |
| 1081 | | |
| 1082 | | Note the use of the `$id` variable. This variable is guaranteed to |
| 1083 | | make an ID unique among all of the slot editing forms that may be |
| 1084 | | present on the page at any given time. Be sure to take advantage |
| 1085 | | of `$id` rather than second-guessing the process by inserting |
| 1086 | | `$name` and `$permid` yourself. |
| 1087 | | |
| 1088 | | A Symfony 1.2+ forms-based implementation of the |
| 1089 | | executeEditView method looks like this. Note that we don't |
| 1090 | | always have to create a new form object! When the user's first |
| 1091 | | attempt to fill out the form results in a validation error, |
| 1092 | | the form object is automatically restored to `$this->form`. |
| 1093 | | When you output that form object again, the validation errors |
| 1094 | | are displayed just as they would be if you were using |
| 1095 | | ordinary action templates. Neat and tidy. |
| 1096 | | |
| 1097 | | Since Symfony generates form fields with ID attributes, |
| 1098 | | it is necessary to set the form's name format in a way that |
| 1099 | | will not conflict with other slot editing forms hidden in |
| 1100 | | the page. This is the purpose of the `setId()` call below. |
| 1101 | | Don't worry, you'll see the form class code that |
| 1102 | | implements that method in a moment. |
| 1103 | | |
| 1104 | | public function executeEditView() |
| 1105 | | { |
| 1106 | | $this->setup(); |
| 1107 | | // If there is already a form object reuse it! It contains |
| 1108 | | // validation errors from the user's first attempt to submit it. |
| 1109 | | if (!isset($this->form)) |
| 1110 | | { |
| 1111 | | $this->form = new FvtestForm(); |
| 1112 | | |
| 1113 | | // Be sure to show the current value. |
| 1114 | | |
| 1115 | | $this->form->setDefault('count', $this->slot->value); |
| 1116 | | |
| 1117 | | // Necessary to prevent HTML id collisions between multiple slots |
| 1118 | | // on the same page (see the setId method of the FvtestForm class) |
| 1119 | | $this->form->setId($this->id); |
| 1120 | | } |
| 1121 | | } |
| 1122 | | |
| 1123 | | You'll note that we explicitly call `setDefault` to redisplay the |
| 1124 | | current value of the slot. This raises an interesting question: if |
| 1125 | | we were taking full advantage of column aggregation inheritance |
| 1126 | | by using the Doctrine form that Symfony auto-generates for our |
| 1127 | | slot model class, could we skip this step and also avoid the |
| 1128 | | need to create our own form class? |
| 1129 | | |
| 1130 | | Unfortunately, as of this writing the answer is no. Doctrine |
| 1131 | | column aggregation inheritance is a beautiful thing, but it doesn't |
| 1132 | | currently generate good forms. That's because the forms generated |
| 1133 | | for the subclasses contain *all* of the fields for *all* of the |
| 1134 | | subclasses. Obviousy, that's not desirable. So build your own |
| 1135 | | form classes to edit your custom slot types... like this one: |
| 1136 | | |
| 1137 | | class FvtestForm extends sfForm |
| 1138 | | { |
| 1139 | | public function configure() |
| 1140 | | { |
| 1141 | | $this->setWidgets(array( |
| 1142 | | "count" => new sfWidgetFormInput(array()) |
| 1143 | | )); |
| 1144 | | $this->setValidators(array( |
| 1145 | | "count" => new sfValidatorInteger(array( |
| 1146 | | 'min' => 10, 'max' => 20, 'required' => true)))); |
| 1147 | | $this->widgetSchema->setFormFormatterName('table'); |
| 1148 | | } |
| 1149 | | public function setId($id) |
| 1150 | | { |
| 1151 | | $this->widgetSchema->setNameFormat("Fvtest-$id" . "[%s]"); |
| 1152 | | } |
| 1153 | | } |
| 1154 | | |
| 1155 | | Note the `setId()` method which takes care of ensuring that each form |
| 1156 | | field has an ID attribute that is unique throughout the document. |
| 1157 | | |
| 1158 | | The corresponding `_editView.php` template is as follows |
| 1159 | | (very simple indeed): |
| 1160 | | |
| 1161 | | <table> |
| 1162 | | <?php echo $form ?> |
| 1163 | | </table> |
| 1164 | | |
| 1165 | | But how does the form get bound and saved? That's the topic of |
| 1166 | | the next section. |
| 1167 | | |
| 1168 | | 4) *The actions class.* The `aText` slot type's action |
| 1169 | | class is called `aTextActions`. Unlike most action classes, |
| 1170 | | it inherits from `aBaseActions`, like so: |
| 1171 | | |
| 1172 | | class aTextActions extends aBaseActions |
| 1173 | | |
| 1174 | | Our `aTextActions` class must contain at least |
| 1175 | | one action, the edit action. This action is invoked when the user clicks |
| 1176 | | "save" after editing the slot. |
| 1177 | | |
| 1178 | | The `aBaseActions` class provides two private methods that |
| 1179 | | help you code `executeEdit` correctly: |
| 1180 | | |
| 1181 | | * `editSetup`, which locates the appropriate options for the slot and loads |
| 1182 | | or creates a slot object. |
| 1183 | | * `editSave`, which does the hard work of managing version |
| 1184 | | control while saving the slot. |
| 1185 | | |
| 1186 | | Call `$this->editSetup()` at the beginning of your `executeEdit` method, |
| 1187 | | and *return the result* of `$this->editSave()` at the end. If the user's |
| 1188 | | input is unacceptable and you want them to try again, |
| 1189 | | *return the result* of a call to `$this->editRetry()` instead. |
| 1190 | | |
| 1191 | | In between, all you have to do is look at the appropriate |
| 1192 | | request parameters provided by your `_editView.php` template and |
| 1193 | | set the appropriate fields of `$this->slot`. You can store your data in |
| 1194 | | `$this->slot->value` if a variable-length string suits all of your needs |
| 1195 | | (as it often does, especially with PHP's `serialize()` and unserialize()`). |
| 1196 | | Or you can use the custom fields you defined when designing the schema |
| 1197 | | for your model class. This has the advantage that you can more easily |
| 1198 | | look for that information via database queries later. |
| 1199 | | |
| 1200 | | For instance, for the Youtube slot mentioned earlier: |
| 1201 | | |
| 1202 | | public function executeEdit(sfRequest $request) |
| 1203 | | { |
| 1204 | | $this->editSetup(); |
| 1205 | | $url = $this->getRequestParameter('url'); |
| 1206 | | $autorepeat = $this->getRequestParameter('auto_repeat'); |
| 1207 | | $this->slot->youtube_url = $url; |
| 1208 | | $this->slot->youtube_autorepeat = $autorepeat; |
| 1209 | | // Note that we must return the result! |
| 1210 | | return $this->editSave(); |
| 1211 | | } |
| 1212 | | |
| 1213 | | The Symfony 1.2+ forms approach is similar. Note that we once |
| 1214 | | again take advantage of the `setId()` method we added to our |
| 1215 | | form class earlier. We also need to take the unique ID of the |
| 1216 | | slot form into account when calling `bind()`: |
| 1217 | | |
| 1218 | | public function executeEdit(sfRequest $request) |
| 1219 | | { |
| 1220 | | $this->editSetup(); |
| 1221 | | $this->form = new FvtestForm(); |
| 1222 | | $this->form->setId($this->id); |
| 1223 | | $this->form->bind($request->getParameter("Fvtest-" . $this->id)); |
| 1224 | | if ($this->form->isValid()) |
| 1225 | | { |
| 1226 | | $this->slot->value = $this->form->getValue('count'); |
| 1227 | | return $this->editSave(); |
| 1228 | | } |
| 1229 | | else |
| 1230 | | { |
| 1231 | | // Automatically passes $this->form on to the |
| 1232 | | // next iteration of the edit form so that |
| 1233 | | // validation errors can be seen |
| 1234 | | return $this->editRetry(); |
| 1235 | | } |
| 1236 | | } |
| 1237 | | |
| 1238 | | To simplify validation, `$this->form` is automatically |
| 1239 | | provided to the `editView` component when you call `editRetry()`, |
| 1240 | | |
| 1241 | | ### Beyond edit: Additional Actions ### |
| 1242 | | |
| 1243 | | Note that your actions class may contain other actions if you wish. Typically |
| 1244 | | these are AJAX actions and other auxiliary actions that share the work |
| 1245 | | of editing the slot object. |
| 1246 | | |
| 1247 | | You may also have actions that perform AJAX updates of the normal, non-editing |
| 1248 | | view of the slot. |
| 1249 | | |
| 1250 | | In these cases, you may need to be able to retrieve the slot again and |
| 1251 | | refresh a portion of your display. For actions that modify the slot, |
| 1252 | | you can just call `this->editSetup()` to get all of the necessary parameters |
| 1253 | | and initialize `$this->slot`. But what about actions that don't modify |
| 1254 | | the slot but nevertheless need to access its contents? Such actions |
| 1255 | | can call `$this->setup()` instead. The `setup()` method is equivalent |
| 1256 | | to `editSetup()`, except that it allows access by users who do not have |
| 1257 | | editing privileges unless you expressly pass the value `true` for the |
| 1258 | | `$editing` parameter. (It does check for view access.) |
| 1259 | | |
| 1260 | | ### Custom Validation ### |
| 1261 | | |
| 1262 | | Sometimes `$this->form` isn't enough to meet your needs. You might |
| 1263 | | have more than one Symfony 1.2+ form in the slot (although you should |
| 1264 | | look at `embedForm()` and `mergeForm()` first before you say that). |
| 1265 | | Or you might not be using Symfony 1.2+-style forms at all. |
| 1266 | | |
| 1267 | | Fortunately there's a way to pass validation messages from the |
| 1268 | | `executeEdit` action to the next iteration of the `editView` component: |
| 1269 | | |
| 1270 | | // Set it in the action |
| 1271 | | $this->validationData['custom'] = 'My error message'; |
| 1272 | | |
| 1273 | | // Grab it in the component |
| 1274 | | $this->error = $this->getValidationData('custom'); |
| 1275 | | |
| 1276 | | // ... And display it in the template |
| 1277 | | <?php if ($error): ?> |
| 1278 | | <h2><?php echo $this->error ?></h2> |
| 1279 | | <?php endif ?> |
| 1280 | | |
| 1281 | | Note that `$this->validationData['form']` is used internally |
| 1282 | | to store `$this->form`, if it exists in the action. So we suggest |
| 1283 | | that you use other names for your validation data fields. |
| 1284 | | |
| 1285 | | ### Overriding the Outline Box and Double-Click Behavior ### |
| 1286 | | |
| 1287 | | By default, when a user with editing privileges is viewing a slot, |
| 1288 | | the slot has an outline box and can be double-clicked to display |
| 1289 | | the editing view. |
| 1290 | | |
| 1291 | | This works well for many slot types but might not be appropriate |
| 1292 | | for yours. If you wish to implement a different behavior for switching to |
| 1293 | | the editor, such as an "Edit" button, just add this method to |
| 1294 | | your slot's model class: |
| 1295 | | |
| 1296 | | public function isOutlineEditable() |
| 1297 | | { |
| 1298 | | return false; |
| 1299 | | } |
| 1300 | | |
| 1301 | | This will turn off the "double-click to edit" behavior and outline box. |
| 1302 | | |
| 1303 | | You can replace this with the following or similar in your |
| 1304 | | `_normalView.php` template: |
| 1305 | | |
| 1306 | | <?php if ($editable): ?> |
| 1307 | | <?php echo button_to_function('Edit', $showEditorJS) ?> |
| 1308 | | <?php endif ?> |
| 1309 | | |
| 1310 | | Note that `$showEditorJS` comes preloaded with ready-to-run JavaScript code |
| 1311 | | to hide the normal view and display the editing view. |
| 1312 | | |
| 1313 | | You can also turn off the outline box for a particular insertion of a slot |
| 1314 | | by padding the `outline_editable` option to the slot helper with |
| 1315 | | a value of `false`. Explicit settings for this option override |
| 1316 | | what is returned by the `isOutlineEditable` module. |
| 1317 | | |
| 1318 | | ### Overriding the default view for a new slot ### |
| 1319 | | |
| 1320 | | By default, when a user adds a new slot to an area the user must then click the |
| 1321 | | edit button before making changes to the slot. To take the user straight to |
| 1322 | | the editView of your slot override $editDefault in your slots model. For an |
| 1323 | | example refer to PluginaTextSlot.class.php. |
| 1324 | | |
| 1325 | | ### When You Don't Want an Inline Editor ### |
| 1326 | | |
| 1327 | | Most of the time, inline editors are great. But sometimes you might be |
| 1328 | | happier with a full-page interface which eventually redirects back |
| 1329 | | to a when the work is done. In such cases you'll want to |
| 1330 | | display a link to that editor in the normal view template when the user |
| 1331 | | has editing privileges. To make that work, just link to your |
| 1332 | | standalone editor page like this: |
| 1333 | | |
| 1334 | | <?php if ($editable): ?> |
| 1335 | | <?php echo link_to("Edit This", "http://my/editor/page") ?> |
| 1336 | | <?php endif ?> |
| 1337 | | |
| 1338 | | Then, when the editing is complete, your editor will need to return |
| 1339 | | the information to a by redirecting the browser |
| 1340 | | or POSTing a form to a URL constructed like this: |
| 1341 | | |
| 1342 | | url_for("yourSlotModuleName/edit?" . http_build_query( |
| 1343 | | array( |
| 1344 | | "name" => $name, |
| 1345 | | "slug" => $slug, |
| 1346 | | "permid" => $permid, |
| 1347 | | "noajax" => 1))) |
| 1348 | | |
| 1349 | | The easiest way to accomplish this is to pass the complete edit-action |
| 1350 | | URL as a parameter when linking to your external editor. This code |
| 1351 | | demonstrates how we do it for our own aContextMediaSlot, which |
| 1352 | | integrates with apostrophePlugin without the need for any |
| 1353 | | CMS-specific code in apostrophePlugin: |
| 1354 | | |
| 1355 | | <?php if ($editable): ?> |
| 1356 | | <?php echo link_to('Choose media', |
| 1357 | | sfConfig::get('app_media_site', false) . "aMedia/select?" . |
| 1358 | | http_build_query( |
| 1359 | | array("multiple" => true, |
| 1360 | | "after" => url_for("aMedia/edit?" . |
| 1361 | | http_build_query( |
| 1362 | | array( |
| 1363 | | "name" => $name, |
| 1364 | | "slug" => $slug, |
| 1365 | | "permid" => $permid, |
| 1366 | | "noajax" => 1))))), |
| 1367 | | array('class' => 'a-context-button')) ?> |
| 1368 | | <?php endif ?> |
| 1369 | | |
| 1370 | | Note the use of the `noajax` parameter. This suppresses the |
| 1371 | | usual "refresh the slot without refreshing the whole page" behavior, |
| 1372 | | which is not appropriate after we've already left the page to |
| 1373 | | display an external editing page. When `noajax` is set, |
| 1374 | | the user is redirected to the updated page instead. This is |
| 1375 | | described in more detail in the next section. |
| 1376 | | |
| 1377 | | If you want your slot to work as a global slot, you'll also need |
| 1378 | | to be sure to pass the slug of the actual page as a parameter |
| 1379 | | of your `after` URL. As part of a call to `http_build_query` it |
| 1380 | | looks like this: |
| 1381 | | |
| 1382 | | "actual_slug" => aTools::getRealPage()->getSlug() |
| 1383 | | |
| 1384 | | You'll also want to turn off the "double-click to edit" behavior that |
| 1385 | | would otherwise open the inline editor, as explained in the |
| 1386 | | previous section. |
| 1387 | | |
| 1388 | | ### When You Don't Want AJAX ### |
| 1389 | | |
| 1390 | | Normally a displays the updated contents of the slot without |
| 1391 | | refreshing the entire page. If this is unsuitable for your purposes, |
| 1392 | | as will be the case if you are not using an inline editor, |
| 1393 | | then just include a <tt>noajax</tt> parameter in the request received by |
| 1394 | | your edit action, with a value of 1. The edit action will then |
| 1395 | | redirect to the updated page rather than attempting to refresh |
| 1396 | | only the updated slot. |
| 1397 | | |
| 1398 | | ## Adding New Global Admin Buttons to the Top Bar ## |
| 1399 | | |
| 1400 | | When a user with admin privileges is logged in, a bar appears at the top of each page offering links to useful facilities such as the sfGuardUser admin module. Other plugins such as `apostrophePlugin` extend this button bar with additional links. You can add links of your own. |
| 1401 | | |
| 1402 | | 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: |
| 1403 | | |
| 1404 | | class aMediaCMSSlotsTools |
| 1405 | | { |
| 1406 | | // You too can do this in a plugin dependent on a, see |
| 1407 | | // the provided stylesheetfor how to correctly specify an icon to go |
| 1408 | | // with your button. See theapostrophePluginConfiguration class for the |
| 1409 | | // registration of the event listener. |
| 1410 | | static public function getGlobalButtons() |
| 1411 | | { |
| 1412 | | aTools::addGlobalButtons(array( |
| 1413 | | new aGlobalButton('Media', 'aMedia/index', 'a-media'))); |
| 1414 | | } |
| 1415 | | } |
| 1416 | | |
| 1417 | | The first argument to the `aGlobalButton` constructor is the label of the button. The second |
| 1418 | | is the action (in your own code, typically). And the third is a CMS class to be added to the button, |
| 1419 | | which is typically used to supply your own icon and a left offset for the image to |
| 1420 | | reside in. |
| 1421 | | |
| 1422 | | Now, in your plugin or project's `config/config.php` or in the initialize method of your plugin or project's configuration class, make the following call to register interest in the event: |
| 1423 | | |
| 1424 | | // Register an event so we can add our buttons to the set of global |
| 1425 | | // CMS back end admin buttons that appear when the apostrophe is clicked. |
| 1426 | | $this->dispatcher->connect('a.getGlobalButtons', |
| 1427 | | array('aMediaCMSSlotsTools', 'getGlobalButtons')); |
| 1428 | | |
| 1429 | | The bar at the top of each page will now feature your additional button or buttons. |
| 1430 | | |
| 1431 | | *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. |
| 1432 | | |
| 1433 | | ## Engines: Grafting Symfony Modules Into the CMS Page Tree ## |
| 1434 | | |
| 1435 | | 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. |
| 1436 | | |
| 1437 | | *Currently a particular engine can only be placed at a single location in the site.* This is a consequence of the way routing rules are cached in Symfony. We are working to overcome this limitation. However, engine pages are still quite useful with this limitation, as they allow components such as a media plugin or people section to be located at the point in the site where the client wishes to put them without the need to edit configuration files. |
| 1438 | | |
| 1439 | | Engine modules are written using normal actions and templates and otherwise-normal routes of the aRoute class. |
| 1440 | | |
| 1441 | | This is a very powerful way to integrate non-CMS pages into your site. The media browser of apostrophePlugin will soon be rewritten to take advantage of it. |
| 1442 | | |
| 1443 | | Engines should always be used when you find yourself wishing to create a tree of "virtual pages" beginning at a point somewhere within the CMS page tree. |
| 1444 | | |
| 1445 | | 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`. |
| 1446 | | |
| 1447 | | NOTE: if your actions class has a `preExecute` method, be sure to call `parent::preExecute` from that method. Otherwise it will not work as an engine. |
| 1448 | | |
| 1449 | | 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`: |
| 1450 | | |
| 1451 | | # Engine rules must precede any catch-all rules |
| 1452 | | |
| 1453 | | enginetest_index: |
| 1454 | | url: / |
| 1455 | | param: { module: enginetest, action: index } |
| 1456 | | class: aRoute |
| 1457 | | |
| 1458 | | enginetest_foo: |
| 1459 | | url: /foo |
| 1460 | | param: { module: enginetest, action: foo } |
| 1461 | | class: aRoute |
| 1462 | | |
| 1463 | | enginetest_bar: |
| 1464 | | url: /bar |
| 1465 | | param: { module: enginetest, action: bar } |
| 1466 | | class: aRoute |
| 1467 | | |
| 1468 | | enginetest_baz: |
| 1469 | | url: /baz |
| 1470 | | param: { module: enginetest, action: baz } |
| 1471 | | class: aRoute |
| 1472 | | |
| 1473 | | 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: |
| 1474 | | |
| 1475 | | enginetest_action: |
| 1476 | | url: /:action |
| 1477 | | param: { module: enginetest } |
| 1478 | | class: aRoute |
| 1479 | | |
| 1480 | | You can also use Doctrine routes. Configure them as you normally would, but set the class name to aDoctrineRoute: |
| 1481 | | |
| 1482 | | a_event_show: |
| 1483 | | url: /:slug |
| 1484 | | param: { module: aEvent, action: show } |
| 1485 | | options: { model: Event, type: object } |
| 1486 | | class: aDoctrineRoute |
| 1487 | | requirements: { slug: '[\w-]+' } |
| 1488 | | |
| 1489 | | |
| 1490 | | In general, you may use all of the usual features available to Symfony routes. |
| 1491 | | |
| 1492 | | 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. |
| 1493 | | |
| 1494 | | That is, if an engine page is located here: |
| 1495 | | |
| 1496 | | /test1 |
| 1497 | | |
| 1498 | | And the user requests the following URL: |
| 1499 | | |
| 1500 | | /test1/foo |
| 1501 | | |
| 1502 | | 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: |
| 1503 | | |
| 1504 | | /foo |
| 1505 | | |
| 1506 | | To the appropriate rule. |
| 1507 | | |
| 1508 | | 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 `/`. |
| 1509 | | |
| 1510 | | 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. |
| 1511 | | |
| 1512 | | 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: |
| 1513 | | |
| 1514 | | echo link_to('Bar', 'enginetest/bar') |
| 1515 | | |
| 1516 | | To make the user interface aware of your engine, add the following to `app.yml`: |
| 1517 | | |
| 1518 | | all: |
| 1519 | | a: |
| 1520 | | engines: |
| 1521 | | '': 'Template-Based' |
| 1522 | | enginetest: 'Engine Test' |
| 1523 | | |
| 1524 | | 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. |
| 1525 | | |
| 1526 | | 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? |
| 1527 | | |
| 1528 | | Just call `link_to` exactly as you did before: |
| 1529 | | |
| 1530 | | echo link_to('Bar', 'enginetest/bar') |
| 1531 | | |
| 1532 | | 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. |
| 1533 | | |
| 1534 | | 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: |
| 1535 | | |
| 1536 | | <?php if (aPageTable::getFirstEnginePage('enginetest')): ?> |
| 1537 | | <?php echo link_to('Bar', 'enginetest/bar') ?> |
| 1538 | | <?php endif ?> |
| 1539 | | |
| 1540 | | Thissystem works very well as long as there is only one engine page per engine in the site, and therefore no ambiguity about where the link should go. Currently engine support is limited to one engine page per engine in any case. However, we are working on this limitation and hope to soon offer a way to specify a particular engine page as a destination for the link. This will be useful in situations where you wish to instantiate the same engine more than once, possibly with page-specific settings. However, note that you can already have any number of "subpages" of your engine page using engine routes as described above. You can write a complete database-driven sub-application in an engine, with many sub-"pages." The only thing you currently can't do is insert that engine into two completely distinct portions of your CMS page tree. |
| 1541 | | |
| 1542 | | After executing a `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. |
| 1543 | | |
| 1544 | | Note that engine pages can be moved about the site using the normal drag and drop interface. |
| 1545 | | |
| 1546 | | You can create your own subnavigation within your engine page. We suggest overriding appropriate portions of your page layout via Symfony slots. |
| 1547 | | |
| 1548 | | ## Internationalization ## |
| 1549 | | |
| 1550 | | Internationalization is supported at a basic level: separate versions |
| 1551 | | of content are served depending on the result of calling getCulture() |
| 1552 | | for the current user. When you edit, you are editing the version of |
| 1553 | | the content for your current culture. The user's culture defaults, as |
| 1554 | | usual, to the sf_default_culture settings.yml setting. The search index also |
| 1555 | | distinguishes between cultures. Webmasters who make use of internationalization will want |
| 1556 | | to add a "culture switcher" to their sites so that a user interface is |
| 1557 | | available to make these features visible. Thanks to Quentin |
| 1558 | | Dugauthier for his assistance in debugging these features. |
| 1559 | | |
| 1560 | | ## Support ## |
| 1561 | | |
| 1562 | | You can obtain support for a via the [a Google group](http://groups.google.com/group/a). |
| 1563 | | |
| 1564 | | Also be sure to [follow our Twitter account](http://twitter.com/apostrophenow). |
| 1565 | | |
| 1566 | | And be sure to [visit the Apostrophe Now site](http://www.apostrophenow.com/). |
| 1567 | | |
| 1568 | | Apostrophe is our CMS product based on the open-source a plugin. |
| | 5 | For complete and up to date information. |