I don’t normally just link to other people’s blog posts in lieu of writing my own content, but this was so incredibly hard for me to find that I want to link to it in hopes that other people will find it. Here you go:
Archive for the ‘symfony’ Category
How to create a custom theme for symfony’s admin generator (Doctrine)
Thursday, November 11th, 2010Shell script to install symfony
Friday, October 15th, 2010# This script follows the symfony installation instructions here: # http://www.symfony-project.org/getting-started/1_4/en/04-Project-Setup # # Written by Jason Swett # http://jasonswett.net/ # # To use this script, run ./script-name project-directory-name symfony_version="1.4.8" project_directory=$1 mkdir $project_directory cd $project_directory zip_filename="symfony-$symfony_version.tgz" mkdir -p lib/vendor cd lib/vendor wget http://www.symfony-project.org/get/$zip_filename tar zxpf $zip_filename mv symfony-$symfony_version symfony rm $zip_filename
Saving Multiple Objects With One Form in symfony
Monday, October 4th, 2010Using Doctrine, the symfony developers have set up a pretty nifty form system that makes setting up forms a breeze. (I can personally say that it’s worlds apart from Zend’s form system, which, as of ZF 1.6, was pretty sad.) When you’re dealing with just one object at a time, symfony gives you a pretty clear path to take. What about if you’re saving two or more objects, though? It’s not quite as clear. In this tutorial I’ll walk you through the steps to saving multiple objects with a symfony form.
The Schema
In this tutorial we’ll be using two tables: person and pet. Each person has a name. Each pet has a name and a person (its owner).
Here’s the SQL for the two tables:
CREATE TABLE `person` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;</code> CREATE TABLE `pet` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) DEFAULT NULL, `person_id` BIGINT(20) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), KEY `owner_id` (`person_id`), CONSTRAINT `pet_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Or, if you like, here’s the YAML:
Person:
connection: doctrine
tableName: person
columns:
id:
type: integer(8)
fixed: false
unsigned: false
primary: true
autoincrement: true
name:
type: string(255)
fixed: false
unsigned: false
primary: false
notnull: false
autoincrement: false
relations:
Pet:
local: id
foreign: person_id
type: many
Pet:
connection: doctrine
tableName: pet
columns:
id:
type: integer(8)
fixed: false
unsigned: false
primary: true
autoincrement: true
name:
type: string(255)
fixed: false
unsigned: false
primary: false
notnull: false
autoincrement: false
person_id:
type: integer(8)
fixed: false
unsigned: false
primary: false
notnull: true
autoincrement: false
relations:
Person:
local: person_id
foreign: id
type: one
I strongly encourage you to use the SQL version, though. Look at the YAML file. Where are the unique keys? They get lost in translation. If you want to really control your database, build it with SQL and generate the YAML from that, not the other way around.
Setting Up The Basics
First generate your frontend app.
php symfony generate:app frontend
If you used SQL, build your model, forms and filters like this:
php symfony doctrine:build-model php symfony doctrine:build-forms php symfony doctrine:build-filters
Otherwise use build-all:
php symfony doctrine:build-all
Now generate your Pet and Person modules:
php symfony doctrine:generate-module frontend pet Pet php symfony doctrine:generate-module frontend person Person
Now if we navigate to http://your-project/person/new, we should see a “New Person” form. Same deal with /pet/new. That’s fine if we want to create people and pets separately, but we want to create a person and a pet in one fell swoop. We’ll achieve this by splicing the person form into the pet form.
Splicing In The Person Form
Before we do all this, quickly go to /person/new and create a new person. We’ll need at least one person in order to test the pet form. It’s probably a good idea to then save one pet, just to make sure our forms are working in the first place.
Go to /apps/frontend/modules/pet/actions/actions.php and change the executeNew() method to look like this:
public function executeNew(sfWebRequest $request) { $this->pet_form = new PetForm(); $this->person_form = new PersonForm(); }
Change /apps/frontend/modules/pet/templates/newSuccess.php to look like this:
<h1>New Pet</h1> <?php include_partial('form', array('pet_form' => $pet_form, 'person_form' => $person_form)) ?>
Now open /apps/frontend/modules/pet/templates/_form.php. Change all instances of $form to $pet_form and add a row for person name above the row for pet name.
<?php use_stylesheets_for_form($pet_form) ?> <?php use_javascripts_for_form($pet_form) ?> <?php use_stylesheets_for_form($person_form) ?> <?php use_javascripts_for_form($person_form) ?> <form action="<?php echo url_for('pet/'.($pet_form->getObject()->isNew() ? 'create' : 'update').(!$pet_form->getObject()->isNew() ? '?id='.$pet_form->getObject()->getId() : '')) ?>" method="post" <?php $pet_form->isMultipart() and print <?php if (!$pet_form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="put" /> <?php endif; ?> <table> <tfoot> <tr> <td colspan="2"> <?php echo $pet_form->renderHiddenFields(false) ?> <?php echo $person_form->renderHiddenFields(false) ?> <a href="<?php echo url_for('pet/index') ?>">Back to list</a> <?php if (!$pet_form->getObject()->isNew()): ?> <?php echo link_to('Delete', 'pet/delete?id='.$pet_form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?')) ?> <?php endif; ?> <input type="submit" value="Save" /> </td> </tr> </tfoot> <tbody> <?php echo $pet_form->renderGlobalErrors() ?> <?php echo $person_form->renderGlobalErrors() ?> <tr> <th><?php echo $person_form['name']->renderLabel() ?></th> <td> <?php echo $person_form['name']->renderError() ?> <?php echo $person_form['name'] ?> </td> </tr> <tr> <th><?php echo $pet_form['name']->renderLabel() ?></th> <td> <?php echo $pet_form['name']->renderError() ?> <?php echo $pet_form['name'] ?> </td> </tr> <tr> <th><?php echo $pet_form['person_id']->renderLabel() ?></th> <td> <?php echo $pet_form['person_id']->renderError() ?> <?php echo $pet_form['person_id'] ?> </td> </tr> </tbody> </table> </form>
If you now go to /pet/new in the browser, you’ll see two identical rows, each labeled “Name.” That’s confusing so let’s fix that.
Open /lib/form/doctrine/PetForm.class.php and change the configure() method like this:
class PetForm extends BasePetForm { public function configure() { $this->widgetSchema['name']->setLabel('Pet Name'); } }
Do the same with the PersonForm class and your labels with be clearer.
Saving The Person Record
Now that we have the person form spliced into the pet form, we can save the person record. Open /apps/frontend/modules/pet/actions/actions.class.php and change the executeCreate method like this:
public function executeCreate(sfWebRequest $request) { $this->forward404Unless($request->isMethod(sfRequest::POST)); $this->pet_form = new PetForm(); $this->person_form = new PersonForm(); $this->pet_form->bind($request->getParameter($this->pet_form->getName()), $request->getFiles($this->pet_form->getName())); $this->person_form->bind($request->getParameter($this->person_form->getName()), $request->getFiles($this->person_form->getName())); if ($this->pet_form->isValid() && $this->person_form->isValid()) { $person = $this->person_form->save(); $pet = $this->pet_form->updateObject(); $pet->setPersonId($this->person_form->getObject()->getId()); $pet->save(); $this->redirect('pet/edit?id='.$pet->getId()); } $this->setTemplate('new'); }
Now, when you save your pet form, not only will a pet be created, but a person as well. There’s a problem with this code, though. What if the person save goes okay but the pet save fails? We’ll end up with a new person in our database but no pet, which is not what we wanted to do. The saving of this form should be an all-or-nothing action. To make sure that this is the case, let’s wrap this code in a transaction:
public function executeCreate(sfWebRequest $request) { $this->forward404Unless($request->isMethod(sfRequest::POST)); $this->pet_form = new PetForm(); $this->person_form = new PersonForm(); $this->pet_form->bind($request->getParameter($this->pet_form->getName()), $request->getFiles($this->pet_form->getName())); $this->person_form->bind($request->getParameter($this->person_form->getName()), $request->getFiles($this->person_form->getName())); if ($this->pet_form->isValid() && $this->person_form->isValid()) { $conn = Doctrine_Manager::connection(); try { $conn->beginTransaction(); $person = $this->person_form->save(); $pet = $this->pet_form->updateObject(); $pet->setPersonId($this->person_form->getObject()->getId()); $pet->save(); $conn->commit(); } catch(Exception $e) { throw new Exception($e->getMessage()); $conn->rollback(); } $this->redirect('pet/edit?id='.$pet->getId()); } $this->setTemplate('new'); }
Now we have another problem, albeit a less severe one: our function is getting too big. Let’s do some refactoring.
public function executeCreate(sfWebRequest $request) { $this->forward404Unless($request->isMethod(sfRequest::POST)); $this->pet_form = new PetForm(); $this->person_form = new PersonForm(); $this->pet_form->bind($request->getParameter($this->pet_form->getName()), $request->getFiles($this->pet_form->getName())); $this->person_form->bind($request->getParameter($this->person_form->getName()), $request->getFiles($this->person_form->getName())); if ($this->pet_form->isValid() && $this->person_form->isValid()) { $this->processPetForm($this->pet_form, $this->person_form); } $this->setTemplate('new'); } protected function processPetForm(sfForm $pet_form, sfForm $person_form) { $conn = Doctrine_Manager::connection(); try { $conn->beginTransaction(); $person = $person_form->save(); $pet = $pet_form->updateObject(); $pet->setPersonId($person_form->getObject()->getId()); $pet->save(); $conn->commit(); } catch(Exception $e) { throw new Exception($e->getMessage()); $conn->rollback(); } $this->redirect('pet/edit?id='.$pet->getId()); }
That’s a little better. We should probably take it further than that but that’s good enough for now.
Cleaning Up
Isn’t it a little silly that we still have the person select field in the pet form? Let’s take that out.
First we have to make person_id not required.
/lib/form/doctrine/PetForm.class.php
public function configure() { $this->widgetSchema['name']->setLabel('Pet Name'); unset($this->validatorSchema['person_id']); }
Now take person_id out of /apps/frontend/modules/pet/templates/_form.php:
<?php use_stylesheets_for_form($pet_form) ?> <?php use_javascripts_for_form($pet_form) ?> <?php use_stylesheets_for_form($person_form) ?> <?php use_javascripts_for_form($person_form) ?> <form action="<?php echo url_for('pet/'.($pet_form->getObject()->isNew() ? 'create' : 'update').(!$pet_form->getObject()->isNew() ? '?id='.$pet_form->getObject()->getId() : '')) ?>" method="post" <?php $pet_form->isMultipart() and print <?php if (!$pet_form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="put" /> <?php endif; ?> <table> <tfoot> <tr> <td colspan="2"> <?php echo $pet_form->renderHiddenFields(false) ?> <?php echo $person_form->renderHiddenFields(false) ?> <a href="<?php echo url_for('pet/index') ?>">Back to list</a> <?php if (!$pet_form->getObject()->isNew()): ?> <?php echo link_to('Delete', 'pet/delete?id='.$pet_form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?')) ?> <?php endif; ?> <input type="submit" value="Save" /> </td> </tr> </tfoot> <tbody> <?php echo $pet_form->renderGlobalErrors() ?> <?php echo $person_form->renderGlobalErrors() ?> <tr> <th><?php echo $person_form['name']->renderLabel() ?></th> <td> <?php echo $person_form['name']->renderError() ?> <?php echo $person_form['name'] ?> </td> </tr> <tr> <th><?php echo $pet_form['name']->renderLabel() ?></th> <td> <?php echo $pet_form['name']->renderError() ?> <?php echo $pet_form['name'] ?> </td> </tr> </tbody> </table> </form>
And you’re done. You probably noticed that the edit page doesn’t work right. It won’t, of course, until you do similar things in the edit/update actions that you did in the new/create actions.
How to Write a symfony Plugin
Wednesday, September 8th, 2010One of the main things that makes symfony great is its thorough, clearly-written documentation. However, the documentation on how to write a plugin seems a trifle incomplete, so I will attempt to supplement it here by writing a comprehensive, step-by-step tutorial.
First let me point out what is already out there. The official documentation on how to write a symfony plugin is here. There is also a slideshow here (apparently authored by Fabien himself) that sheds some more light on the subject. Using these two sources I was able to piece together a plugin of my own but it took some head scratching. My goal here is to walk you through the plugin creation process with minimal cognitive strain.
The Plugin: jsHelloWorldPlugin
The plugin we will write today will be called jsHelloWorldPlugin. A quick note about the name: The “sf” prefix is reserved for symfony-ordained plugins. I chose “js” for my initials but the first two letters can, of course, be whatever you want them to be. The name of the plugin should end in “Plugin”.
To keep things simple, we’ll have this plugin perform one simple (and classic) task: print “Hello, world!” to the screen. We’ll accomplish this by creating a symfony task.
Installing sfTaskExtraPlugin
The sfTaskExtraPlugin has a nifty task bundled with it that lets you kick off your plugin creation by running a simple command. Let’s start by installing that:
$ php symfony plugin:install sfTaskExtraPlugin
Generating the Task
We can now generate the plugin by running the following command:
$ php symfony generate:plugin jsHelloWorldPlugin
Writing the Task
Next we’ll create a task. As you can see in the symfony task documentation, you can create a task using generate:task. We’ll call our task helloWorld, so we’ll run this command:
$ php symfony generate:task helloWorld
This generates a task at lib/task/helloWorldTask.class.php. This isn’t ultimately where we’ll want it but we’ll leave it there for now while we get the task working. Since all we need to do is print “Hello, world!”, let’s clear the contents of the execute() method and put our own code in:
protected function execute($arguments = array(), $options = array()) { echo 'Hello, world!'."\n"; }
Let’s see what happens when we try to run the task:
$ php symfony helloWorld Hello, world!
Success! Now that we have that task working, let’s move it from our project and into our plugin. First we’ll have to create a lib/task directory in our plugin folder:
$ mkdir plugins/jsHelloWorldPlugin/lib/task
Now let’s move the task:
$ mv lib/task/helloWorldTask.class.php plugins/jsHelloWorldPlugin/lib/task/
If you try to run the task again, you’ll notice that it won’t work. That’s okay. We have to install the plugin properly in order for the task to work again.
Getting the Plugin Ready For Installation
There are certain things that need to (or at least should) be in place in order for people to be able to install our plugin. First let’s make a README file at jsHelloWorldPlugin/README
jsHelloWorldPlugin
=================
This plugin prints "Hello, World!" to the screen.
Installation
------------
* Install the plugin
$ symfony plugin:install jsHelloWorldPlugin.tgz
* Clear the cache
$ symfony cache:clear
Documentation
-------------
Once the plugin is installed, run the following command:
$ symfony helloWorld
Next we’ll create a license file at plugins/jsHelloWorldPlugin/LICENSE:
Copyright (c) 2010 Jason Swett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Now we’ll take the important step of creating a package.xml file at plugins/jsHelloWorldPlugin/package.xml. You can see what a package file is supposed to look like here. Ours can look like this:
<?xml version="1.0" encoding="UTF-8"?> <package packagerversion="1.4.6" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> <name>jsHelloWorldPlugin</name> <channel>plugins.symfony-project.org</channel> <summary>sample plugin</summary> <description>Just a test plugin</description> <lead> <name>Jason Swett</name> <user>jasonswett</user> <email>jason.swett@gmail.com</email> <active>yes</active> </lead> <date>2010-09-08</date> <time>15:54:35</time> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.org/license">MIT license</license> <notes>-</notes> <contents> <dir name="/"> <file role="data" name="README" /> <file role="data" name="LICENSE" /> <dir name="lib"> <dir name="task"> <!-- tasks --> <file role="data" name="helloWorldTask.class.php" /> </dir> </dir> </dir> </contents> <dependencies> <required> <php> <min>5.1.0</min> </php> <pearinstaller> <min>1.4.1</min> </pearinstaller> <package> <name>symfony</name> <channel>pear.symfony-project.com</channel> <min>1.4.0</min> </package> </required> </dependencies> <phprelease /> <changelog /> </package>
Now we’re ready to wrap everything up into a nice package. Rename your plugin directory to include the version name:
$ mv plugins/jsHelloWorldPlugin/ jsHelloWorldPlugin-1.0.0
Now compress the plugin folder into a .tgz file:
$ tar cvzf jsHelloWorldPlugin-1.0.0.tgz jsHelloWorldPlugin-1.0.0/
Installing the Plugin
Now you can install the plugin like this:
$ php symfony plugin:install jsHelloWorldPlugin-1.0.0.tgz
Testing the Plugin
To test your plugin, simply run your helloWorld task like you did before:
$ php symfony helloWorld
Congratulations! You just wrote your first plugin.
jsDoctrineSchemaOverriderPlugin
Tuesday, September 7th, 2010Why I Wrote This Plugin
In order to understand what this plugin is for, it’s helpful to have a little background. There are two ways to handle your schema in symfony:
Method 1: Write your config/doctrine/schema.yml by hand (for this post we’ll assume you’re using Doctrine) and run symfony doctrine:build-sql to generate your database tables. This is what most people do.
Method 2: Create your database tables by hand and run symfony doctrine:build-schema to generate your schema. This is what I do.
Why do I generate my schema from my database instead of generating my database from my schema? There’s one big reason. Every time you run symfony doctrine:insert-sql, your entire database—meaning not just the database structure, but the data as well—gets wiped out. The database structure is put right back in place but if you want to keep your data intact, you’re on your own. Doing a mysqldump only works for the most trivial changes. Because the symfony developers are well aware that you’re going to want your data to stay intact, they let you use what are called fixtures—sets of data that you can easily load into your database—but it seems terribly inefficient to have to manually shuffle around your fixture data each time you make a change to your database structure. Plus, what if you like to periodically load your production data into your development database so you’re working with a realistic dataset? As far as I know, fixtures don’t accommodate that. Those are the main reasons why I use build-schema instead of build-sql. (From now on, I will call these two methods the build-from-database method and the build-from-schema method.)
Neither the build-from-database method or the build-from-schema method is perfect. (I just find build-from-database “less bad.”) I’ve come across the following problems with build-from-database:
- If any of my entities inherit from other entities (read about that here), the relationship gets wiped out every time I build the schema
- If I have SQL views in my database, each view shows up as a model, which doesn’t always make sense
- If I have, say, a WordPress install as part of my website, all my WordPress tables show up as models (I know I could work around this by putting the WP install in a separate database. The point is that every table in your database will get translated into a model, regardless of whether you want them to or not)
These are the main problems I’ve had with the build-from-database method. The most frustrating part is that I would have to manually correct my schema every single time I ran build-schema! It gets old fast, as I’m sure you know if you do build-from-database instead of build-from-schema.
How jsDoctrineSchemaOverriderPlugin Fixes The Problem
The brilliant part of symfony’s model design is the idea where there’s a base class and an inheriting class for each model. For example, if you have a table in your database called animal, symfony creates an Animal class as well as a BaseAnimal class. Then, when you re-run symfony doctrine:build-model, BaseAnimal is wiped out and rewritten but Animal—the class you’re allowed to customize—is left undisturbed. If you’re familiar with this idea, you’ll understand how jsDoctrineSchemaOverriderPlugin works.
The meat of jsDoctrineSchemaOverriderPlugin is in two classes: BaseSchema and Schema. Like symfony’s models, BaseSchema is auto-generated and Schema is there for you to override BaseSchema. BaseSchema is automatically built by jsDoctrineSchemaOverriderPlugin as a direct reflection of your database structure and Schema is where you can edit that structure. And get this: you don’t have to re-do all your work each time you rebuild the schema!
Installation
$ symfony plugin:install http://jasonswett.net/jsDoctrineSchemaOverriderPlugin-0.1.0.tgz
Examples
Removing an Entity
Let’s first take a trivial example: you have an view called my_view that you don’t want a model for. Edit your plugins/jsDoctrineSchemaOverriderPlugin/lib/Schema.class.php by adding a line to unset this view:
<?php class Schema extends BaseSchema { public function configure() { unset($this->entities['MyView']); } }
Now, instead of running symfony doctrine:build-schema, you’ll do something a little different. Run:
$ symfony schema-overrider:build-schema
You’ll want to always run schema-overrider:build-schema instead of doctrine:build-schema. This ensures that the overrides you make in Schema.class.php are reflected in your schema.yml.
That’s the simplest case: completely wiping out an entity. What if we want to edit an entity’s attributes?
Overriding Attributes
Let’s say we have two classes, Animal and Horse, with Horse inheriting from Animal. If you just run symfony doctrine:build-schema, symfony will have no idea just by looking at your database schema that Horse inherits from Animal. You can make sure symfony knows about this inheritance by adding the following to plugins/jsDoctrineSchemaOverriderPlugin/lib/Schema.class.php:
<?php class Schema extends BaseSchema { public function configure() { $this->entities['Horse'] = array( 'connection' => 'doctrine', 'tableName' => 'horse', 'inheritance' => array( 'extends' => 'Animal', 'type' => 'concrete', ), ); } }
Just like in the first example, we’ll run this command:
$ symfony schema-overrider:build-schema
Now Horse will always inherit from Animal and you won’t have to keep copying and pasting every time you run build-schema.
Please Help!
If you have any feedback of any kind about this plugin, please feel free to leave me a comment and let me know. I don’t know if I’m going about this the right way and I don’t know if my documentation is clear enough for other people to be able to use my plugin. All I know is that it works for me and it saves me a ton of work. Any feedback you have would be greatly appreciated!
Transactions with Doctrine
Friday, September 3rd, 2010If you’re going to make multiple related updates to your database, you should use a transaction. Here’s how to do a transaction with Doctrine. This is essentially lifted from the official Doctrine documentation but it’s a little more to-the-point than theirs. Plus my version doesn’t fail silently.
<?php $conn = Doctrine_Manager::connection(); try { $conn->beginTransaction(); $myObject->save(); $myOtherObject->save(); $conn->commit(); } catch(Exception $e) { $conn->rollback(); throw new Exception($e->getMessage()); }
How to validate and sanitize a URL in symfony
Friday, September 3rd, 2010This validator works for symfony 1.4 and is not necessarily backward compatible.
If you’d like to both validate and sanitize a URL in symfony, it’s pretty easy. First, put the following code in lib/myValidatorUrl.class.php:
<?php /** * myValidatorUrl validates and sanitizes a URL. * * @author Jason Swett (http://jasonswett.net/how-to-validate-and-sanitize-a-url-in-symfony) */ class myValidatorUrl extends sfValidatorUrl { protected function doClean($value) { $clean = (string) $value; // If the URL doesn't start with "http", add "http://". if (!preg_match('/https?:\/\/.+/', $clean)) { $clean = 'http://'.$clean; } // Add a trailing slash if the URL doesn't have one. if (!preg_match('/https?:\/\/.+\//', $clean)) { $clean .= '/'; } // If the URL still isn't valid after that, it probably wasn't close enough to begin with. if (!preg_match($this->generateRegex(), $clean)) { throw new sfValidatorError($this, 'invalid', array('value' => $value)); } return $clean; } }
Then, in the form where you have your URL field, add the following line to your configure() method:
$this->validatorSchema['url'] = new myValidatorUrl(array('required' => false));
That’s all! Now, if someone enters a URL like “example.com”, it will get saved as “http://example.com/”.
How to validate and sanitize a phone number in symfony
Monday, May 31st, 2010This validator works for symfony 1.4 and is not necessarily backward compatible.
If you’d like to both validate and sanitize a phone number in symfony, it’s pretty easy. First, put the following code in lib/myValidatorPhone.class.php:
<?php /** * myValidatorPhone validates a phone number. * * @author Jason Swett (http://jasonswett.net/how-to-validate-and-sanitize-a-phone-number-in-symfony/) */ class myValidatorPhone extends sfValidatorBase { protected function doClean($value) { $clean = (string) $value; $phone_number_pattern = '/^(^(1\s*[-\/\.]?)?(\((\d{3})\)|(\d{3}))\s*[-\/\.]?\s*(\d{3})\s*[-\/\.]?\s*(\d{4})\s*(([xX]|[eE][xX][tT])\.?\s*(\d+))*$)*$/'; if (!$clean && $this->options['required']) { throw new sfValidatorError($this, 'required'); } // If the value isn't a phone number, throw an error. if (!preg_match($phone_number_pattern, $clean)) { throw new sfValidatorError($this, 'invalid', array('value' => $value)); } // Take out anything that's not a number. $clean = preg_replace('/[^0-9]/', '', $clean); // Split the phone number into its three parts. $first_part = substr($clean, 0, 3); $second_part = substr($clean, 3, 3); $third_part = substr($clean, 6, 4); // Format the phone number. $clean = '('.$first_part.') '.$second_part.'-'.$third_part; return $clean; } }
Then, in the form where you have your phone number field, add the following line to your configure() method:
$this->validatorSchema['phone'] = new myValidatorPhone(array('required' => false));
That’s all! Now, if someone enters a number like 123.456.7890, it will get saved as (123) 456-7890.
Changing your title in symfony
Friday, May 28th, 2010The following is for symfony 1.4 and is not necessarily backward compatible.
If you just want to completely change your title in symfony and you’re okay with wiping out whatever’s already there, it’s simple. Just use this in your action:
$this->getResponse()->setTitle('My New Title');
But what if you want to keep the first part of your app’s title and only change the rest? That’s also pretty easy. In apps/frontend/templates/layout.php, just change this
<?php include_title() ?>
to this:
<title>The Static Part of Your Title <?php echo $sf_response->getTitle() ?></title>
Now when you do this in your action
$this->getResponse()->setTitle('My New Title');
The “The Static Part of Your Title” will still be there and only the rest of it will have changed.
Inheritance with symfony and Doctrine ORM
Monday, February 1st, 2010The Problem
Doctrine ORM claims to support some kind of inheritance but I have yet to see a good example. The inheritance documentation on the Doctrine ORM site could be worse but it certainly has a lot of room for improvement. How about an example with some actual data? The symfony documentation for Doctrine inheritance lays things out a little more clearly than the Doctrine docs but it still wants for good examples.
The Solution
Here I intend to perform a hearty exploration of Doctrine inheritance with the hope that I can show, by example, what it can and can’t do. The inheritance model I will use will be simple: the base class will be called FarmAnimal and the three inheriting classes will be Cow, Dog and Chicken. All FarmAnimals will have at least a name, a sound and a number of legs. Each type of FarmAnimal will have the following unique properties: Cow will have a use (beef or dairy), a Dog will have a breed and a Chicken will have an egg color.
If you’d like to follow along, I’ve put together some setup instructions for this project.
Doctrine offers three styles of inheritance: Simple, Concrete and Column Aggregation. The symfony docs give the following definitions:
| Name | Description |
|---|---|
| Concrete | Each child class has a separate table has all the columns of its parents |
| Simple | Each child class shares the same table and columns as its parents |
| Column Aggregation | All columns must be defined in the parent and each child class is determined by a type column |
Simple Inheritance
The Doctrine documentation shows an example where we have a User class and a Group class. Both of these classes inherit from Entity and neither one has any unique properties. When the example schema is evaluated, Doctrine only spits out one table: entity. If that’s all that “simple inheritance” is good for, I don’t really see how that’s inheritance. Any object you might instantiate, whether it be a User, Group or plain Entity, is an Entity, nothing more and nothing less. What’s gained there?
Let’s try something similar to their example but go a little further with it.
config/doctrine/schema.yml
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: simple
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: simple
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: simple
columns:
egg_color:
type: enum
values: [brown, white]Since we’re using enums, we’ll have to update config/databases.yml and add the use_native_enum directive:
all:
doctrine:
class: sfDoctrineDatabase
param:
dsn: 'mysql:host=localhost;dbname=inheritance'
username: inheritance
password: pass123
attributes:
use_native_enum: trueNow let’s build the model, build the SQL and insert the SQL:
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
What does this give us in the database?
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | farm_animal | +-----------------------+ 1 ROW IN SET (0.00 sec) mysql> DESCRIBE farm_animal; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | | breed | VARCHAR(20) | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 9 ROWS IN SET (0.01 sec)
As you can clearly see, all the inheriting classes’ properties have been rolled into one table along with the base class’s properties. This is exactly what the Doctrine doc tells us, but it doesn’t totally spell it out with an example.
The table above is a bad model, both from a purist and pragmatic perspective. A purist might say, “We have three distinctly different types of things but only one data structure! Are we expected to deduce, based on which values are NULL and which are not, which type of FarmAnimal we’re dealing with? What if we add the idea of a Cat with a breed? How do we tell it apart from a Dog?” And a pragmatist might say, “What if we have 20 different types of FarmAnimals, each having 5 different properties? That’s at least 100 columns in the FarmAnimal table!” I would agree with both the pragmatist and the purist. Imagine looking at a table with 100 columns, most of the rows having NULL in most of the columns most of the time. That’s an extreme example, but the principle is the same on any scale.
If you look at the PHP classes created by symfony, they’re meaningless. Take a look at lib/model/doctrine/base/BaseChicken.class.php, for example:
/** * BaseChicken * * This class has been auto-generated by the Doctrine ORM Framework * * * @package INHERITANCE * @subpackage model * @author Your name here * @version SVN: $Id: Builder.php 6820 2009-11-30 17:27:49Z jwage $ */ abstract class BaseChicken extends FarmAnimal { public function setUp() { parent::setUp(); } }
There’s nothing unique to Chicken there. All the properties of all the FarmAnimals are kept in lib/model/doctrine/base/BaseFarmAnimal.class.php:
abstract class BaseFarmAnimal extends sfDoctrineRecord { public function setTableDefinition() { $this->setTableName('farm_animal'); $this->hasColumn('name', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('sound', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('leg_count', 'integer', null, array( 'type' => 'integer', )); $this->hasColumn('created_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('updated_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('purpose', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'beef', 1 => 'dairy', ), )); $this->hasColumn('breed', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('egg_color', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'brown', 1 => 'white', ), )); } public function setUp() { parent::setUp(); } }
I’ve seen enough of “simple inheritance” to know that I would never use it. Let’s see what “concrete inheritance” is all about.
Concrete Inheritance
Apparently all that’s needed to switch from simple inheritance to concrete inheritance is to replace all instances of type: simple with type: concrete:
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: concrete
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: concrete
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: concrete
columns:
egg_color:
type: enum
values: [brown, white]Let’s run our symfony commands again:
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
What’s in the database now?
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | chicken | | cow | | dog | | farm_animal | +-----------------------+ 4 ROWS IN SET (0.00 sec)
That’s encouraging: each class has its own separate table.
mysql> DESCRIBE farm_animal; +------------+-------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+-------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | +------------+-------------+------+-----+---------+----------------+ 6 ROWS IN SET (0.00 sec) mysql> DESCRIBE cow; +------------+----------------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+----------------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | +------------+----------------------+------+-----+---------+----------------+ 7 ROWS IN SET (0.00 sec) mysql> DESCRIBE dog; +------------+-------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+-------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | breed | VARCHAR(20) | YES | | NULL | | +------------+-------------+------+-----+---------+----------------+ 7 ROWS IN SET (0.01 sec) mysql> DESCRIBE chicken; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 7 ROWS IN SET (0.00 sec)
That looks like what I want. Each entity is represented by a table that has a column for each of its properties, nothing more and nothing less. The only way this might be improved, in my opinion, is if we could tell Doctrine that FarmAnimal is an abstract class and we don’t want a table for it.
If we look at the symfony-generated classes, they look how we’d expect:
abstract class BaseChicken extends FarmAnimal { public function setTableDefinition() { parent::setTableDefinition(); $this->setTableName('chicken'); $this->hasColumn('egg_color', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'brown', 1 => 'white', ), )); } public function setUp() { parent::setUp(); } }
abstract class BaseFarmAnimal extends sfDoctrineRecord { public function setTableDefinition() { $this->setTableName('farm_animal'); $this->hasColumn('name', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('sound', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('leg_count', 'integer', null, array( 'type' => 'integer', )); $this->hasColumn('created_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('updated_at', 'timestamp', null, array( 'type' => 'timestamp', )); } public function setUp() { parent::setUp(); } }
Doctrine’s “concrete inheritance” looks pretty solid. Let’s take a look at the third and final style, “column aggregation.”
Column Aggregation
To be honest, I’m pretty baffled as to what this one is supposed to do for us. Hopefully this example will make things clearer.
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 1
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 2
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 3
columns:
egg_color:
type: enum
values: [brown, white]$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | farm_animal | +-----------------------+ 1 ROW IN SET (0.00 sec) mysql> DESCRIBE farm_animal; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | BIGINT(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | VARCHAR(20) | YES | | NULL | | | sound | VARCHAR(20) | YES | | NULL | | | leg_count | BIGINT(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | TYPE | VARCHAR(255) | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | | breed | VARCHAR(20) | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 10 ROWS IN SET (0.01 sec)
One table again, just like simple inheritance. Since I already rejected this structure, I see no reason to continue with column aggregation.
Conclusion
In my opinion, Doctrine’s simple inheritance and column aggregation are invalid and concrete is the only way to go. I hope these examples cleared up some confusion for anyone who had as much trouble with these concepts as I did.