February 16th, 2010

Symfony live: John Cleveley on the admin generator

Tom Boutell
Chief Software Architect
John Cleveley @jcleveley on the Symfony admin generator @jcleveley gave an excellent talk on the Symfony admin generator at #sflive2010. Here are my notes.

View more presentations from jcleveley.
He's using Zend Framework at the moment but has done a lot of good Symfony work in the past

"Backend development" photo of a zoopeeper holding up a trash can to an elephant's rump very nice.

John worked with the NHS for a while, they were using Excel spreadsheets for complex workflows, web apps were a big change for the better

A big fan of the admin generator: "it's super awesome"

Provides most common admin requirements out of the box (CRUD: Create, Retrieve, Update, Delete actions to manage more or less any type of information)

Can be extended to meet bespoke needs

What's new since 1.0? "Completely re-written for the form framework"

Many-to-many relations work without further configuration (*cough* as long as you don't do embedded forms) You don't have to expressly explain refClasses to the system generator.yml is validated, finds errors for you Batch actions (delete many easily) - wasn't this in 1.0? Less reliance on generator.yml, more DRY, the schema and form framework already spell out a lot. More templates and actions to override Separate configuration for 'edit' and 'new' actions in generator.yml Adds a REST route for your module (doctrine route collection)

New PHP configuration file in your module: lib/newsGeneratorConfiguration.class.php

You still want to look in the cache for generated examples to override

First access it, then look at it: cache/frontend/your/module/etc

Held a vote: do you like configuration in PHP or in YAML? More and more he prefers PHP. I have come to like PHP because I'm not typing symfony cc all the darn time (well I wouldn't be). I voted for PHP. Of course YAML is easier to sell to administrators who are not programmers.

Think requirements, don't jump to use the admin generator. What do the users actually need to do?

