Symfony

Syndicate content
symfony Project Blog
Updated: 3 hours 50 min ago

Call the expert: Retrieving Data with Doctrine

Wed, 2008-11-19 04:11

In the last few blog posts about Doctrine we have demonstrated some of the functionality that integrates Doctrine with symfony like customizing sfDoctrineGuardPlugin and the new admin generator. This article is a bit different as it will demonstrate some of the functionality that exists in Doctrine whether you use it with symfony or by itself.

Schema File & Data Fixtures

First we need to define a schema and some data fixtures to test our queries against.

Schema File User: actAs: [Timestampable] columns: username: type: string(255) password: type: string(255) last_login: type: timestamp relations: Friends: class: User refClass: UserFriend local: user_id1 foreign: user_id2 Groups: class: Group refClass: UserGroup foreignAlias: Users Permissions: class: Permission refClass: UserPermission foreignAlias: Users   Group: tableName: groups columns: name: string(255) relations: Permissions: class: Permission refClass: GroupPermission foreignAlias: Groups   Permission: columns: name: string(255)   Phonenumber: columns: user_id: integer phonenumber: string(55) relations: User: foreignAlias: Phonenumbers onDelete: CASCADE   Profile: columns: user_id: integer first_name: string(255) last_name: string(255) email_address: string(255) relations: User: foreignType: one onDelete: CASCADE   UserFriend: columns: user_id1: type: integer primary: true user_id2: type: integer primary: true relations: User1: class: User local: user_id1 foreignAlias: UserFriends onDelete: CASCADE User2: class: User local: user_id2 foreignAlias: UserFriends onDelete: CASCADE   UserGroup: columns: user_id: type: integer primary: true group_id: type: integer primary: true relations: User: foreignAlias: UserGroups onDelete: CASCADE Group: foreignAlias: UserGroups onDelete: CASCADE   UserPermission: columns: user_id: type: integer primary: true permission_id: type: integer primary: true relations: User: foreignAlias: UserPermissions onDelete: CASCADE Permission: foreignAlias: UserPermissions onDelete: CASCADE   GroupPermission: columns: group_id: type: integer primary: true permission_id: type: integer primary: true relations: Group: foreignAlias: GroupPermissions onDelete: CASCADE Permission: foreignAlias: GroupPermissions onDelete: CASCADE   BlogPost: actAs: Timestampable: Sluggable: fields: [title] columns: user_id: integer title: string(255) body: clob relations: Author: class: User foreignAlias: BlogPosts onDelete: CASCADE Tags: class: Tag refClass: BlogPostTag foreignAlias: BlogPosts Comments: class: Comment refClass: BlogPostComment foreignAlias: BlogPosts   Tag: columns: name: string(255)   Comment: columns: title: string(255) body: clob   Page: actAs: Timestampable: Sluggable: fields: [title] columns: title: string(255) body: clob   BlogPostTag: columns: blog_post_id: type: integer primary: true tag_id: type: integer primary: true relations: BlogPost: foreignAlias: BlogPostTags onDelete: CASCADE Tag: foreignAlias: BlogPostTags onDelete: CASCADE   BlogPostComment: columns: blog_post_id: type: integer primary: true comment_id: type: integer primary: true relations: BlogPost: foreignAlias: BlogPostComments onDelete: CASCADE Comment: foreignAlias: BlogPostComments onDelete: CASCADE   Data Fixtures User: jwage: username: jwage password: changeme Profile: first_name: Jonathan last_name: Wage email_address: jonwage@gmail.com Groups: [Administrator] Friends: [fabpot, joeblow] Phonenumbers: Phonenumber_1: phonenumber: 6155139185 fabpot: username: fabpot password: changeme Profile: first_name: Fabien last_name: Potencier email_address: fabien.potencier@symfony-project.com Groups: [ContentEditor] Friends: [jwage] joeblow: username: joeblow password: changeme Profile: first_name: Joe last_name: Blow email_address: jowblow@gmail.com Groups: [Registered] Friends: [jwage, fabpot]   Group: Administrator: name: Administrator Permissions: [EditPages, EditBlog, EditUsers, EditPages, Frontend] Blogger: name: Blogger Permissions: [EditBlog, Frontend] Moderator: name: Moderator Permissions: [EditUsers, EditComments, Frontend] ContentEditor: name: Content Editor Permissions: [EditPages, EditBlog, Frontend] Registered: name: Registered Permissions: [Frontend]   Permission: EditPages: name: Edit Pages EditBlog: name: Edit Blog EditUsers: name: Edit Users EditPages: name: Edit Pages EditComments: name: Edit Comments Frontend: name: Frontend   BlogPost: BlogPost_1: Author: jwage title: Sample Blog Post body: This is a sample blog post Tags: [symfony, doctrine, php, mvc] Comments: Comment_1: title: This is a bad blog post body: Yes this is indeed a horrible blog post Comment_2: title: I think this is awesome body: This is an awesome blog post, what are you talking about?!?!?!   Tag: symfony: name: symfony php: name: PHP doctrine: name: Doctrine mvc: name: MVC   Page: home: title: Home body: This is the content of the home page about: title: About body: This is the content of the about page faq: title: F.A.Q. body: This is the content of the frequently asked questions page   Select Queries DBMS Functions

First we will demonstrate how you can use DBMS functions in your queries. For example you might want to retrieve all blog posts with a count of the number of comments for each blog post.

$q = Doctrine_Query::create() ->select('p.*, COUNT(c.id) as num_comments') ->from('BlogPost p') ->leftJoin('p.Comments c') ->groupBy('p.id'); $results = $q->execute(); echo $results[0]['num_comments'];  

You can use any combination of functions and nest them as deeply as you want.

Multiple Joins

Doctrine makes it easy to retrieve data from multiple tables. In this example we can retrieve all the permissions a user has, even the ones through his assigned groups.

$q = Doctrine_Query::create() ->from('User u') ->leftJoin('u.Permissions p') ->leftJoin('u.Groups g') ->leftJoin('g.Permissions p2') ->where('u.id = ?', 1); $user = $q->fetchOne();  

Now we can build a Doctrine_Collection of all the Permissions the user has.

$permissions = new Doctrine_Collection('Permission'); foreach ($user['Groups'] as $group) { foreach ($group['Permissions'] as $permission) { $permissions[] = $permission; } } foreach ($user['Permissions'] as $permission) { $permissions[] = $permission; }  

In a blog application, it is a common need to want to retrieve a BlogPost with the related Author, Comments and Tags all in one query. With Doctrine this is just as easy as it was for me to type the previous sentence.

$q = Doctrine_Query::create() ->from('BlogPost p') ->leftJoin('p.Author a') ->leftJoin('p.Comments c') ->leftJoin('p.Tags t') ->where('p.id = ?', 1);   Sub-Queries

We can alternatively retrieve the same Permission Doctrine_Collection directly from Doctrine using sub-queries to know which Permission records to retrieve.

$userId = 1; $q = Doctrine_Query::create() ->from('Permission p');   $q2 = $q->createSubquery() ->select('p2.permission_id') ->from('UserPermission p2') ->where('p2.user_id = ?');   $q3 = $q->createSubquery() ->select('p3.id') ->from('Permission p3') ->leftJoin('p3.GroupPermissions gp') ->leftJoin('gp.Group g') ->leftJoin('g.Users u') ->where('u.id = ?');   $q->where('p.id IN (' . $q2->getDql() . ')') ->orWhere('p.id IN (' . $q3->getDql() . ')');   $permissions = $q->execute(array($userId, $userId));   Shorthand Left Joins

One of the great convenience features of Doctrine is the ability to specify joins in a shorthand syntax. This will greatly reduce the number of lines of code a query may occupy. You can simply change models together in the from() part to specify joins and they default to the same thing as using leftJoin().

$q = Doctrine_Query::create() ->from('User u, u.Profile p, u.Groups g');  

The above code is equal to doing:

$q = Doctrine_Query::create() ->from('User u') ->leftJoin('u.Profile p') ->leftJoin('u.Groups g');   Delete & Update Queries

Doctrine_Query can be used to specify UPDATE and DELETE queries by simply using the update() or delete() functions. Here are some examples.

Delete Query

In this example we will delete a user by his username.

Doctrine_Query::create() ->delete() ->from('User u') ->where('u.username = ?', 'jwage') ->execute();   Update Query

In this example query we will update a users password.

Doctrine_Query::create() ->update('User u') ->set('u.password', '?', 'newpassword') ->where('u.username = ?', 'jwage') ->execute();  

The set() function accepts three arguments. The first is the name of the field you want to set, the second is the part that is passed through to PDO untouched and the third is the parameter/value.

Another example would be setting a timestamp field with a dbms functions. We don't use the third argument because we want NOW() to be passed through to PDO un-touched.

Doctrine_Query::create() ->update('User u') ->set('u.last_login', 'NOW()') ->where('u.username = ?', 'jwage') ->execute();  

