Solving the Doctrine Parenthesis Problem
by scott on Sep.23, 2009, under doctrine, php
I really like Doctrine. It’s important to start my posts with this comment because many of my posts are about it’s limitations and how to overcome them, not about it’s nice features. One of the worst limitations is the inability to wrap complex WHERE statements in parenthesis. DQL relies almost entirely on the precedence of AND and OR. This is great for simple stuff, but it falls apart pretty quickly if your where statement is complex.
Even something as simple as “(ConditionA OR ConditionB) AND ConditionC” can be difficult. In DQL,
$query
->where('ConditionA')
->orWhere('ConditionB')
->andWhere('ConditionC')
will result in “ConditionA OR ConditionB AND ConditionC”. AND takes precedence over OR and the result is “ConditionA OR (ConditionB AND ConditionC)”. That is NOT what we wanted.
The solutions I’ve seen posted suggest wrapping the entire phrase in a single DQL where statement. For example,
$query
->where("(ConditionA OR ConditionB) AND ConditionC");
This works sometimes. It works until the level of complexity of the DQL statement makes Doctrine’s parser spit up blood. I can’t tell you exactly when that will happen, but I’ve encountered it enough times that it had become a serious issue.
My ultimate solution (pay attention, this is the magic part) was to figure out how to inject specific parenthesis into the DQL stream. I looked through the Doctrine source code (I’m using the version distributed with Symfony 1.2) and tried to find a good hook. This was probably my third time through the entire DQL parsing engine so I was starting to feel at home.
I had already created a subclass of Doctrine_Query called DQ (my Dairy Queen class). It has a couple of handy methods in it like andWhereIf() and orWhereIf() that made my life easier. To fix the parenthesis problem, I added another method called whereParamWrap().
To use the DQ class, simply substitute DQ for Doctrine_Query when creating a query. For example,
DQ::create()
->from('ImportantTable i')
->where('i.field = ?', 1)
->andWhereIf($filter);
In this case, the andWhereIf($filter) will be ignored if $filter is empty().
To use the parenthesis magic, you’d create a query like this
DQ::create()
->from('ImportantTable i')
->where('i.field = 1')
->orWhere('i.otherfield = 2')
->whereParenWrap()
->andWhere('i.finalfield = 3');
whereParenWrap() will wrap parenthesis around all of the where conditions collected so far. Additional where conditions will be outside the parens. The result is (i.field = 1 OR i.otherfield = 2) AND i.finalfield = 3. The DQL elite would now point out that I could have simply built that condition into the initial where clause and saved myself the trouble of spending a day figuring this paren thing out. In reply, I would say to try this as one statement:
DQ::create()
->from('Foo f')
->leftJoin('f.FooData d')
->leftJoin('f.UserA s')
->leftJoin('f.UserB p')
->leftJoin('f.Group g')
->where("f.group_id IN (SELECT guA.group_id as group_id FROM GroupHasUser guA WHERE guA.user_id = $uid AND guA.status in ($members))")
->andWhere("f.group_id IN (SELECT guB.group_id as group_id FROM GroupHasUser guB WHERE guB.user_id = $cid AND guB.status in ($members))")
->orWhere("f.primary_user_id = $uid AND g.view_feeds = " . Group::EVERYONE)
->orWhere("f.primary_user_id = $uid AND f.secondary_user_id = $cid")
->orWhere("f.primary_user_id = $cid AND f.secondary_user_id = $uid")
->whereParenWrap()
->andWhere('f.type in ?', $types);
The whereParenWrap is particularly useful if you have to pass the query around to have additional conditions added to it. You can wrap all of the initial statements in parenthesis so they are treated autonomously. For example,
function getQuery() {
$query = DQ::create()
->from('ImportantTable i')
->where('i.field = 1')
->orWhere('i.otherfield = 2')
->whereParenWrap();
return $query;
}
$query = getQuery();
$query->andWhere(’i.importantfield = 3′);
Here’s the class. Use it at your own risk. It worked for me, but no promises.
/*
* This is a simple short-hand wrapper for Doctrine_Query. It provides
* a shorter class name and a few additional functions.
*/
class DQ extends Doctrine_Query
{
/**
* Returns a DQ object to get started
*
* @return DQ
*/
public static function create($conn = null) {
return new DQ($conn);
}
/**
* This function will wrap the current dql where statement
* in parenthesis. This allows more complex dql statements
* It can be called multiple times during the creation of the dql
* where clause.
*
* @return $this
*/
public function whereParenWrap() {
$where = $this->_dqlParts['where'];
if (count($where) > 0) {
array_unshift($where, '(');
array_push($where, ')');
$this->_dqlParts['where'] = $where;
}
return $this;
}
/**
* Create and andWhere if the where parameter is not empty
*
* @param string $where where string
* @param parameters $params
*
* @return DQ this object
*/
public function andWhereIf($where, $params = array()) {
return empty($where)
? $this
: $this->andWhere($where, $params);
}
/**
* Create and orWhere if the where parameter is not empty
*
* @param string $where where string
* @param parameters $params
*
* @return DQ this object
*/
public function orWhereIf($where, $params = array()) {
return empty($where)
? $this
: $this->orWhere($where, $params);
}
}
Measuring Pixel Size of String in PHP
by scott on Sep.10, 2009, under php
I frequently find myself needing to approximate the pixel width of strings on the server side in PHP. The function below does that nicely. It WILL NOT return the exact number of pixels for all font in all cases, but the approximation can be used to switch CSS classes. I’ll do a measurement and then wrap a code block with a class of “strink” if I think the string will be too long for the display.
You can do a better job in Javascript, but this is good enough for most cases.
/**
* This function uses the array below to calculate the pixel width of a string
* of characters. The widths of each character are based on a 12px Helvetica font.
* Kerning is not taken into account so RESULTS ARE APPROXIMATE.
*
* The purpose is to return a relative size to help in formatting. For example,
*
* strPixels('I like cake') == 54
* strPixels('I LIKE CAKE') == 67
*
* @param string $string characters to measure size
*
* @return integer size in pixels.
*/
public static $strPixelWidths = array(
' ' => 3, '!' => 3, '"' => 4, '#' => 7, '$' => 7, '%' => 11, '&' => 8, ''' => 2, '(' => 4, ')' => 4, '*' => 5, '+' => 7, ',' => 3, '-' => 4,
'.' => 3, '/' => 3, '0' => 7, '1' => 7, '2' => 7, '3' => 7, '4' => 7, '5' => 7, '6' => 7, '7' => 7, '8' => 7, '9' => 7, ':' => 3, ';' => 3,
'<' => 7, '=' => 7, '>' => 7, '?' => 7, '@' => 12, 'A' => 7, 'B' => 8, 'C' => 9, 'D' => 9, 'E' => 8, 'F' => 7, 'G' => 9, 'H' => 9, 'I' => 3,
'J' => 6, 'K' => 8, 'L' => 7, 'M' => 9, 'N' => 9, 'O' => 9, 'P' => 8, 'Q' => 9, 'R' => 9, 'S' => 8, 'T' => 7, 'U' => 9, 'V' => 7, 'W' => 11,
'X' => 7, 'Y' => 7, 'Z' => 7, '[' => 3, '\' => 3, ']' => 3, '^' => 5, '_' => 7, '`' => 4, 'a' => 7, 'b' => 7, 'c' => 6, 'd' => 7, 'e' => 7,
'f' => 3, 'g' => 7, 'h' => 7, 'i' => 3, 'j' => 3, 'k' => 6, 'l' => 3, 'm' => 11, 'n' => 7, 'o' => 7, 'p' => 7, 'q' => 7, 'r' => 4, 's' => 7,
't' => 3, 'u' => 7, 'v' => 5, 'w' => 9, 'x' => 5, 'y' => 5, 'z' => 5, '{' => 4, '|' => 3, '}' => 4, '~' => 7);
public static function strPixels($string) {
$weight = 0;
if (!empty($string)) {
for ($i = 0; $i < strlen($string); $i++) {
$w = @self::$strPixelWidths[substr($string, $i, 1)];
if ($w) $weight += $w;
}
}
return $weight;
}
Where’s My Doctrine Object?
by scott on Jun.23, 2009, under doctrine, symfony
The Doctrine ORM does a nice job of handling object oriented data, but has a number of flaws when it comes to finding the objects again. Every object it creates is stored in the Table’s repository, but they are not indexed in any meaningful way. It’s pretty much up to you to keep track of your objects once you create them.
What you’d like to do is check to see if a particular object has already been instantiated and not do it again. Recreating the same object multiple times is costly in terms of CPU time even if you use SQL caching.
When it comes to keeping objects around, Doctine is kind of half-pregnant. They are definately there and Doctrine does a good job of making sure you never have duplicate objects floating around that represent the same data in the database. However, finding them is another story.
For example, if you want to see if the User with ID=4 has an object floating around in memory, you can do this:
$repository = Doctrine::getTable(’User’)->getRepository();
foreach ($repository as $u) {
if ($u->getId() == 4) {
do something
}
}
This is great, but imagine if the you have a lot of User objects floating around. This is a very slow way of finding one of them.
What we’d really like is for the repository to be indexed in some meaningful way (or ways), but it’s indexed by a sequential number that’s incremented each time a Doctrine_Record is created. You can get this reasonably useless number by calling $obj->getOID().
If you’ve read a group of objects using the foriegn key or local key relationships of an object, there is another way of finding your objects. All of the references that have been read are stored in an array of reference Doctrine_Collections. To see all of the references for a user object, you can do this:
foreach ($user->getReferences() as $reference)
foreach ($reference as $obj)
(do something with $obj)
You can also get a single reference array using
$collection = $user->reference(’PhoneNumbers’)
where PhoneNumbers is the name of an object set referenced by $user.
These are some basic functions you might find useful. I’ll post later on some of the deficiencies I’ve found in Doctrine relating to handling object associations where the many-to-many is MANY-to-MANY. Doctine is too eager to load EVERYTHING into memory which is find for a few objects, but not so good when it ends up being 10,000.
Installing PAMPA for PHP Development in Windows
by scott on May.01, 2009, under PAMPA, symfony
I’ve had a ongoing problem with PHP crashing under Symfony and Doctrine in a number of environments. The crash is random and appears and disappears like acne on the face of 16-year-old boy. The best solution I’ve found is upgrading to PHP 5.3. It seems to be faster and more stable for complex OO designs than any of the 5.2 variants.
Compiling and installing PHP 5.3 on unix had it’s own set of challenges, but it was, by and large, straightforward. Once you’ve got the configuration right, it seems to compile without any major headaches.
Getting PHP 5.3 to work under windows in another story altogether. I’d given up on XAMPP and switched to WAMPSERVER with PHP 5.2.8. It installs fairly cleanly and has a nicer interface than XAMPP. However, when I went to install PHP 5.3.0RC1 under WAMPSERVER, the walls of Jerico came tumbling down. What a nightmare. My partner burned a day trying to get it work and I burned another. No love. It simply crashed Apache on startup.
There are a bazillion Wamp installations out there (see http://en.wikipedia.org/wiki/Comparison_of_WAMPs). Looking through the list, I found one called PAMPA that already has 5.3.0RC1 installed. Glory be!
Doing an initial install of PAMPA is amazingly easy. You simply unzip it and run the EXE. All of the directories are relative and it fires up like a champ. The problem is that PAMPA is designed primarily for CD/DVD installations so it shuts off anything that might conflict and runs on odd ports.
To get it to behave like a more standard installation, follow the instructions below.
Change the following in the pampa.ini file:
- Enable InnoDB if you plan to use it.
- Change the Listen=85 to Listen=80.
- Change the port=3307 to port=3306
Change this stuff in php.ini:
- Find mysqli.default_port = 3307 and change it to 3306 (the mysql port again)
- Uncomment extension=php_pdo_mysql.dll if you are using PDO
- DO NOT uncomment php_pdo.dll. PDO is build into 5.3.0. There is no external DLL.
Change this in my.ini:
- Change 3307 to 3306 (yet again)
In the depth of PhpMyAdmin:
- Find pampa\PAMPA\apache.32\htdocs\phpMyAdmin\libraries\config.default.php
- Change 3307 to 3306 (yep, again. finding this one took me hours)
To create a virtual host do this:
- Add a hosts file entry (e.g. 127.0.0.1 localhost.mywebsite)
- Add the stuff below to the top of httpd.conf
NameVirtualHost *
<VirtualHost *>
Servername localhost
</VirtualHost><VirtualHost *>
<Directory “location/of/files/for/web”>
AllowOverride All
Options Indexes
Order deny,allow
</Directory>
ServerAdmin an@email.address
DocumentRoot “location/of/files/for/web”
ServerName localhost.mywebsite
ServerAlias localhost.mywebsite
ErrorLog logs/localhost.mywebsite.log
CustomLog logs/localhost.mywebsite common
AddType application/x-httpd-php .php
</VirtualHost>
If you have already run PAMPA and make the changes, stop it and then run the task manager and verify that mysql really stopped. Often, it’s still running and may cause a conflict when you start PAMPA again. Kill it with the task manager or reboot if its running.
Changes to configuration that don’t involve MySql you can initiate by simply clicking on the P icon in the system tray and hitting restart.
Note that PAMPA does NOT install a CLI version of PHP. You’ll have to install PHP yourself if you use it from the command line (like I do).
That should do it. The nice thing about PAMPA is that you can zip it up and upload it somewhere and someone else can use the configuration by simply unzipping it and running the pampa.exe file. It’s pretty cool.
Why Doesn’t My Form Default Work?
by scott on Feb.21, 2009, under symfony
Here’s a little tip for symfony forms. It’s a bug really. Cost me around three hours.
Forms are closely tied to the database model (like it or not). Their default values are bound to the Doctrine model class specified in the getModelName() call. For example, if you specify a default value for STATUS in your schema and then have a form element called STATUS, the form processor will bind the default value from your schema to your form element and IGNORE the default that you set in the form element itself.
For example, if you have the following widget specified in your form
’status’ => new sfWidgetFormSelectRadio(array(’choices’ => self::$choices, ‘default’ => self::DEFAULT_CHOICE))
you’ll might be surprised to find that the default you picked for the radio group is not lit up as it should be. If that’s the case, its probably because your database schema specified a default for STATUS. Using the default from the model is a good thing, but a programmatically specified default should have precedence. It’s a bug, in my opinion.
The solution is to set the default values in your schema to null (or none) which causes symfony to properly use the programmatically set default.
Impressions of the Symfony Framework
by scott on Feb.20, 2009, under symfony
I am using the Symfony framework for my second project major now and now consider myself proficient, if not an expert. The first project was for iChange.com and the guys on my team had enough experience that I leaned on them for the esoterica that is not included in the ‘book’. On my current project (LittleYell.com), I am working with a Symfony neophite and thus largely flying solo when it comes to figuring out the hard stuff.
For those of you in the PHP world that have never used a framework, it’s time to start. It’s not appropriate for very small projects, but as soon as you are developing a real site, it’s pointless to try to invent all of the programmatic patterns you’ll find already completed in an existing framework. By all means, check out CakePHP, Zend, CodeIgniter and all of the bazillion’s of others (http://www.phpframeworks.com/), but I recommend you use one of them rather than write yet another one of your own.
Symfony was an easy choice for my first project because I had two engineers working for me that were experienced with the framework. I talked to lots of folks (including friends at Yahoo) who were using it and did a little diligence and liked what I saw. Reading the documentation on the site was also a compelling factor in my choice. It is very well written with clear examples of how to use the framework’s elements.
When you get deeper into Symfony, you’ll find that while the ‘book’ is very clear, you’ll find it very difficult as soon as the problem you are trying to solve is not included there. The API documentation is very hard to navigate and search and very thin. Most of the time, when I’m solving a hard problem, I start by searching the web for examples, and then start looking through the Symfony code itself looking for an answer.
I would describe the Symfony code as very well written and pretty poorly documented. Had my partner Jeff and I written it, there would have been less reliance on option arrays and better leveraging of the PHP class architecture. Too many functions and classes rely on magic methods that obfuscate the underlying processes. I described it once as looking at PHP assembly language.
You can get a simple web site going with Symfony pretty quickly, but don’t expect to write a full featured, high production, website without going through the steep learning curve. Symfony is trying to solve a lot of disparate problems and thus, it is very complex.
The first project (at iChange.com) was done using Propel which was the only stable choice for a Symfony ORM at the time. Propel works great, but it has two flaws. The first is that it is VERY difficult to subclass the classes in the model. If you have a ‘user’ table with a ‘User’ class in the model and want to create a UserGuest subclass, you’ll have to go through a lot of gyrations to make it happen. The separation of Propel’s data and factory classes is at the heart of this problem.
The second problem with Propel is that complex queries are VERY hard to write. A query that takes a few minutes to test in PHPMyAdmin make take hours to code using Propel. When you get it done, it will be hard to read and understand as well.
Doctrine, which we are using on our LittleYell.com project, mostly solves these problems. You can specify subclasses of model classes when making queries and Doctrine will happily hydrate into your subclass. The DQL syntax at the heart of Doctrine is fairly easy to read and creating fairly complex queries is certainly easier than in Propel.
Doctrine is still somewhere between the leading edge and the bleeding edge as far as stability, but I’m guessing that over the next year it will be the superior ORM. I’ve had some scary problems hydrating into objects using complex queries that have crashed PHP and Apache. Doctrine really wants you to hydrate into arrays which is efficient, but not very good OO practice. I solved the problem by creating my own lightweight container objects to hold data.
I still have one query that is crashing PHP that gives me some concern. I’ve worked around it, but I’m full of anxiety waiting for it to occur somewhere else where just the right conditions for the bug to appear. I’ve done some searches on Symfony Doctrine crashes and found enough posts to make me think I’m not the only one having this problem.
An interesting note is that PHP programmers like to believe that any crash of the PHP system is the fault of the xAMP stack. Not so. It’s fairly easy to write a PHP program to crash PHP.EXE (try function x() { x(); }, for example). If you have two software programs and one crashes PHP and the other does not, folks will use the one that doesn’t.
We aren’t far enough along for me to say whether Doctrine or Propel is the better choice. We are betting on the future and better structure and picked Doctrine. You might pick differently.
In conclusion, if you are doing a PHP project that will occupy you for a few months rather than a few days, use a framework. Despite the flaws mentioned above, I like Symfony and am glad I chose to use it. Over the next few days, I’ll try to post some solutions to some of the thornier problems I’ve faced.