User loginNavigation |
SymfonyCall the expert: Retrieving Data with DoctrineIn 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 FixturesFirst 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 FunctionsFirst 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 JoinsDoctrine 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-QueriesWe 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 JoinsOne 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 QueriesDoctrine_Query can be used to specify UPDATE and DELETE queries by simply using the update() or delete() functions. Here are some examples. Delete QueryIn this example we will delete a user by his username. Doctrine_Query::create() ->delete() ->from('User u') ->where('u.username = ?', 'jwage') ->execute(); Update QueryIn 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 DQLFor 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 QueriesIn 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 HydrationHere 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 HydrationWe 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 RecordYou 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 Parissymfony 1.1.5 releasedThe new 1.1.5 maintenance version of symfony has just been released today. Here's the changelog:
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.5If 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 ParisA week of symfony #98 (10->16 november 2008)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
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
Some new symfony powered websites
They talked about us
Call the expert: Customizing sfDoctrineGuardPluginGetting 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 sfDoctrineGuardPluginThe 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/sfDoctrineGuardPluginNow 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 ccEnable 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 FixturesFirst 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: oneNow 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.comNow 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-reloadWell 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.comYou 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 CustomizingNow 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.ymlNow 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 ProductNow 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 Parissymfony 1.2 beta2 - The Doctrine Admin Generator is hereWe 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
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 tryWe 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 ccThis 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. ContributeDo you want to see the new admin generators in your language? Then:
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 ParisCall the expert: Nested forms - A real implementationAs 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 projectLet'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 InitializationIf you want to follow along, create a new symfony project the usual way: $ mkdir classifieds $ cd classifieds $ symfony generate:project classifiedsThen 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-loadIn 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 backendThen create the backend module to list, create, edit, and delete the ads: $ ./symfony propel:generate-admin backend DemoAdThe 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 CustomizationAs 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 ParisA week of symfony #97 (3->9 november 2008)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
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
Some new symfony powered websites
They talked about us
Additional tasks to streamline your workflowThe 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 TasksWe now turn our focus to easing the process of creating, developing and releasing plugins. The following tasks are included in sfTaskExtraPlugin:
Very much like generate:app task, the generate:plugin task creates a basic plugin directory structure: $ ./symfony generate:plugin myFirstPluginAfter 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.tmplAs 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.phpBuilding 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-moduleOften 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 myFirstModuleExecuting 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.phpAs 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:packageAnyone 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 myFirstPluginThis 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 ParisNew in symfony 1.2: Doctrine goodiesBig 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 ExampleIn 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 ProjectFirst 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 cmsGenerate backend application $ symfony generate:app backend Everybody get your Doctrine onNow 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 SchemaNow 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 FixturesWe 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 TestingNow 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: frThat 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 GeneratorsNow 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 authorsNow 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 TranslationsNow 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 AuthorIn 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 ParisPlugin Developers Day This Saturday!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 #symfonyThese times are GMT. Check out this nifty utility for translating them to your time zone (thanks Dennis Benkert). In other newsI'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 ParisNew in symfony 1.2: God save the nested formsIn 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 ParisA week of symfony #96 (27 october -> 2 november 2008)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
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
Some new symfony powered websites
|