working with the symfony admin generator
DESCRIPTION
Advanced usage of the symfony admin generator.TRANSCRIPT
Working with the admin generator
My background
John Cleveley Trained as a Systems Engineer with BAE SystemsStarted my own business in 2006 Created symfony apps for NHS, V&A Museum and Hornby. Currently work at the BBC using their Forge platform (ZF)
Objectives
Provide information beyond the docsUpdate on what’s new Suggest a few best practicesReal world examples of customisingHow to get the best from the form frameworkUtilise the various ways of extending
It’s super awesome.
Saves huge amount of development time and costsProvides most common admin requirements out of the boxCan be extended to provide bespoke needsFully tested and documentedIt’s free!
What’s new since 1.0?
Completely re-written for the form frameworkRelationships including m2m work without configurationGenerator.yml is validatedBatch actions (delete)Less reliance on generator.yml (DRY)More templates and actions to overrideDifferent configuration for the edit and new formAdds a REST route for your module
New PHP configuration file
Configure generator via PHP to be more dynamic: lib/newsGeneratorConfiguration.class.php
The generated class in the cache lists functions cache/backend/dev/modules/autoNews/lib/BaseNewsGeneratorConfiguration.class.php
You can also mix configuration between the two
New helper file
getUrlForAction ()linkToDelete () linkToEdit ()linkToList () linkToNew ()linkToSave () linkToSaveAndAdd ()
Create custom links with extra javascript etc
lib/[module]GeneratorHelper.class.php
Provides html snippets for action links
Your thoughts?
PHP! YAML!
Is it the right tool?
…maybe.
Think requirements!
Don’t jump to use the admin generatorAnalyse what’s needed firstA bespoke solution may be more appropriateMisusing the admin generator could cause big problems in the future
How do we decide?
AdminNormal CRUD operationsNon – technical users need to add dataTrusted site administrators Ownership of all records
BespokePublic interface to data Sophisticated sorting and searching of data
The admin site can take you a long way….…. But be careful it doesn’t become a mess.
The 10 Commandmentsadmin
10 Commandments (1-5)
1. Understand the client’s workflow and customise admin to suit
2. Think about security from the start
3. Look through and understand the cached phpfiles
4. Change table_method to reduce db calls
5. Use bespoke Form class for admin if different
10 Commandments (6-10)
6. Keep all form form configuration in the Form Class
7. If you need to make changes to multiple admin modules – create a theme.
8. Think about small screens and target browser
9. Create functional tests – guard against regression
10. Maintain good MVC and decoupling practices
What’s the object-oriented way to become wealthy?
Inheritance!
John’s Top tips!
1. The URL
Clients don’t like using: application.com/admin.php
Option 1: application.com/admin
Option 2: admin.application.com
1. The URL: /admin
RewriteCond %{REQUEST_URI} ^/admin/?
RewriteRule ^(.*)$ admin.php [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]
homepage:
url: /admin
param: { module: default, action: index }
test:
url: /admin/test
...
Change all your standard routes – routing.yml
Modify web/.htaccess
1. The URL: /admin
Remove script name in urls – settings.yml
prefix_path: /admin/module_name
prod:.settings:
no_script_name: true
Change your route collections – routing.yml
1. The URL: admin.app
Tell symfony not to output admin.php in urls
Prod
.settings:no_script_name: true
Create a new virtual host in httpd.conf<VirtualHost *:80>
ServerName admin.application.comDirectoryIndex admin.phpDocumentRoot "/path/to/web/folder"<Directory "/path/to/web/folder">
AllowOverride AllAllow from All
</Directory></VirtualHost>
2. Dynamic MaxPerPage
Add a select box within the _list_header.php
public function executeChangeMaxPage(sfWebRequest $request){
$this->getUser()->setAttribute('maxPage',$request->getParameter('maxPage'));
$this->redirect($request->getReferer());}
Add an action to set a user attribute
js onchangesubmits to action
2. Dynamic MaxPerPage
Override getPagerMaxPerPage()
class employeeGeneratorConfiguration extends BaseEmployee{
public function getPagerMaxPerPage(){
$maxPage = sfContext::getInstance()->getUser()->getAttribute('maxPage', 10);
return $maxPage;}
}
3. Adding relations
Fewest clicks for common tasksRelevant data placed togetherCurrently unsupported by the generatorSymfony provides functions to help
sfForm : : mergeForm()sfForm : : embedForm()sfFormDoctrine : : embedRelation()
Employee has many phonesEmployee:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
notnull: true
relations:
Phones:
type: many
class: Phone
local: id
foreign: employee_id
onDelete: CASCADE
Phone:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
number:
type: string(255)
notnull: true
employee_id:
type: integer(4)
notnull: true
type:
type: enum
values: [mobile, home, work]
Embed the ‘Phones’ relation in EmployeeForm::configure
class EmployeeForm extends BaseEmployeeForm{
public function configure(){$this->embedRelation('Phones');
}}
Add ability to edit existing phone numbers from within employee form
3. Adding relations
class PhoneForm extends BasePhoneForm{
public function configure(){
$this->widgetSchema['employee_id'] =new sfWidgetFormInputHidden();
...
Hide employee_id in PhoneForm::configure()
No Delete?No Add?
There’s a symfony plugin for that!Thanks to ahDoctrineEasyEmbeddedRelationsPlugin by Daniel Lohse
4. Translate admin interface
.all:
.settings:
i18n: on
default_culture: fr
Add chosen culture to settings.yml
./symfony cc and delete browser cookies
4. Translate admin interface
Create a new startrek catalogue
Add vulcan XLIFF files to:apps/admin/il8n/• startrek.vu.xml• startrek_forms.vu.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd" ><xliff version="1.0"><file original="global" source-language="en" target-
language="vu_VU" datatype="plaintext"><header /><body><!-- Actions --><trans-unit><source>New</source><target>Uzh</target>
</trans-unit><trans-unit><source>Edit</source><target>Ver-tor</target>
</trans-unit>...
startrek.vu.xml
4. Translate admin interface
4. Translate admin interfaceTell admin generator to use alternative catalogue
generator: class: sfDoctrineGeneratorparam:
i18n_catalogue: startrek
Tell the forms as well – sfFormDoctrine::setup()
abstract class BaseFormDoctrine extends sfFormDoctrine{
public function setup(){$this->widgetSchema->getFormFormatter()
->setTranslationCatalogue('startrek_forms');}
}
5. Tidy up filters
Filters work great – but the default style is a bit off
A few CSS tweaks
#sf_admin_container #sf_admin_bar {float:none;margin-left: 0px;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tr {
clear: none;border: 1px solid #DDD;padding: 0px;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tr td {
height: 50px;vertical-align: middle;border: none;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tbody {
clear: none;float: left;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tbody tr {
float: left;border-right: none;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tfoot {
clear: none;float: right;
}#sf_admin_container #sf_admin_bar .sf_admin_filter table tfoot tr {
float: right;}
Thanks to Sebastien
What is the definition of programmer?
Programmers are machines that turn coffee into code.
6. Timestampable fields
Simply unset them in form class
class NewsForm extends BaseNewsForm{
public function configure(){unset($this['created_at'], $this['updated_at']);
}}
Generally don’t need to edit these
6. Timestampable fields
What if you still need to see the value?
6. Timestampable fieldsUse sfWidgetFormPlain widget
http://trac.symfony-project.org/attachment/ticket/7963/sfWidgetPlain.diff
Thanks to Stephen.Ostrow
public function configure(){$this->setWidget('created_at',
new sfWidgetFormPlain(array('value'=>$this->getObject()->created_at)));
unset($this->validatorSchema['created_at']);
$this->setWidget('updated_at',new sfWidgetFormPlain(array('value'=>$this->getObject()->updated_at)));
unset($this->validatorSchema['updated_at']);...
7. Pre-filter list
7. Pre-filter list Add an object action to generator.yml
Set filter atribute in user session
list:object_actions:_edit: ~viewPhones: { label: Phone numbers, action: viewPhones }
class employeeActions extends autoEmployeeActions{
public function executeViewPhones($request){
$this->getUser()->setAttribute('phone.filters',array('employee_id' => $request->getParameter('id')),'admin_module');
$this->redirect($this->generateUrl('phone'));}
}
8. Row level ownership
Only allow owners of objects access
Example presumes: sfGuard plugin is installedObjects have a user_id field fk
8. Row level ownership
Secure the list page - moduleActions::buildquery()
protected function buildQuery(){
$query = parent::buildQuery();
$query->andWhere('user_id = ?', $this->getUser()->getId()
);
return $query;} This belongs in the model!!
8. Row level ownership
Secure all other actions - moduleActions::preExecute()
public function preExecute(){
if($this->getActionName()!= 'new' &&$this->getActionName()!= 'index'){
$this->forward404Unless(
$this->getUser()->isOwner($this->getRoute()->getObject())
);}
parent::preExecute();}
8. Row level ownership
Add a new method to user class - myUser::isOwner()
class myUser extends sfGuardSecurityUser{
public function isOwner($obj){
if(is_object($obj)){
if($this->getId() == $obj->getUserId()) return true;
}return false;
}}
8. Row level ownershipWhat about the user_id field in the form?
Don’t want users to change ownerAlso be careful with injected data
8. Row level ownershipWe need to remove the widget - Form::configure()
Set the user_id manually – Form::doUpdateObject()
public function configure(){
unset($this['user_id']);...
public function doUpdateObject($values){
$userId = sfContext::getInstance()->getUser()->getId();$this->getObject()->setUserId($userId);
return parent::doUpdateObject($values);}
eatmymonkeydust.com
9. Custom filters
Find out who has a birthday today
Based on info from Tomasz Ducin and dlepage
Add the new filter name to generator.yml
filter:display: [ name, birthday_today ]fields:
birthday_today:help: Employees who have a birthday today!
9. Custom filtersCreate a new widget in - xxFormFilter::configure()
Filter form is now displayed
public function configure(){$this->widgetSchema['birthday_today'] =new sfWidgetFormInputCheckbox();
$this->validatorSchema['birthday_today'] = new sfValidatorPass();
}
9. Custom filtersAdd a add*ColumnQuery to FormFilter class
public function addBirthdayTodayColumnQuery($query,$field,$value){if($value){$query->andWhere("SUBSTRING(`birthday`, 6, 5)
= SUBSTRING(NOW(), 6, 5)");}
return $query;}
Now buy the presents!
Plugins
sfAdminDashPluginKevin Bond
Joomla style adminAdds a dashboardConfigurable admin navigation
Replaces the admin cssManually add header component and footer partial to layout
sfAdminThemejRollerPluginGerald Estadieu
Looks stunningjQueryTheme roller systemPopup filtersTabs in edit view
Completely new admin theme
Optimist : The glass is half full. Pessimist : The glass is half empty..
Coder: The glass is twice as big as it needs to be
Extending Methods
What degree of customisation do you need?Will you need to re-use the functionality?
Extending - CSS
Define an alternative CSS
generator:class: sfDoctrineGeneratorparam:
model_class: Newstheme: adminnon_verbose_templates: truewith_show: falsesingular: ~plural: ~route_prefix: newswith_doctrine_route: 1css: funkystyle
Extending - Override code
Override individual templates and actionsQuick and easyCan’t be re-used between modulesCan become untidy
Extending – Create a theme
More work upfrontCan be used for multiple modules / projectsMuch more scope for customisingSteep learning curve – PHP in PHP!
Extending – Create a theme
Create container folder for new theme
cp -r lib/vendor/symfony/lib/plugins/
→ sfDoctrinePlugin/data/generator/sfDoctrineModule/admin/*
→ data/generator/sfDoctrineModule/newtheme/
mkdir -p data/generator/sfDoctrineModule/newtheme
Copy the generator files from sfDoctrine plugin
Extending – Create a theme
Skeleton – copied to admin module
Templates – generated into cache
Parts – Snippets of code included into cache
Name of theme
Extending – Create a theme
Change theme name in generator.yml
Clear cache
You’ve made your own theme!
generator:class: sfDoctrineGeneratorparam:
model_class: Newstheme: newtheme
...
Extending – Admin events
admin.pre_execute: Notified before any action is executed. admin.build_criteria: Filters the Criteria used for the list view. admin.save_object: Notified just after an object is saved. admin.delete_object: Notified just before an object will be deleted.
So, what does…
…do?
Dashboard
Show related models
Object history
Delete related objects warning
Future….What do you want the admin generator to do?What should the scope of the generator be?
Better support for embedded forms?More customisable list view (sfGrid)?Fulltext search in the fields? Saving goes back to list view?Dashboard?Nested sets? Ordering?Inherit from multiple themes?
Thanks for listening!Twitter: @jcleveley