As a web developer, you have to deal with web forms. Why would you make a new form, and an edit form, which is basically the same.
How to handle this would be to use this design pattern:
1.) load the get item to load, if it exists. Lets call it $dataObject
2.) if $dataObject is empty/does not exist, start a new dataObject. Lets call it $dataObject
3.) dataObject is now instantiated.
4.) Do what needs to be done — for example “title” of the page, being “editing XXX”, or “new dataObject”. Whatever frontend reflections need to happen, based on if it’s new object or editing an old object.
5.) populate the form, accordingly. If the object is blank, it’ll just be populated with the default values.
6.) onSubmit()… check to see if an ID value was submitted, and the appropriate security checks, and deal accordingly. If there was no ID value, start a new dataObject, to be populated with the data submitted.
7.) $dataObject->save();
The above described is how symfony admin generator handles form reuse. While the admin generator is useful for most cases, sometimes you need to just do “more” customizations, than using the generator.
For example, you need to handle a collection of both NEW and Prior saved objects… to do batch edits, it’s easier to do it manually, instead of using the symfony-admin-generator.
How to make a restful interface easily with symfony:
# “project_root”/app/”app_name”/config/routing.yml
# default rules
homepage:
url: /
param: { module: test, action: index }
test_get:
url: /v2/tester
class: sfRequestRoute
param: { module: test, action: get }
requirements:
sf_method: [get]
test_put:
url: /v2/tester
class: sfRequestRoute
param: { module: test, action: put }
requirements:
sf_method: [put]
test_post:
url: /v2/tester
class: sfRequestRoute
param: { module: test, action: post }
requirements:
sf_method: [post]
test_head:
url: /v2/tester
class: sfRequestRoute
param: { module: test, action: head }
requirements:
sf_method: [head]
# generic rules
# please, remove them by adding more specific rules
default_index:
url: /:module
param: { action: index }
default:
url: /:module/:action/*
Remember, order DOES matter… if you have the default one at the top, the more specific matches to be overlooked. So move more specific items to the top, and your routing will be fine.
Summary:
post to http://localhost/sf_project/frontend_dev.php/test, will call frontend->test->post
delete to http://localhost/sf_project/frontend_dev.php/test, will call frontend->test->delete
put to http://localhost/sf_project/frontend_dev.php/test, will call frontend->test->put
head to http://localhost/sf_project/frontend_dev.php/test, will call frontend->test->head
you get the point…
Doctrine Soft Delete is a fabulous method that will set a deleted_at column when the user deletes a record. So it doesn’t really delete the record but timestamps it as deleted.
This offers wonderful functionality to you admin generated app. But there’s a catch!
If you’re using a batch delete you’ll notice that the code uses DQL (Doctrine Query Language) to make the delete and not actually calling the delete() method from the class.
I fixed this quickly by actually time stamping deleted_at field:
$count = Doctrine_Query::create()
->update('myclass')
->set('deleted_at','now()')
->whereIn('id', $ids)
->execute();
Symfony should account for this so that soft delete will work properly with admin generated code.
Here’s how to set up the blameable extension for Doctrine 1.2 through Symfony.
1 – in your Symfony project folder, create a folder called “doctrine_extensions” under the lib folder: lib/doctrine_extensions
2 – Download the Blameable extension from Doctrine-project.org and copy it to the newly created doctrine_extensions folder. It should look like this:
lib/doctrine_extensions/Blameable
3 – update you’re yml file actAs section:
myTableClass:
connection: doctrine
tableName: myTableName
actAs:
Blameable:
listener: BlameableSymfony
columns:
created:
disabled: true
updated:
name: updated_by
*NOTE: my db field is called updated_by and i’m not using the created option, so i’ve disabled it. I’ve also created a yml key value called listener. I point the listener to a class called BlameableSymfony, which overrides the doctrine getUserIdentity() to use the sfGuard user id.
4 – Override the Blameable getUserIdentity() method. To do this i create the file lib/doctrine_extensions/Blameable/lib/Doctrine/Template/Listener/BlameableSymfony.php and add this content:
<?php
/**
* extending Doctrine_Template_Listener_Blameable to use symfony sf_guard_user id
*/
class BlameableSymfony extends Doctrine_Template_Listener_Blameable
{
/**
*
* @return int $ident sf_guard_user.id
*/
public function getUserIdentity()
{
$ident = sfContext::getInstance()->getUser()->getGuardUser()->getId();
return $ident;
}
}
That’s it! Good Luck!
Pulled from “More With Symfony” : http://www.symfony-project.org/more-with-symfony/1_4/en/08-Advanced-Doctrine-Usage.
Configuring Result Cache
In order to use the result cache we need to configure a cache driver for the queries to use. This can be done by setting the ATTR_RESULT_CACHE attribute. We will use the APC cache driver as it is the best choice for production. If you do not have APC available, you can use the Doctrine_Cache_Db or Doctrine_Cache_Array driver for testing purposes.
We can set this attribute in our ProjectConfiguration class. Define a configureDoctrine() method:
class ProjectConfiguration extends sfProjectConfiguration
{
// ...
public function configureDoctrine(Doctrine_Manager $manager)
{
$manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE, new Doctrine_Cache_Apc());
}
}
Here’s how it’s used (note useResultCache()):
$q = Doctrine_Core::getTable('User')
->createQuery('u')
->orderBy('u.username ASC');
$q->useResultCache(true, 3600, 'users_index');
To check if the cache exists you’d do this:
$manager = Doctrine_Manager::getInstance();
$cacheDriver = $manager->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
if ($cacheDriver->contains('users_index'))
{
echo 'cache exists';
}
else
{
echo 'cache does not exist';
}
Symfony 1.4 wasn’t properly deleting records using a batch action delete. This is caused by the sfDoctrineChoice validator doClean.
line 98 of lib/plugins/sfDoctrinePlugin/lib/validator/sfDoctrineChoice.class.php
was:
$query->andWhere(sprintf(‘%s.%s = ?’, $query->getRootAlias(), $this->getColumn()), $value);
Patched to:
if (is_array($value))
{
$query->andWhereIn(sprintf(‘%s.%s’, $query->getRootAlias(), $this->getColumn()), $value);
}
else
{
$query->andWhere(sprintf(‘%s.%s = ?’, $query->getRootAlias(), $this->getColumn()), $value);
}
I’m not as familiar with the code but i’ve put in a bug request. http://trac.symfony-project.org/ticket/8271
Couldn’t figure out why I kept getting this error:
_csrf_token [CSRF attack detected.]
It turned out that it was a bug in sfAdminThemejRollerPlugin:
plugins/sfAdminThemejRollerPlugin/data/generator/sfDoctrineModule/jroller/template/templates/_list_batch_actions.php
line: 9 from:
[?php $form = new sfForm(); if ($form->isCSRFProtected()): ?]
to:
[?php $form = new BaseForm(); if ($form->isCSRFProtected()): ?]
Thanks to Adrian (edsadr)
http://gestadieu.blogspot.com/2010/01/new-version-for-sfadminthemejrollerplug.html
I have a deleted_at field in my db that is a time stamp. I wanted to filter my records by records that are deleted and ones that are not.
He’re what i did:
lib/model/xxx.class.php: i added a magic function getDeleted this returns true/false if the record has a deleted_at value:
public function getDeleted()
{
return ($this->deleted_at) ? 1 : 0;
}
generator.yml: I then add it to my generator to be displayed in the index action. I set it to boolean to have it use a check for on and nothing for off.
config:
fields:
deleted: {type: Boolean}
my xxxFormFilter.class.php: In my filter class i add the deleted widget as a sfWidgetFormChoice with a validator to get the value back from the filter. I also add the magic method: addDeletedColumnQuery which handles my query. Please note i had to hardcode the name of the field that i was running a where on. I should probably just pass this to a doctrine table method that would add the where, but for now this works.
public function configure()
{
$this->widgetSchema['deleted'] =
new sfWidgetFormChoice(array(
'choices' => array('' => 'yes or no', 1 => 'yes', 0 => 'no')));
$this->validatorSchema['deleted'] =
new sfValidatorChoice(array(
'required' => false,
'choices' => array('', 1, 0)));
}
protected function addDeletedColumnQuery(Doctrine_Query $query, $field, $values)
{
$rootAlias = $query->getRootAlias();
$fieldName = "deleted_at";
if ($values == 0) {
$query->addWhere(
sprintf('%s.%s IS NULL OR %s.%s = ?',
$rootAlias, $fieldName, $rootAlias, $fieldName),
'');
}
else if ($values == 1) {
$query->addWhere(
sprintf('%s.%s IS NOT NULL AND %s.%s <> ?',
$rootAlias, $fieldName, $rootAlias, $fieldName),
'');
}
}