The benefit of using the DQL update and delete is that it only requires one query to accomplish what you want. If you use objects then the object must be retrieved first, then updated or deleted which means two individual queries.

Manually writing DQL

For you SQL buffs, we didn't forget about you. You can optionally write your DQL queries manually and parse them in to a Doctrine_Query instance or just execute them.

$dql = "FROM User u, u.Phonenumbers p"; $q = Doctrine_Query::create()->parseQuery($dql);  

Or you can just execute them by using the query() method of Doctrine_Query.

$dql = "FROM User u, u.Phonenumbers p"; $q = Doctrine_Query::create()->query($dql);   Executing Queries

In all the above examples we show you how you can create the queries, but what about executing them? Doctrine offers a few different ways to execute the queries and a few different ways to hydrate the data. It can hydrate the data as objects, php arrays which is much much faster than using objects or it can simply skip the hydration process all together.

Array Hydration

Here are a few examples of how you can execute array hydration.

$results = $q->execute($params, Doctrine::HYDRATE_ARRAY);  

A convenience method exists called fetchArray().

$results = $q->fetchArray($params);   Record Hydration $results = $q->execute($params, Doctrine::HYDRATE_RECORD);  

Record hydration is the default so you can omit the 2nd argument if you like.

No Hydration

We can simply skip the hydration process completely and return what PDO gives us. This is only useful in very few cases, like when you only have one row and one column of data. The data is returned as an array with numeric keys so it is not very useful in any other cases.

$results = $q->execute($params, Doctrine::HYDRATE_NONE);   Fetching one Record

You can use the fetchOne() convenience method to automatically add a limit of one and return a single result instead of multiple.

$result = $q->fetchOne($params, Doctrine::HYDRATE_ARRAY);  

That is all for today. In the next article we will demonstrate how to work with the objects defined in the relationships and retrieved in the above queries.

Be trained by symfony experts - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris - Feb 18 Paris
Categories: php, software

symfony 1.1.5 released

Tue, 2008-11-18 17:59