(I've been on the other side of this sometimes though. "Oh this is a really simple module, I want to customize it a lot and I don't need the admin generator." Oops, now I have lots of filters and I'm reinventing the admin generator when I could have customized it)

In general, if it doesn't feel like CRUD, you shouldn't use it.

@jcleveley says it's not good news to allow use by non-trusted admins, or very heavy use. But the admin generator is generated code, not metacode... performance should be pretty good actually. We've had good success overriding methods with additional security checks and you can also use security.yml

Admin generator 10 commandments:
  • Understand the user's workflow and customize admin to suit
  • Think about security from the start
  • Look through and understand the cached (autogenerated) PHP files
  • Change table_method to reduce db calls. By default, on the list page, there are no joins being made. You should substitute a new table_method that does any needed joins to fix performance
  • Use bespoke Form class for admin if you don't want to just let them edit all fields of the model, you can set which form class in generator.yml
  • Keep all form configuration in the Form class if you can, not generator.yml
  • If you need to make changes to multiple admin modules - create a theme. (We have a nice one, take a look at the aAdmin theme in apostrophePlugin)
  • Think about small screens and target browser. NHS had tiny screens and IE6. Be realistic about your client's environment, not your favorite one
  • Create functional tests - guard against regression. No excuses
  • Maintain good MVC and decoupling practices. No HTML in the model, no queries in the partials (realistically you do wind up with some get() calls on objects to get their fields, but no heavy model code)
Major Admin Generator Points

1. Think about the URL. Clients don't like:

application.com/admin.php

Try:

application.com/admin

Discussed web/.htaccess rewrite rules. His preferred method is a separate virtualhost that goes to an admin.php front controller.

(We at P'unk Avenue prefer not to have a separate admin controller and app, we prefer to customize the user experience to the user inside a single Symfony app, which makes the URLs even friendlier and the experience more contextual)

2. Dynamic MaxPerPage

People like a choice about how much information appears on one page of the list view.

Add a select box within the _list_header.php "records per page" Override getPagerMaxPerPage

3. Adding relations

Fewest clicks for common tasks Relevant data all placed together Currently not directly supported by the generator But: you can use mergeForm, embedForm, and sfFormDoctrine::embedRelation.

(embedForm has big problems. Many-to-many in the embedded form will not save properly, you must override updateObject and implement your own link/unlink stuff)

You don't want to switch back and forth between two admin generators for, say, employees and phone numbers. You want phone numbers in the context of editing employees.

Apparently the solution is embedRelation in your form class configure method:
public function configure()
{
  $this->embedRelation('PhoneNumbers');
}
Make sure you hide the id:
$this->widgetSchema['employee_id'] = new sfWidgetFormInputHidden();
This lets you edit but it doesn't let you add and delete. What to do?

There's a symfony plugin that contributes delete and add features for embedded relations:

ahDoctrineEasyEmbeddedRelationsPlugin by Daniel Lohse

Has created embedRelations (plural) method

Gives you embedded forms with new and delete capabilities

4. Internationalization

Ha: demonstrated an xliff internationalization file for Vulcan

You tell the admin generator to use it:
generator:
  class: sfDoctrineGenerator
  param:
    i18n_catalogue: startrek
5. Tidy up filters

Filters work great but the default style is a bit off on a small screen (800x600 at NHS ouch!)

Fortunately there is a lot of CSS provided so it's easy to move them to go across the top.

Again, peek at aAdmin for a nice example.)

6. Timestampable fields

Unset created_at and updated_at. Or make them static:

Override the template, or...

Create a plaintext widget

sfWidgetFormPlain widget (Stephe.Ostrow has submitted it for the formextra plugin, not in there yet; I wound up doing this for a client project too)

trac.symfony-project.org/attachment/ticket/7963

7. Pre-filter the list
list:
  object_actions:
    _edit: ~
    viewPhones: { label: Phone numbers, action: viewPhones }
Set filter attribute in user session. This is a bit complex. (Not certain how this is "pre-filtering")

8. Row-level ownership

Only allow owners of objects access. Example presumes sfGuard plugin and there is a user_id field as a foreign key on each object
protected function buildQuery() {
  $query = parent::buildQuery();
  $query->andWhere('user_id = ?', $this->getUser()->getId();
}
preExecute() {
  if the action is not new, check getUser()->isOwner($object)
  which looks for a user_id on the object, PHP is polymorphic
  so we don't care what class it is
}
(Our preferred solution is a userHasPrivilege module on the model class, which looks at the currently logged in user if no sfGuardUser object is passed. Our standard privilege names are 'new', 'edit', 'delete' and others as needed for that particular model. This is very extensible and allows the model to decide what is appropriate for a particular object)

Remove the user_id widget from the form of course, and in doUpdateObject (not updateObject ?), set the user id. Get the user id from the context -> getUser() -> getId()

Cute satan graphic pitchforking the sfContext call, you can inject the user into the form constructor instead

9. Custom filters

Find out who has a birthday today

addBirthdayTodayColumnQuery($query, $field, Value)

(Is this auto-sniffed or is there something in generator.yml?)

sfAdminDashPlugin. Joomla style admin dashboard with configurable admin navigation. Replaces the admin css. Manyally add header component and footer partial to layout. Nice if you don't have time to bespoke. By Kevin Bond.

sfAdminThemejRollerPlugin by Gerald Estadieu

Looks stunning, jQuery, theme roller system, popup filters, abs in edit view, completely new admin theme

Extending Methods

Sometimes you want to just define an alternate CSS file in generator.yml.

You can also override templates and actions, but you can't re-use that between modules (unless you move it to a theme). Can become untidy and hard for other devs to follow

So: create a theme. "Steep learning curve - PHP in PHP!"
mkdir -p data/generator/sfDoctrineModule/newtheme
Then copy the generator files from sfDoctrine plugin

[Screenshot of path. He's on Windows]

Extending admin events
admin.pre_execute: notified before any action is executed.
admin.build_criteria: helps you filter criteria used for list view. An alternative to overriding buildQuery.
admin.save_object and admin.delete_object
The future: "so, what does django do?"

You get a dashboard with all of your modules laid out. "Show related models" is more remarkable, you automatically get embedded forms for related models that let you manage connected objects. Object history. Delete related objects warning: warns you about what ON DELETE CASCADE is about to do to various objects.

Future: scope and features

Better support for embedded forms sfGrid Fulltext search Save and list! Should act like save button in any other application! Dashboard Nested sets and ordering as standard features Inherit from multiple themes. Right now you can only have one

(Symfony 2.0's "everything is a plugin and a dependency injection container" philosophy should yield great things here)


Check out Apostrophe, our Content Management System for Symfony!
Tom Boutell
Chief Software Architect

Check out another article
February 11th, 2010
Apostrophe 1.0!
February 10th, 2010
Oh So Close!