The new 1.1.5 maintenance version of symfony has just been released today. Here's the changelog:

  • The sfValidatorAnd and sfValidatorOr validators are now required by default, which is the normal expected behavior (see ticket #4877)
  • The response time when using the command line interface has been decreased, especially under Microsoft® Windows™ (see ticket #4882)
  • The symfony cache:clear command was broken and didn't allow to clear the global cache, and is now fixed (see ticket #4614)
  • symfony could use another local installation of Propel: this has been fixed (see ticket #4650)
  • A bug which preventing from handling large numbers using sfNumberFormat has been fixed (see ticket #4521)
  • The HTTP version used to send the response in in the sfWebResponse class can now be configured in the factories.yml configuration file (see ticket #4578)
  • We also fixed a bug which prevented from using the cache with the with_layout option and the web_debug setting turned on at the same time (see ticket #4514)

As usual, please upgrade your existing projects by updating the reference to the 1.1.5 subversion tag or by running the PEAR upgrade command:

$ pear upgrade symfony/symfony-1.1.5

If you use the stable branch from our SVN repository, just run the svn update command to upgrade your project.

Last but not least, dont forget to clear your cache by running the symfony cache:clear command ;-)

Be trained by symfony experts - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris - Feb 18 Paris
Categories: php, software

A week of symfony #98 (10->16 november 2008)

Sun, 2008-11-16 23:15

The last beta of symfony 1.2 was released this week, setting the pace for the upcoming release candidate. In addition, last week it was officially announced that symfony will be supported in the next version of Netbeans IDE. Lastly, symfony achieved another remarkable milestone with its 13,000th changeset.

Development mailing list

Development highlights

  • r12864, r12868: [1.1, 1.2] added cache to symfony CLI
  • r12865: [1.2] added sfCompat10Plugin in excluded plugins when migrating (which was the case in symfony 1.1)
  • r12867: [1.1] added autoload cache to CLI
  • r12870: [1.2] fixed generator.yml doesn't handle object_actions configuration parameters correctly
  • r12871: [1.2] enhanced usability of the admin stylesheet: a link icon is now part of the link itself
  • r12873: [1.2] added CSS and JS support to sfWidgetFormDateRange widget
  • r12876: [1.2] Added a way to define default messages for required and invalid error codes in sfValidatorBase class
  • Completed milestone 1.2.0 Beta 2
  • r12920: [1.2] fixed symfony default CLI
  • r12949: [1.2] added .css for the default CSS in the default view.yml file
  • r12952: [1.2] fixed assumption plugin name can be determined using basename()
  • r12957, r12958: [1.0, 1.1] ordered fixtures by rsort
  • r12960: [1.2] added sfCompat10Plugin as an excluded plugin in the default skeleton
  • r12968: [1.2] fixed sfRoute tokenizer to ease extensions
  • r12969: [1.2] fixed URL generation when custom tokens have been defined
  • r12982: [1.2] added alt attributes to web debug toolbar icons to be xhtml valid
  • r12996, r12997: [1.1, 1.2] added a workaround to a PHP weird behavior where classes are not autoload within an Exception constructor
  • r13002: [1.2] removed the default route for symfony default module
  • r13018: [1.2] Fixed get Local / Remote Ip address from Request
  • r13023: [1.2] added basic implementation of select()/deselect() support to test browser for changing selection in check and radiobuttons
  • r13026: [1.2] fixed genUrl() when a relative URL (/foo/bar) is passed as an argument (it is possible because an internal URI cannot start with a / anymore since symfony 1.1)
  • r13028: [1.2] updated the default databases.yml in skeleton
  • r13029: [1.2] added some exception in sfObjectRoute when the method does not exist
  • r13035: [1.2] fixed the configure:database task for Propel 1.3
  • r13036: [1.2] fixed sandbox default database configuration
  • r13037: [1.2] reordered .sf creation in sandbox script
  • Updated dwhittle branch: implemented ArrayAccess for sfUser attributes, cleaned up phpdoc,
  • ...and many other changes

Development digest: 181 changesets, 59 defects created, 29 defects closed, 17 enhancements created, 10 enhancements closed, 9 documentation defects created, 4 documentation defects closed and 33 documentation edits.

Book and documentation

Wiki

Plugins

  • New plugins
    • vo2AdminGeneratorPlugin: brings the Admin Generator of symfony 1.1 to symfony 1.0
    • xsPasswordManagerPlugin: plugin for storing and managing passwords
    • sfCsvPlugin: easily read and write CSV files
    • sfFormButtonsPlugin: allows you to use button tags instead input ones in common template design cases
    • eCropPlugin: provides mechanism for croping images with GD library
    • sfErrorHandlerPlugin: provides error handling to rethrow PHP errors as sfExceptions, which rather than cause a white screen will now produce the 500 page in a prod env or a stack trace in dev env
  • Updated plugins
    • sfPropelPlugin:
      • [1.2] added hide feature to admin gen
      • [1.2] fixed sfFormPropel::saveFile() when a filename is provided
      • [1.2] added lv, el translations to the admin generator
      • [1.2] updated pt_br, da, de and fr translations for the admin generator
      • [1.2] ordered fixtures by rsort
      • [1.2] added a method_for_criteria option
    • sfDoctrinePlugin:
      • [1.2] fixed css load
      • [1.2] fix for saving nested forms
      • [1.2] fixed issue with sfDoctrineColumng::isNotNull() and notblank validator
      • [1.2] fixed web debug toolbar only shows prepared sql statements
      • [1.2] fixed sfDoctrineForm::saveFile() when a filename is provided
      • [1.2] committed update to tests for generated base models
      • [1.2] fixed location of BaseFormDoctrine in fixtures
      • Continued work on symfony + Doctrine book
      • [1.2] changing fromArray() to not be recursive/deep as the save embedded forms takes care of this for us now
      • [1.2] test coverage, updated sfFormDoctrine to be equal to sfFormPropel, fixed issue with i18n
    • sfDoctrineActAsTaggablePlugin: fixed two syntax errors in PluginTagTable, updated PluginTagTable
    • sfFormExtraPlugin: updated README, fixed IE problem when no image is provided
    • sfPhpunitPlugin: fixed problem with Doctrine Table test classes, fixed problem with some php installations when running testall
    • sfGuardExtraPlugin: started register implementation, fix password in email, add action and module parameter to mail, add register complete action, added more data for forgot password mail template
    • sfGuardPlugin: updated 1.2 admin generator, deleted base form filters (do not forget to rebuild your form filters), fixed sfGuardUserAdminForm::updateObject signature, fixed sfGuardUserForm
    • sfFeed2Plugin: fixed encoding test, added a new date test, fixed a problem with reading feed items according to correct timezone, fixed a timezone issue, updated package.xml info
    • sfJobQueuePlugin: created branch for symfony 1.1 and Propel 1.3 development, created StartQueue, ListQueue and StartQueueManager tasks, updated model and job queue manager, updated README, updated code to symfony 1.1
    • sfStatsPlugin: restructured the repository, ported the plugin to symfony 1.2, updated the js files of flot to version 0.5
    • sfMogileFSPlugin: added memcached caching of GET_PATHS command
    • sfDoctrineGuardPlugin: updated admin generators, tweaks to forms
    • ysfR3Plugin: performance improvements for translations
    • sfTaskExtraPlugin: fixed packaging of empty directories, added post_command hook to propel:build-model to insert model class for plugin logic, added test:plugin task
    • sfPropelSlotBehaviorPlugin: fixed a bug
    • sfDynamicCMSPlugin: pre-release 0.3.3.7 (make plugin compatible with symfony 1.1 branch, little bugfix in 1.1 branch), fixed bugs concerning permission management and symfony 1.1 compatibility
    • sfLucenePlugin: removed temporary return
    • sfExtjsThemePlugin: backported some improvements from the dbfinder branch, fixes to the admin columns generator methods, fixes to the admin columns generator methods, fixed path problem with the base action include, reverted getColumnsForDisplay to old getColumnsGrouped until the rest of the bugs are worked out
    • sfI18NTranslatorPlugin: added logger and ajax response corrections
    • sfAssetsLibraryPlugin: fixed typo in _messages.php
    • sfSimpleCMS2Plugin: delete news from fixtures, delete sfSimpleNewsPlugin data from fixtures
    • sfErrorHandlerPlugin: fix so fatal_error_handler doesn't handle errors above 1024 (must make this configurable)
    • sfLanguageSwitchPlugin: removed 1.2 part from query array, made it 1.2 compatible, created new tag version 0.0.3, marked as stable

Some new symfony powered websites

They talked about us

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

Call the expert: Customizing sfDoctrineGuardPlugin

Wed, 2008-11-12 21:18
Getting Started

If you are just now getting started with Doctrine, check this previous blog post for how to get started with a new symfony + Doctrine project.

Installing sfDoctrineGuardPlugin

The sfDoctrineGuardPlugin has not been packaged for symfony 1.2 yet so you will need to install via svn like the following:

$ cd /path/to/symfony1.2project $ svn co http://svn.symfony-project.com/plugins/sfDoctrineGuardPlugin/trunk plugins/sfDoctrineGuardPlugin

Now that the plugin is in your plugins folder you need to tell symfony to enable it. Edit config/ProjectConfiguration.class.php and enable the sfDoctrineGuardPlugin.

public function setup() { $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin')); }  

Be sure to clear your cache after doing the previous steps.

$ ./symfony cc

Enable Modules

sfDoctrineGuardPlugin comes with three modules by default, so in order to use these we will need to enable them in our apps/backend/config/settings.yml. Open it up and modify the enabled_modules setting like the following:

all: .settings: enabled_modules: [default, sfGuardUser, sfGuardGroup, sfGuardPermission] Schema and Data Fixtures

First we need to add some new models to our schema that have relationships defined to the sfGuardUser model included in the sfDoctrineGuardPlugin. So, create a config/doctrine/schema.yml and place the below YAML inside.

In this example we're going to add a Profile model and relate it to sfGuardUser to include some additional information. By default the sfGuardUser schema only includes the information required for the authentication process, nothing more and nothing less. So, in order to capture additional information it is common practice to create a Profile model and relate it to sfGuardUser.

Profile: columns: sf_guard_user_id: integer(4) first_name: string(255) middle_name: string(255) last_name: string(255) email_address: string(255) relations: User: class: sfGuardUser foreignType: one  

Now you will see we defined a one-to-one relationship between Profile and sfGuardUser. By adding that schema, the following is now possible.

$user = new sfGuardUser(); $user->Profile->first_name = 'Jonathan';  

And you can also access the relationship from the opposite end as Doctrine automatically makes relationships bi-directional.

$profile = Doctrine_Query::create() ->from('Profile p') ->innerJoin('p.User u') ->where('p.id = ?', 1) ->fetchOne(); $user = $profile->User;  

Now that we have our schema defined, we need to define some simple data fixtures to test against. Edit data/fixtures/data.yml and place the following YAML inside.

sfGuardUser: jwage: username: jwage password: changeme is_super_admin: true Profile: first_name: Jonathan middle_name: Hurley last_name: Wage email_address: jonwage@gmail.com  

Now that we have our schema and data fixtures defined, we need to build everything. You can do so by running the following command:

$ ./symfony doctrine:build-all-reload

Well that was pretty easy! Now lets just do a little inspecting and make sure that all is well and the data is loaded properly. Run the following DQL query to inspect the data.

$ ./symfony doctrine:dql "FROM sfGuardUser u, u.Profile p" >> doctrine executing dql query DQL: FROM sfGuardUser u, u.Profile p found 1 results - id: '1' username: jwage algorithm: sha1 salt: 9509acc86d1201e5d8314c2421339896 password: a9119b7ca39bbb842fed640ed93c121990a37dbf is_active: true is_super_admin: true last_login: null created_at: '2008-11-12 13:40:42' updated_at: '2008-11-12 13:40:42' Profile: id: '1' sf_guard_user_id: '1' first_name: Jonathan middle_name: Hurley last_name: Wage email_address: jonwage@gmail.com

You can even do the opposite to get the Profile objects with the User record joined.

$ ./symfony doctrine:dql "FROM Profile p, p.User u" >> doctrine executing dql query DQL: FROM Profile p, p.User u found 1 results - id: '1' sf_guard_user_id: '1' first_name: Jonathan middle_name: Hurley last_name: Wage email_address: jonwage@gmail.com User: id: '1' username: jwage algorithm: sha1 salt: 9509acc86d1201e5d8314c2421339896 password: a9119b7ca39bbb842fed640ed93c121990a37dbf is_active: true is_super_admin: true last_login: null created_at: '2008-11-12 13:40:42' updated_at: '2008-11-12 13:40:42'

Now we are ready to take a look at the enabled modules and check out the functionality they provide. Open http://yourhost/backend_dev.php/sf_guard_user and you should see the sfGuardUser module in action like the following:

The Customizing

Now we finally get to the fun part. We added a Profile model and related it to sfGuardUser but how do we make that editable when editing sfGuardUser records via the admin generator? This is pretty simple. We need to tweak a few pieces of code to make this possible.

First, lets update our sfGuardUserAdminForm to embed the ProfileForm. To do this we need to copy a file from the plugin to our project so we can customize the form.

$ cp plugins/sfDoctrineGuardPlugin/lib/form/doctrine/sfGuardUserAdminForm.class.php lib/form/doctrine/

Now open lib/form/doctrine/sfGuardUserAdminForm.class.php and override the configure() method to customize it and embed the ProfileForm.

public function configure() { parent::configure();   $profileForm = new ProfileForm($this->object->Profile); unset($profileForm['id'], $profileForm['sf_guard_user_id']); $this->embedForm('Profile', $profileForm); }  

The last thing we need to make this all work is to customize the generator.yml that comes with the plugin. To do this we need to override some of the plugin.

$ mkdir apps/backend/modules/sfGuardUser $ mkdir apps/backend/modules/sfGuardUser/config $ touch apps/backend/modules/sfGuardUser/config/generator.yml

Now we need to open apps/backend/modules/config/generator.yml in our editor and tweak it a bit to include the embedded form we named Profile in the previous step.

generator: class: sfDoctrineGenerator param: config: form: class: sfGuardUserAdminForm display: "NONE": [username, password, password_again, Profile] "Permissions and groups": [is_active, is_super_admin, groups_list, permissions_list]  

Notice all we added was the Profile to the list of things to display in the form.

Final Product

Now when you add and edit users you have the ability to enter the Profile information directly inside of the user form.

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

symfony 1.2 beta2 - The Doctrine Admin Generator is here

Mon, 2008-11-10 22:53

We are very happy to announce the symfony 1.2 beta 2 release, which now supports the admin generators for both Doctrine and Propel. No need to use Propel for sexy admin generator anymore. Hooray!

Thanks to Jonathan for his great work on this. But of course we other folks were not lazy, and killed a lot of bugs. The main man to praise here is Fabien, who did work really a lot the last week.

Many enhancements have been made based on community feedback or tickets to the admin generator and forms system, and even better, quite some of them have been backported to symfony 1.1.

What's in here? Main Highlights
  • To provide an easier upgrade path form 1.1, all plugins are now enabled by default when upgrading (except for the Doctrine plugin and the Compat10 one) r12639
  • Admin Generation creation can now also creates the required route for you r12758
  • Forms save now embedded forms automatically. Read more in this tutorial
  • Session storage now also can work with propel connections where it could not before r12763
  • Propel Tasks using plugins with behaviours now work correctly r12809
  • Time logging in the Web Debug toolbar is back to the old mechanism, because the new one was faulty r12698
  • The CLI is now much faster thanks to caching the autoloading settings r12864
  • A lot of small bugs in the new admin generator have been fixed
  • Web assets now get relative symlinks r12847
  • Improved PHP 5.3 and PHP 5.1 compatibility r12625 and r12676
In Total

Tracking all the changes is not always very easy, especially when they affect multiple versions. For the 1.2 milestone, we have now a "Closed Tickets" count of 104, and Beta 2 milestone adds up another 29.

Give it a try

We encourage you to try this new beta version. Please be reminded to follow the UPGRADE procedure. Even if you are upgrading from beta 1, you need to follow these steps:

$ php symfony project:upgrade1.2 $ php symfony propel:build-model $ php symfony propel:build-forms $ php symfony propel:build-filters $ php symfony cc

This is due to changes in code that will be generated by these tasks for you.

What's up next?

As we are pretty pleased with the quality and also feature wise everything for 1.2 is in now, this will be the last beta. The next release on the path to a 1.2 final will be the symfony 1.2 RC1, coming very soon.

We are also ramping up the documentation efforts to make sure that symfony will remain the best documented framework available.

So if you think "hey this is really missing in 1.2" it is time to speak up (and create a ticket with patch, tests and documentation, you get it).

You own a plugin? You have not started porting it to symfony 1.2 yet? You should get started. Kris made the sfTaskExtraPlugin which can help you out.

Contribute

Do you want to see the new admin generators in your language? Then:

  • Download the French file and replace all the French strings with your translations (the file also contains the English strings)
  • Rename the file with your language ISO code
  • Send the translated file to one of the core team members, or the documentation mailinglist, or create a ticket and attach it there.

Thanks a lot to all the folks out there who already translated the admin generator. 28 translations are a pretty impressive number already.

Also I would like thank you to everybody who already tried symfony 1.2. I appreciate your feedback in mailinglist, IRC and especially in tickets. Thank you!

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

Call the expert: Nested forms - A real implementation

Mon, 2008-11-10 06:30

As announced in a previous post, symfony 1.2 is able to automatically save objects from deep nested forms. I gave a simple example in the announcement post, but some people asked me for a real project example. So here it is.

The project

Let's take a classified website. The website is composed of ads. Each ad is described with some generic information (like a title, a description, ...) and some more specific ones based on the ad type (like the number of beds or the year of construction for a house, or the make, the model, or the color for a car).

So, the model schema is composed of a main demo_ad table and some type tables (demo_ad_type_house and demo_ad_type_car) to host the detailed information for the ad:

# config/schema.yml propel: demo_ad: id: ~ title: { type: varchar(255), required: true } description: { type: longvarchar, required: true } price: float type: { type: varchar(255), required: true }   demo_ad_type_house: id: ~ demo_ad_id: { type: integer, foreignTable: demo_ad, foreignReference: id, required: true } square_footage: { type: integer, required: true } nb_beds: { type: integer, required: true } nb_baths: { type: integer, required: true } year: { type: varchar(255), required: true }   demo_ad_type_car: id: ~ demo_ad_id: { type: integer, foreignTable: demo_ad, foreignReference: id, required: true } make: { type: varchar(255), required: true } model: { type: varchar(255), required: true } year: { type: varchar(255), required: true } color: { type: varchar(255), required: true }  

To make it more real, let's add some initial data:

# config/fixtures/ads.yml DemoAd: house_1: title: Farm description: | 250 acres with irrigation, several shares of water rights, creek, spring and a well. price: 2225000 type: house car_1: title: Honda Accor description: | Honda accord fully loaded, power windows, sun roof, new timing belt, new brakes, a/c , cd player. price: 6900 type: car   DemoAdTypeHouse: house_1_desc: demo_ad_id: house_1 square_footage: 4500 nb_beds: 4 nb_baths: 3 year: 1910   DemoAdTypeCar: car_1_desc: demo_ad_id: car_1 make: honda model: accor year: 2002 color: green   Project Initialization

If you want to follow along, create a new symfony project the usual way:

$ mkdir classifieds $ cd classifieds $ symfony generate:project classifieds

Then create the two files we have described above (config/schema.yml and config/fixtures/ads.yml), configure the database, build the model, and feed the database with the initial data:

$ ./symfony configure:database "mysql:host=localhost;dbname=classifieds" root mYsEcret $ mysqladmin -uroot -pmYsEcret create classifieds $ ./symfony propel:build-all-load

In this post, we will create the backend of the application to demonstrate all the power of the new admin generator bundled with symfony 1.2.

$ ./symfony generate:app --escaping-strategy=on --csrf-secret=Unique$ecret1 backend

Then create the backend module to list, create, edit, and delete the ads:

$ ./symfony propel:generate-admin backend DemoAd

The propel:generate-admin automatically adds a route to the routing.yml configuration file.

The ad module is now ready to be used as shown on these screenshots:

Project Customization

As you can see for yourself on the screenshots, it is not quite finished yet.

The type column, which is stored as a string in the database, need to be changed from an input text box to a select box in the form.

Instead of hardcoding the possible types in the form, declare them as a simple property of the DemoAdPeer class, so it can be reused later on in the project:

// lib/model/DemoAdPeer.php class DemoAdPeer extends BaseDemoAdPeer { static public $types = array('house' => 'house', 'car' => 'car'); }  

It is now easy to change the type widget of DemoAdForm from a text input to a choice:

// lib/form/DemoAdForm.class.php class DemoAdForm extends BaseDemoAdForm { public function configure() { $this->widgetSchema['type'] = new sfWidgetFormChoice(array('choices' => DemoAdPeer::$types)); $this->validatorSchema['type'] = new sfValidatorChoice(array('choices' => array_keys(DemoAdPeer::$types))); } }  

When editing an ad, we want to edit the main information but also the detailed ones. So, we need to embed the specific description form in the main form.

As there is no database relation between the ad and the type, we need to create a custom method in the DemoAd model to get the DemoAdType* object:

// lib/model/DemoAd.php class DemoAd extends BaseDemoAd { public function getTypeObject() { // if no type has been defined yet, there is no type object if (!$this->getType()) { return null; }   // the type class depends on the ad type $class = sprintf('DemoAdType%s', ucfirst($this->getType())); $peer = constant($class.'::PEER');   // get the type object related to the current ad $criteria = new Criteria(); $criteria->add(constant($peer.'::DEMO_AD_ID'), $this->getId());   // if there is none, create a new one associated with this ad if (is_null($desc = call_user_func(array($peer, 'doSelectOne'), $criteria))) { $type = new $class(); $type->setDemoAd($this); }   return $type; } }  

Now for the fun part. Embed the type form into the main ad form if there is one:

// lib/form/DemoAdForm.class.php class DemoAdForm extends BaseDemoAdForm { public function configure() { $this->widgetSchema['type'] = new sfWidgetFormChoice(array('choices' => DemoAdPeer::$types)); $this->validatorSchema['type'] = new sfValidatorChoice(array('choices' => array_keys(DemoAdPeer::$types)));   // only embed if there is a type object (edit vs create) if ($this->getObject()->getType()) { $this->embedForm('desc', $this->getTypeForm()); } }   public function getTypeForm() { $class = sprintf('DemoAdType%sForm', ucfirst($this->object->getType()));   return new $class($this->object->getTypeObject()); } }  

If you refresh your browser now, you will have an exception because the embedded form has a select box to choose the ad to which it is linked to. To render this select box, symfony needs a text representation of an Ad:

class DemoAd extends BaseDemoAd { public function __toString() { return $this->getTitle(); }   // ... }  

As we don't want people to be able to change the link between the ad and the type, we need to disable the corresponding widget in the type form classes:

// lib/form/DemoAdTypeCarForm.class.php class DemoAdTypeCarForm extends BaseDemoAdTypeCarForm { public function configure() { unset($this['demo_ad_id']); } }   // lib/form/DemoAdTypeHouseForm.class.php class DemoAdTypeHouseForm extends BaseDemoAdTypeHouseForm { public function configure() { unset($this['demo_ad_id']); } }  

That's all there is to it. You can now change the main ad columns or the specific ones and when saving the form, symfony will save everything back to the database. And this has been possible with the admin generator without customizing anything. It just works!

Of course, you will find some edge cases that need to be worked on, but hopefully you are now able to customize it further.

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

A week of symfony #97 (3->9 november 2008)

Sun, 2008-11-09 23:30

The first plugin developers day took place this week resulting in an historic plugin development activity: 8 new plugins were released and nearly 30 plugins were updated. Meanwhile, symfony 1.2 continues refining and improving its great new features and prepares its imminent second beta release.

Development mailing list

Development highlights

  • r12587: [1.2] tweaked web debug toolbar css (better cross-browser compat)
  • r12594: [1.1] fixed propel plugin does not ignore doctrine schemas
  • r12595: [1.2] fixed ObjectHelper for doctrine
  • r12625: [1.2] removed magic_quotes calls for 5.3 compat
  • r12631: [1.2] fixed plugins autoloading
  • r12636: [1.2] added nested save support to sfPropelForm
  • r12639: [1.2] changed the default behavior of symfony: now all plugins are enabled by default (to keep a better BC with 1.1 behavior)
  • r12641: [1.1, 1.2] fixed typo in sfValidatorString
  • r12646: [1.2] added support for nested forms in the admin generator
  • r12660: [1.2] made sfRequest cloneable
  • r12661: [1.2] added a new option to be able to change the default text regex
  • r12668: [1.2] added sfProjectConfiguration::setPluginPath() for specifying the path to a particular plugin
  • r12671: [1.2] fixed sfForm::renderHiddenFields() doesn't render hidden fields from embedded forms (reverted)
  • r12672: [1.2] added exception if a plugin is enabled too late
  • r12676: [1.1, 1.2] fixed spl_autoload_register for PHP 5.1.2
  • r12696, r12697: [1.1, 1.2] made sure that the core autoloaded is registered only once per request
  • r12698: [1.2] reverted usage of REQUEST_TIME
  • r12728: [1.2] fixed token replacement in databases.yml
  • r12751: [1.1, 1.2] fixed HTTP header for exceptions
  • r12752: [1.2] reformated the default layout
  • r12753: [1.2] changed some sfObjectRoute error messages to be more explicit
  • r12757: [1.2] added automatic route creation to propel:generate-admin
  • r12763: [1.2] applied patch for UTF-8 support and using PropelConnections for PDOSession storage
  • r12769: [1.2] fixed form field ordering in admin generator when no display configuration is used
  • r12804: [1.2] added nested sets builders in the upgrade task
  • r12806: [1.2] fixed app:routes display when a route has several method requirement
  • r12810: [1.1, 1.2] fixed widget form schema positions after unsetting some widgets
  • r12813: [1.1, 1.2] fixed setDefaultFormFormatterName() when called from BaseFormPropel
  • r12811: [1.0, 1.1, 1.2] fixed order of data deleting in sfPropelData
  • r12813: [1.1, 1.2] fixed setDefaultFormFormatterName() when called from BaseFormPropel
  • r12829: [1.2] fixed form fields labels in the new admin generator
  • r12830: [1.2] fixed labels for list in admin generator
  • r12837: [1.2] fixed positioning of calendar popup
  • r12840: [1.2] proposed implementation for calculating relative path
  • r12843: [1.2] sfSymfonyPluginManager now creates relative symlinks if possible
  • r12844: [1.2] added PDO debugging when running functional tests
  • r12847: [1.2] needed to move relative path calculation into sfFilesystem to prevent collision with the mirror command
  • r12852: [1.2] fixed sfBrowser when an action is empty
  • r12855: [1.2] removed sf_admin_module_web_dir from settings.yml
  • Updated dwhittle branch
  • ...and many other changes

Development digest: 273 changesets, 53 defects created, 81 defects closed, 17 enhancements created, 22 enhancements closed, 12 documentation defects created, 10 documentation defects closed and 24 documentation edits.

Book and documentation

Wiki

Plugins

  • New plugins
    • sfAuthorizeNetCIMPlugin: allows you to create, retreive, update and delete payment profiles from the Authorize.NET CIM service
    • jsThumbnailPlugin: creates thumbnails on the fly using the GD library. It creates a thumbnail of a image in the given size and stores it in cache for the next calls, until the image changes.
    • sfSimpleCMS2Plugin: based on SimpleCMSPlugin conception but with lots of new features (backend control panel, layout customization, integration YAML CSS Framework, easy integration on symfony plugins, optimized perfomance)
    • sfWikifyPlugin: reads from the schema.yml file and generates a doku-wiki-like syntax to describe it
    • sfVisualRoutesPlugin: display routes from the command line using the project:routes task
    • sfZendOpenIdPlugin: integrates the OpenID Consumer of Zend Framework with the popular sfGuardPlugin. It only supports authentication, but other functionality is to be added.
    • sfUPSShippingPlugin: allows you to retreive shipping cost estimates from the UPS XML API. You can submit to/from zip codes, and get back the cost for a specific shipping type, or get back a list of costs for all possible shipping types
    • sfSimplePagePlugin: allows you to manage like static pages with symfony
  • Updated plugins
    • sfI18NTranslatorPlugin: using sfForm to handle translate form, handle current culture in translate form, using rawurlencode / decodeURIComponent, added default value for language select, set default value on culture select tag, using selected culture in translation update, replacing args on return string, styling
    • sfPropelPlugin:
      • [1.2] added peer_method option to sfWidgetFormPropelChoice and sfWidgetFormPropelSelect
      • [1.2] updated API doc
      • [1.2] added hr, it, uk, pl, hu, ar, fi and tr translations for the admin generator
      • [1.2] added nested save support to sfPropelForm
      • [1.2] made some more tweaks for people with output escaping set to off
      • [1.2] added support for nested forms in the admin generator
      • [1.2] changed the order of objects saving in sfPropelForm
      • [1.2] changed an error message to be more descriptive
      • [1.2] fixed deep nested forms saving
      • [1.2] added i18n missing catalogue information
      • [1.2] fixed nested i18n forms
      • [1.2] added automatic route creation to propel:generate-admin
      • [1.2] converted some functional tests to the new syntax
      • [1.2] changed Propel externals to 1.3 branch
      • [1.2] added key_method to sfWidgetFormPropelSelect and sfWidgetFormPropelChoice
      • [1.2] fixed CLI tasks when some plugins register behaviors
      • [1.2] fixed order of data deleting in sfPropelData
      • [1.2] fixed location of the base form class for Propel and Doctrine
      • [1.2] fixed order of data deleting in sfPropelData
      • [1.2] splitted default admin generator stylesheets to global and default
      • [1.2] fixed i18n support for forms (WIP)
      • [1.2] added PDO debugging when running functional tests
      • [1.2] fixed i18n in Propel forms
      • [1.2] removed sf_admin_module_web_dir from settings.yml
    • ysfYUIPlugin: made yui_submit_tag name attribute customizable, updated README
    • ckWebServicePlugin: changed wsdl style from rpc/encoded to rpc/literal, refactored ckWebServiceController, fixed typo in ckSoapHandler, fixed method call in ckSoapHandler, fixed call to sfEventDispatcher::notify in ckSoapHandler, changed wsdl operation creation so a response message is always present, updated api documentation, renamed ckMemberResultAdapter to ckPropertyResultAdapter, added ckMethodResultAdapter, changed serialization to produce WS-I compliant wsdl, changed order in which header and parameter parts are added on operation creation, changed array type implementation to be ws-i compliant
    • sfCombinePlugin: do not version generated model files, avoid needless database queries when no JavaScript or CSS is present, allowed certain files to be exempt from combination (e.g. tiny_mce.js), fixed case when sfCombineFilter was applied to the output of the sfCombine module (in case a JavaScript file contains the string), fixed wrong way to access the plugin configuration (damn app.yml nesting levels), fixed cache on sfCombine module (the cache layer needs a view, using renderText() bypasses the cache)
    • swToolboxPlugin: added sw_t function helper, added admin backend js helper
    • ysfR3Plugin: fixed i18n:translate task does not translate all apps + added optimizations
    • sfTaskExtraPlugin: added generate:plugin and generate:plugin-module tasks, removed earlier event-based WIP, added test project to plugin generation, added plugin:package task, added package.xml template
    • sfExtjsThemePlugin: renamed renderer class to renderers, moved all possible common partials (constructor, initComponent, and initEvents) to be generated by the generator reducing a large number of very similar partial files in the generator templates directory, the partials are now generated strait to cache, cleaned up a lot of extra whitespace in the generated javascript, added and used the $className variable in the non-generated partials where applicable, moved rowactions config to the generator, moved toolbar top config to the generator, moved filterpanel config to generator, started re-working groupColumn methods to take advantage of dbfinders 'with' ability
    • sfDoctrinePlugin:
      • [1.2] initial entry of new admin generators for Doctrine
      • [1.2] fixed admin generator filters
      • [1.2] fixed indention of generated forms and removing primary key from generated filter forms
      • [1.2] cleaning up sqlite database files when functional test shuts down
      • [1.2] removing old admin generator and crud code
      • [1.2] updating default crud theme
      • [1.2] reporting admin and crud themes
      • [1.2] fixing sfDoctrineRoute
      • [1.2] added i18n missing catalogue information
      • [1.2] fixed typo and adding logic to use textarea for string columns with length greater than 255
      • [1.2] updated README file in markdown
      • [1.2] fixed issue with autoloading and custom behaviors
      • [1.2] added exception when user tries to use package parameter in symfony Doctrine schema files
      • [1.2] fixed sfFormDoctrine didn't save automatically the file
      • [1.2] fixed doctrine:build-forms issue
      • [1.2] fixed Doctrine does not create Plugin*Translation class
      • [1.2] fixed location of the base form class for Propel and Doctrine
      • [1.2] removed sf_admin_module_web_dir from settings.yml
      • [1.1] updated README file in markdown
      • [1.1] fixed issue with autoloading and custom behaviors
      • [1.1] fixed Doctrine does not create Plugin*Translation class
      • [1.1] added support for saving nested objects in sfFormDoctrine
      • [1.1] fixed dql task to output results of query in a more readable format
      • [1.1] fixed doctrine:build-forms
      • [1.0] fixed error in sfDoctrineAdminColumn class
    • isicsSitemapXMLPlugin: added branch for 1.2, fixed README
    • sfLucenePlugin: create branch for the sf1.1 Doctrine version, added doctrine support for symfony 1.1
    • sfBugsPlugin: small redesign, added CSS file, added title field, added subpage for detailed view of a bug, updated the README file, updated the new models for the changed DB structure
    • sfDoctrineGuardPlugin: fixed signin form doesn't include "remember" widget, fixed not all forms are bundled with sfDoctrineGuardPlugin
    • sfDoctrineManagerPlugin: fixed issue with use of old form helpers
    • sfDynamicCMSPlugin: merging changes from 1.0 to 1.1
    • sfPropelPlanetPlugin: created 1.1 branch, added few missing characters to sfDynamicCMSTools::stripText pattern array
    • sfXssSafePlugin: released 0.8 version (uses html purifier 3.2)
    • sfDoctrineSettingsPlugin: creating the proper subversion directory layout, moving all the current files to branches/1.0, updated package.xml file, changing the sample data so it doesn't load by default, updated the plugin to work with sfDoctrinePlugin 1.0, made the backend work with sfDoctrinePlugin 1.0, made the wysiwyg editor use the options field for extra options, updated the README file to be more helpful
    • sfHamlViewPlugin: created a branch for symfony 1.2 version of the plugin, made plugin compatible with symfony 1.1, fixed a problem with 5.2.6 which was only fixed in the 1.0 branch of the plugin
    • sfFeed2Plugin: new repo structure, fixed unit test, fixed gmt time in unit test
    • sfSmartyPlugin: updated AppUrlHelper.php
    • sfDoctrineUserPlugin: added a package.xml file for the 1.0 and 1.1 branches, added LICENSE files to all branches, updated the README files for markdown language, updated the package.xml file to have the correct dependencies, fixed a bug where when the sfGuardDoctrinePlugin was renamed to sfDoctrineGuardPlugin that the overriding of the sfGuardUser module did not work correctly, added the generator from sfGuardUser
    • sfDoctrinePollPlugin: made a package.xml file, updated the README file to be markup, added a LICENSE file, fixed the dependency for the sf1.0
    • sfImageTransformPlugin: branching to add mime detection functionality
    • sfDoctrineActAsTaggablePlugin: updated getObjectTaggedWith

Some new symfony powered websites

  • Miit.Me: (italian, english) the easy way to meet in real life people that you met over the Internet
  • Wotol: (english) buy or sell new and used industrial machines
  • Climalife: (english, french, german, dutch): Climalife, The Dehon Group brand markets products and services to refrigeration, climate control and heating industry professionals
  • Seconde Chance: (french) pet adoption portal

They talked about us

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

Additional tasks to streamline your workflow

Sat, 2008-11-08 13:15

The sfTaskExtraPlugin is a plugin maintained by the symfony core team. It adds a number of useful tasks to your symfony command line to help streamline your workflow. This plugin is relatively young, so I will just be discussing those tasks that we'll be using for today's Plugin Developers Day. I should also note this plugin requires symfony 1.2.

Plugin Tasks

We now turn our focus to easing the process of creating, developing and releasing plugins. The following tasks are included in sfTaskExtraPlugin:

  • generate:plugin
  • generate:plugin-module
  • plugin:package
New Task: generate:plugin

Very much like generate:app task, the generate:plugin task creates a basic plugin directory structure:

$ ./symfony generate:plugin myFirstPlugin

After running this command you should see the following plugin structure in your project's plugins directory:

myFirstPlugin/ config/ myFirstPluginConfiguration.class.php lib/ test/ bin/ prove.php bootstrap/ functional.php unit.php fixtures/ project/ functional/ unit/ LICENSE README package.xml.tmpl

As you can see, part of what this task does is setup a proper testing environment for your plugin, including an isolated symfony project to run your plugin's functional tests through. Once you've created your test scripts, you can easily execute them all by running the prove.php script before committing your code:

$ php plugins/myFirstPlugin/test/bin/prove.php

Building a robust unit and functional testing suite is a strongly recommended best practice, but if you would rather not bother you can simply include the --skip-test-dir option when generating your plugin.

New Task: generate:plugin-module

Often times a plugin will require one or more modules to support its functionality in the project, such as an administrative backend interface. In order for your module to be easily customizable for its housing project you will need to provide a "stub" actions class that can be replicated and customized in an application's modules directory.

Previously this would have involved either creating the module directory structure by hand, or running generate:module in your application and then copying, modifying and creating new files in your plugin. This process is now much more streamlined thanks to the generate:plugin-module task.

$ ./symfony generate:plugin-module myFirstPlugin myFirstModule

Executing this command will add the following to your plugin's directory structure:

modules/ myFirstModule/ actions/ actions.class.php lib/ BasemyFirstModuleActions.class.php templates/ test/ functional/ myFirstModuleActionsTest.php

As you develop this module, write all your action code in the /lib/BasemyFirstModuleActions.class.php file. Leaving the actions.class.php file empty makes it possible to replicate that file in the project and not lose or have to duplicate code from the plugin; this will be taken care of by the symfony autoloader and the magic of OOP inheritance.

This task also creates a functional test script for the new module and updates the embedded test project's config/settings.yml to enable the module.

New Task: plugin:package

Anyone who has released a symfony plugin in the past is familiar with the tedious task of filling out the necessary values in the requisite package.xml file. We have added the plugin:package task in order to speed up this process.

$ ./symfony plugin:package myFirstPlugin

This task will look for either a package.xml or package.xml.tmpl file in the plugin directory. If neither are found, the task will package the plugin using a default package.xml template. In any case, if any field values are not known the task will take advantage of one of the new symfony 1.2 task features and ask you for that information.

Once the task knows what it needs to package the plugin, a myFirstPlugin-0.0.1.tgz file will be created in the plugin directory. Once you upload this file to the symfony plugins application, your work will become immediately available to the entire symfony community.

What's Next?

The sfTaskExtraPlugin is young, but we have big plans for it. If you have any tasks you'd like to see incorporated, please let us know.

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

New in symfony 1.2: Doctrine goodies

Fri, 2008-11-07 03:00
Big Thanks

A lot of awesome stuff has been added recently to the next major symfony release, 1.2. Fabien has worked very hard to add without a doubt the most sophisticated features of any PHP framework that exists today. Not only are they nice features but he has implemented them in a OO way so that it is easy for me to implement the same features with another ORM, Doctrine. All this is done with very little work by me. So, give a big thanks to him if you enjoy this.

Real World Example

In this article I will start from the beginning with a brand new symfony 1.2 project so you can get going with Doctrine. We will use a schema for your typical, run-of-the-mill content management system. The schema consists of articles, authors and categories where the articles are internationalized.

Start your Project

First you need to initialize a brand new symfony 1.2 project and initialize a backend application. Make sure you are using the latest code from svn as beta1 did not include this Doctrine functionality.

Generate your project

$ mkdir cms $ cd cms $ symfony generate:project cms

Generate backend application

$ symfony generate:app backend Everybody get your Doctrine on

Now we need to enable Doctrine and disable Propel :) Edit your config/ProjectConfiguration.class.php and add the following code to your setup() function.

public function setup() { $this->enablePlugins(array('sfDoctrinePlugin')); $this->disablePlugins(array('sfPropelPlugin')); }  

Now that Doctrine is enabled we can list the available Doctrine tasks:

$ ./symfony list doctrine Available tasks for the "doctrine" namespace: :build-all Generates Doctrine model, SQL and initializes the database (doctrine-build-all) :build-all-load Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-load) :build-all-reload Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-reload) :build-all-reload-test-all Generates Doctrine model, SQL, initializes database, load data and run all test suites (doctrine-build-all-reload-test-all) :build-db Creates database for current model (doctrine-build-db) :build-filters Creates filter form classes for the current model :build-forms Creates form classes for the current model (doctrine-build-forms) :build-model Creates classes for the current model (doctrine-build-model) :build-schema Creates a schema from an existing database (doctrine-build-schema) :build-sql Creates SQL for the current model (doctrine-build-sql) :data-dump Dumps data to the fixtures directory (doctrine-dump-data) :data-load Loads data from fixtures directory (doctrine-load-data) :dql Execute a DQL query and view the results (doctrine-dql) :drop-db Drops database for current model (doctrine-drop-db) :generate-admin Generates a Doctrine admin module :generate-migration Generate migration class (doctrine-generate-migration) :generate-migrations-db Generate migration classes from existing database connections (doctrine-generate-migrations-db, doctrine-gen-migrations-from-db) :generate-migrations-models Generate migration classes from an existing set of models (doctrine-generate-migrations-models, doctrine-gen-migrations-from-models) :generate-module Generates a Doctrine module (doctrine-generate-crud, doctrine:generate-crud) :generate-module-for-route Generates a Doctrine module for a route definition :insert-sql Inserts SQL for current model (doctrine-insert-sql) :migrate Migrates database to current/specified version (doctrine-migrate) :rebuild-db Creates database for current model (doctrine-rebuild-db) The Schema

Now the fun begins. We have Doctrine enabled so the first thing we need to is define our schema for the CMS in config/doctrine/schema.yml.

--- Article: actAs: Timestampable: I18n: fields: [title, content] columns: author_id: integer status: type: enum values: [Draft, Published] notnull: true title: type: string(255) notnull: true content: type: clob notnull: true is_on_homepage: boolean published_at: timestamp relations: Author: foreignAlias: Articles Categories: class: Category refClass: ArticleCategory foreignAlias: Articles   Category: columns: name: type: string(255) notnull: true   Author: columns: name: type: string(255) notnull: true about: string(1000)   ArticleCategory: columns: article_id: integer category_id: integer relations: Article: foreignAlias: ArticleCategories Category: foreignAlias: ArticleCategories   Data Fixtures

We have our schema, now we need some data to test against so copy the following YAML in to data/fixtures/data.yml

--- Article: Article_1: Author: jwage status: Published is_on_homepage: true published_at: '<?php echo date("Y-m-d h:i:s"); ?>' Categories: [article, ontheedge] Translation: en: title: symfony 1.2 and Doctrine content: Article about the new Doctrine integration in symfony 1.2 fr: title: symfony 1.2 et doctrine content: Article sur l'intégration de Doctrine dans symfony 1.2   Author: jwage: name: Jonathan H. Wage about: Jonathan is the lead developer of the Doctrine project and is also a core contributor to the symfony project.   Category: article: name: Article tutorial: name: Tutorial ontheedge: name: Living on the edge   Building and Testing

Now that we have our schema and data fixtures we have everything we need to initialize our database, models, forms, data, etc. This can all be done with the extremely simply command below:

$ ./symfony doctrine:build-all-reload --no-confirmation >> doctrine dropping databases >> doctrine creating databases >> doctrine generating model classes >> doctrine generating sql for models >> doctrine generating form classes >> doctrine generating filter form classes >> doctrine created tables successfully >> doctrine loading data fixtures from "/Us...ymfony12doctrine/data/fixtures"

That was too easy, when is this gonna get hard? Now lets do some inspecting with DQL to see that the data was loaded properly.

$ ./symfony doctrine:dql "FROM Article a, a.Author a2, a.Translation t" >> doctrine executing dql query DQL: FROM Article a, a.Author a2, a.Translation t found 1 results - id: '1' author_id: '1' status: Published is_on_homepage: true published_at: '2008-11-06 04:37:11' created_at: '2008-11-06 16:37:11' updated_at: '2008-11-06 16:37:11' Author: id: '1' name: 'Jonathan H. Wage' about: 'Jonathan is the lead developer of the Doctrine project and is also a core contributor to the symfony project.' Translation: en: id: '1' title: 'symfony 1.2 and Doctrine' content: 'Article about the new Doctrine integration in symfony 1.2' lang: en fr: id: '1' title: 'symfony 1.2 et doctrine' content: 'Article sur l''intégration de Doctrine dans symfony 1.2' lang: fr

That may be your first taste of the Doctrine Query Language, also known as DQL. Looks a lot like SQL huh? Close your mouth, you're drooling.

For those of you who don't wanna write raw DQL strings, don't worry we have a fully featured Doctrine_Query object for building your queries.

$q = Doctrine_Query::create() ->from('Article a, a.Author a2, a.Translation t'); $articles = $q->execute();   Admin Generators

Now that we have everything built, we can start generating some magic with symfony. Lets start first by defining the route collections for managing our articles, authors and categories. Open apps/backend/config/routing.yml in your editor and paste the following routes inside.

articles: class: sfDoctrineRouteCollection options: test: true model: Article module: articles with_show: true collection_actions: { filter: post, batch: post }   categories: class: sfDoctrineRouteCollection options: model: Category module: categories with_show: true collection_actions: { filter: post, batch: post }   authors: class: sfDoctrineRouteCollection options: model: Author module: authors with_show: true collection_actions: { filter: post, batch: post }  

These routes will allow us to generate an admin generator module for managing the data through each of the Doctrine models. Run the following commands to generate the three modules.

$ ./symfony doctrine:generate-admin backend articles $ ./symfony doctrine:generate-admin backend categories $ ./symfony doctrine:generate-admin backend authors

Now when you access the categories module from the backend you should see the following.

Now you may want to slightly customize the articles admin generators to display only a certain set of fields in the list and filters form. You can do so by editing the generator.yml located in apps/backend/modules/categories/config/generator.yml.

config: list: display: [title, published_at, is_on_homepage, status] filter: class: ArticleFormFilter display: [author_id, status, is_on_homepage, published_at, categories_list]  

You can customize the other modules as well in the same way.

Now if you change the url to have ?sf_culture=fr in your url the title will show the french version. You should see the following in your browser when you pull up the articles module.

Editing Translations

Now how do we go about editing those translations? If you remember this with the old admin generators, it was pretty much impossible. Now, it is a matter of adding one line of code to our ArticleForm. All we need to do is embed the ArticleTranslation form. This can be done by editing lib/form/doctrine/ArticleForm.class.php and adding the following code to configure()

public function configure() { $this->embedI18n(array('en', 'fr')); }  

Now when you edit an article you will see you have the ability to edit translations directly inside of the article.

Now that is pretty cool but what if you want to edit the Author directly inside of the Article as well and have it create a new Author if it doesn't exist and use the existing Author record if that name does exist. This is simple as well.

Edit/Add Author

In order to add the above described functionality we need to add a little bit of code in three difference places. First you need to embed the Author form in to the Article form by editing lib/form/doctrine/ArticleForm.class.php and add the following code.

public function configure() { unset($this['author_id']); $authorForm = new AuthorForm(); unset($authorForm['about']); $this->embedForm('Author', $authorForm);   $this->embedI18n(array('en', 'fr')); }  

Now we need to modify the articles actions class to join in the Author information so that the data is present in the form when editing. Open apps/backend/modules/articles/actions/actions.class.php and override the executeEdit() function.

public function executeEdit(sfWebRequest $request) { $this->article = Doctrine_Query::create() ->from('Article a') ->leftJoin('a.Author a2') ->where('a.id = ?', $request->getParameter('id')) ->fetchOne(); $this->form = $this->configuration->getForm($this->article); }  

Now when you view the form for editing and creating an Article you will see the embedded form for the Author with the existing information populated.

Now the last step is to tell Doctrine to look for existing Author objects when setting the name so that duplicate Author names are not created. Edit lib/model/doctrine/Author.class.php and override the name mutator by adding the following code.

public function setName($name) { $name = trim($name); $found = Doctrine_Query::create() ->select('a.id') ->from('Author a') ->where('a.name = ?', $name) ->fetchOne(array(), Doctrine::HYDRATE_ARRAY); if ($found) { $this->assignIdentifier($found['id']); } else { $this->_set('name', $name); } }  

The above code will check if an Author with the passed name already exists, if it does it will assign the identifier of the found record otherwise it does what it normally would do, set the name to the object.

The _set() and _get() methods must be used to avoid a infinite loop when overriding accessors and mutators.

Wow, did we really just build an entire backend for managing the content of a simple content management system in under an hour? We sure did. Try it for yourself today and enjoy developing rich web based functionality using symfony and Doctrine.

THE END

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

Plugin Developers Day This Saturday!

Thu, 2008-11-06 01:15
Plugin Developers Day This Saturday!

Preparations for the coming plugin developers day on Nov. 8th are proceeding apace. I've heard from a number of you who are planning to attend, some planning to start development on new, groudbreaking plugins, others looking to help update existing plugins to work with the latest and greatest version of symfony, and still others just hoping to learn from the lively discussion. All types are welcome!

To provide some structure for the day I've included a basic schedule below. As this is the first event of its kind we've organized, this schedule is subject to change as the day goes on.

Time Session Location 3:00-5:00 PM Creating and releasing a plugin #symfony 5:00-7:00 PM Writing a customizable plugin #symfony 7:00-9:00 PM Ad hoc coding sprints on new and existing plugins #symfony

These times are GMT. Check out this nifty utility for translating them to your time zone (thanks Dennis Benkert).

In other news

I'm happy to promote a presentation of symfony by Christopher MANEU at the Toulibre conference happening on Wednesday November 26th. More details are available on the event's Facebook page.

If you are organizing a symfony event or presentation, please send me an email with the vital information so we can support your evangelizing this great framework.

Be trained by symfony experts - Nov 19 Paris - Dec 10 Paris - Dec 10 Atlanta - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

New in symfony 1.2: God save the nested forms

Wed, 2008-11-05 20:44

In symfony 1.1, we introduced a new form sub-framework. It is a great step forward for symfony, even if the learning curve is a bit steep.

We worked a lot to make it even better for symfony 1.2, and more importantly simpler for newcomers.

One of the form framework strengths is its ability to deal with nested forms. A form can embed a form which in turn can also embed another form (and so on):

$article = ArticlePeer::doSelectOne(new Criteria());   $articleForm = new ArticleForm($article); $authorForm = new AuthorForm($article->getAuthor()); $companyForm = new AuthorForm($article->getAuthor()->getCompany());   $authorForm->embedForm('company', $companyForm); $articleForm->embedForm('author', $authorForm);  

The articleForm renders as show below:

Another great feature of the form framework is its ability to automatically serialize forms. As the forms above are Propel ones, a simple call to $articleForm->save() will automatically update the $article object with the submitted and validated values and save it back to the database.

But there was a problem in symfony 1.1. The author and the company objects were not saved automatically. So, you had to override the save() method to get the validated data and update the objects manually. Nothing impossible to do, but really annoying as the form framework already had all the needed information to make it automatic.

This has been implemented in symfony 1.2 and so will be available with the upcoming beta 2. That's right, a single call to $form->save() will now update the article, the author, and the company objects. That's a great news by itself, but there is another one: The new admin generator also takes this new feature into account.

Be trained by symfony experts - Nov 19 Paris - Nov 26 Atlanta - Dec 10 Paris - Dec 17 Montreal - Jan 21 Paris
Categories: php, software

A week of symfony #96 (27 october -> 2 november 2008)

Sun, 2008-11-02 23:45

As promised, the first beta version of symfony 1.2 was released this week, featuring lots of improvements and a new admin generator. In addition, symfony project activity has been stunning during this week: more than 200 changesets, 5 new plugins, 15 updated plugins, 8 new symfony-powered websites and lots of documentation updates.

Development mailing list

Development highlights

  • r12395: [1.2] ordered output of app:routes task
  • r12400: [1.2] added an option to change the id segment and changed the default requirement for it to be an integer
  • r12403: [1.2] changed ConfigureCore task to PublishAssets. It also now iterates over all installed plugins, rather than only the core plugins. Only working with core plugins can be enforced with --only-core
  • r12405: [1.2] fixed plugin:publish-assets task
  • r12409: [1.2] added support for stylesheets and javascripts in the form framework
  • r12426: [1.1] fixed typo on sfGenerateAppTask.class.php
  • r12427: [1.2] added the possibility to add collection and object actions in collection routes
  • r12430, r12431: [new_admin] added the new admin generator
  • r12434: [new_admin] changed all i18n strings to be under a sf_admin catalogue, added French translations, added the possibility to change the catalogue for user strings
  • r12439: [new_admin] added full support for credentials on actions (link display and action protection)
  • r12455: [new_admin] added support for virtual columns in list
  • r12472: [1.2] added unit tests for sfWidgetFormChoice
  • r12473, r12474: [1.2] merged new_admin branch to 1.2
  • r12475, r12476: [new_admin] removed branch
  • r12479: [1.2] added type hinting for generated execute methods in actions
  • r12481: [1.2] fixed action parameter customization
  • r12483: [1.2] better management of virtual fields and their configuration in the admin generator
  • r12492: [1.2] reimplemented batch actions
  • r12499: [1.1, 1.2] fixed alternate dispatcher being used in command application
  • r12500: [1.2] changed batch actions to be 'real' actions
  • r12503: [1.2] added plugin configuration classes
  • r12506: [1.2] added m2m fields even if nothing is configured in the admin generator
  • r12517: [1.2] added regex support to sfTesterResponse::isHeader()
  • r12519: [1.2] added an halt_on_error option to sfValidatorAnd
  • r12535: [1.2] made sure that sfSimpleAutoload is only registered once
  • r12536: [1.2] fixed autoloading in tasks
  • r12541: [1.2] refactored plugin:publish-assets a bit and make it run when generating a new project
  • r12542: [1.2] added a task to install core plugin assets in the upgrade task
  • r12544: [1.2] pruned symfony dir when autoloading project lib in tasks
  • r12547: [1.2] fixed bad unicode chars in XLIFF files
  • r12548, r12549: [1.1, 1.2] fixed global cache never cleared
  • r12550: Tagged symfony 1.2 beta1 version
  • r12579: [1.2] added tif datatype in mime_types.dat. closes
  • r12584: [1.2] added skeleton fixtures.yml with inline documentation
  • Updated dwhittle branch
  • ...and many other changes

Development digest: 213 changesets, 31 defects created, 39 defects closed, 16 enhancements created, 30 enhancements closed, 7 documentation defects created, 2 documentation defects closed and 52 documentation edits.

Book and documentation

Plugins

  • New plugins
    • sfBugsPlugin: easily report bugs for modules and actions
    • sfI18NTranslatorPlugin: allows to translate an application inline
    • sfForms12Plugin: allows to use the symfony 1.2 form framework into a symfony 1.0 project, or in a non symfony project
    • simpleForumPropel13Plugin: sfSimpleForumPlugin with Propel 1.3
    • sfCombinePlugin: combines multiple JavaScript and CSS files into one JavaScript and one CSS file at runtime, in order to minimize the number of HTTP requests required to render a given page
  • Updated plugins
    • DbFinderPlugin: added the ability to call useCache(true) to let DbFinder choose a caching backend (works for both Propel and Doctrine adapters)
    • sfEasyGmapPlugin: Gmap default constant modification
    • sfExtjsThemePlugin: adding in new components from the original theme, added sfmixin to add init to either propel or doctrine generators, scrapped the mixer idea and switched the variable inits to getters, moved controller and tableDelimiter to the generator, coverted remaming template files, fixed bugs with toolbar_paging, fixed paging in executeList, resolved a bug with boolean columns json, removed unused custom il8n method, fixes for credential checking on rowaction, tweaks to skeleton generator.yml commented examples and to list_renderer, fixed getXtype module, fixed getModule in generator, single edits now working, single deletes now working, fixed listcolumn editable config settings in generator, moved store config to generator, moved columnmodel config to the generator, moved store config to the generator, moved gridpanel config to the generator, moved custom methods and variable generation for all classes to the generator
    • swToolboxPlugin: fixed typo, add swWidgetFormInputCheckboxGroup widget, render only a simple radio html tag for swWidgetFormRadio, added "dynamic form values" features
    • sfPropelSlotBehaviorPlugin: released 0.1.9 and 0.1.10 versions (refactor & bug fixes)
    • sfCombineFilterPlugin: switched to JsMin and CssMin to minimize css and javascript
    • sfDoctrineUserPlugin: copying all the code from 1.1 branch to the trunk so we can begin work on a 1.2 version, changing the way the fixtures are set up
    • sfFormExtraPlugin: removed the need to add an onsubmit attribute to the form tag for the DoubleList widget, added getJavascripts() and getStylesheets() method to the widgets that need some javascripts or stylesheets to work, added some references to articles talking about the plugin
    • sfDynamicCMSPlugin: pre-released 0.3.3.4 and 0.3.3.5 versions (fix bugs & refactoring), pre-released 0.3.3.6 version (some improvements)
    • sfJqueryReloadedPlugin: added a path for plugin dir
    • ysfYUIPlugin: changed symlink path
    • sfPropelPlugin:
      • [new_admin]: changed all i18n strings to be under a sf_admin catalogue, added French translations, added the possibility to change the catalogue for user strings, renamed sfPropelCrud to sfPropelModule, added pt_BR, nl, de, ja, no, es, dk, es_AR, bg, pl, it, zh_CN, sk, id, ro, ru, tr, pt, fi and cs translations, added full support for credentials on actions, added events for plugins (admin.pre_execute, admin.build_criteria, admin.save_object), fixed CSS, added support for virtual columns in list, added partial and component support for filters, added credential support for list, readded the possibility to change the default stylesheet
      • [1.2]: added type hinting for generated execute methods in actions, fixed action parameter customization, better management of virtual fields and their configuration in the admin generator, improved filters and form real field detection, reimplemented batch actions, changed batch actions to be 'real' actions, added m2m fields even if nothing is configured in the admin generator, added a new event when a record is deleted in the admin generator, fixed autoloading in tasks, fixed getRawValue() on empty array
    • sfTaskExtraPlugin: initial import generate:module extension
    • sfFeed2Plugin: fixed timestamp not UTC
    • ckWebServicePlugin: completed api documentation

Some new symfony powered websites