Not logged in
Login - Register
Songs Artists Compos Albums Help Forums

remixsquared - A Case Study in symfony Development

Introduction

symfony is one of the best environments available for Web application development. With its use of well-tested third-party components, its emphasis on avoiding "reinventing the wheel," its adherence to standard design principles, and its enforcement of common coding standards, the framework is the best I've used for creating interactive websites. Unlike other frameworks like Ruby or Django, which build upon lesser-known languages, symfony's foundation of the PHP language provides project coordinators with a huge talent pool should the need arise to hire additional developers. PHP is standard on almost all commercial web hosts today, whereas ASP or ASP .Net often require additional charges. Furthermore, the automatic generation of data objects by Propel eliminates the possibility of SQL injection and many other common security concerns if utilized fully. To make the most out of symfony, however, developers should be aware of some common pitfalls that can hinder even the most promising projects. This article reviews these potential problems that should be avoided as often as possible.

During the past year, I've spent about 1,000 hours developing this website, remixsquared, a community devoted to sharing video game music and providing exposure to game soundtrack artists. remixsquared is the first project I developed using symfony. Instead of spending months investigating the project's "sandbox" applications or developing test sites, I decided that the best way to learn is to simply dive in. The most significant problem I discovered with symfony is that there are actually too many ways to accomplish the same results. To make matters worse, the project's documentation is inconsistent on which methods to use, especially confusing beginners who may not even understand that different approaches are possible or how they are related to each other.

Pick one method and stick to it

One example of symfony's "too many ways" problem involves the ability to include HTTP "meta" information in pages. The "title" meta tag, for example, controls the text that is displayed in the title bar of the user's web browser when viewing a page. The symfony documentation provides several methods to set the tag. The "view.yml" configuration file for a particular module can be modified to declare the title of a page, but it is also possible to set the title by calling the Response object's addMeta('title', "whatever"); function. This "addMeta" function can further be called either in the action or in the template itself. Of course, it is also possible to simply manually "echo" the title using HTML and PHP in the template without accessing the "Response" object or any other symfony methods whatsoever.

In remixsquared, I ended up with a massive refactoring job in declaring page titles and other meta information because it is difficult or impossible to add local variables to these tags when declared in the "*.yml" configuration files. To avoid this problem, and to eliminate confusion with other developers who might later search your code, I recommend placing title and other meta tags in the template files themselves, but still using the "addMeta" function. The practice makes logical sense because meta tags are normally declared in the ": section of an HTML file, which is placed at the top of the page. Placement in the template files, but still using the symfony response object, locates the code in the most logical place for developers and graphic designers to look, while still allowing symfony to remove duplicate tags and to perform its internal checks.

YAML: a poor choice?

symfony lets developers to choose between two markup language formats: XML and YAML. While XML is well-known and widely used, YAML is a newer language that was created to reduce the amount of text in XML files as well as to improve readability. In a YAML file, properties are often declared by simple indentation, rather than creating entirely new opening and closing tags. Therefore, spaces and tabs are of prime importance in YAML; yet they are ignored in the XML format. In my experience, the disadvantages of precise formatting of spaces and tabs significantly outweigh the overhead associated with XML coding. An entire YAML file can be corrupted by simply omitting one or two space characters, and these issues are nearly impossible to spot when a lack of indentation is the cause. To complicate matters, text editors vary widely in their handling of spaces and tabs, so that a file that previously worked great can be "corrupted" when opened with an text editor that handles these characters differently. For example, in some editors, pressing the [TAB] key actually inserts a specified number of space charaters, while in other editors, an actual [TAB] character is inserted.

In addition, while symfony's documentation covers YAML in detail, it does not emphasize that YAML is actually converted to XML in most cases before being processed. It was not clear to me initially that some features available through XML are not supported in YAML. Foreign keys are nearly impossible to implement correctly using YAML and Propel, for example, and I gave up attempting to do so after 10 hours of trying. remixsquared still has a mess of inconsistent code where some modules utilize YAML and some use XML, and it will take hours to resolve this issue if it is ever deemed worthwhile to do so.

Forms and file uploads: needing improvement

symfony's forms interface is by far the weakest part of its interface. As remixsquared is an AJAX application at its core, I had little use for much of symfony's standard forms. The forms were poorly designed in several ways, the most noteworthy of which is that widgets and validators must be defined separately. If a "date" type input is placed on a form, it is also necessary for the user to add a "date" validator to the field, when an intelligent framework should remove this second step. Furthermore, should the programmer decide that the user need not input one field, and therefore omits it from the HTML form, no error message will be displayed at all. I lost 50 hours attempting to figure out why a form would appear to submit correctly but reload the page before I determined that I had forgotten to "unset" the default validator on an programmatically-set field. If you use symfony's forms interface, always check each field to determine if it is present on the form and remove the validators for those fields that are not present.

When it comes to file uploads, however, symfony's built-in interface is certainly better than any of the Flash-based uploaders out there. Since remixsquared's lossless songs can be up to 100MB in size, I wanted to enable asynchronous file transfers early in the site's history. However, I had to try four different Flash-based upload solutions before I finally reverted to symfony's default upload system. Now, as you can see by visiting the site, I got lucky and was finally able to get a third-party Flash system working relatively smoothly, after hundreds of hours of effort on this one issue alone. Clearly, the benefits of a Flash-based upload system are outweighed by the hassle involved in handling many different browsers, operating systems, Flash versions, Javascript implementations, and the like.

Don't let the MVC paradigm control you

symfony builds upon the model-view-controller (MVC) design pattern, with the intention that graphic artists and web designers never need to know what's going on behind the scenes. While this sounds great in practice, every project in which I've ever been a part has found a way to violate these principles, and symfony allows violation easily. Look at the userboxes that are present everywhere on remixsquared, for example. Each box contains the user's picture, with his or her name below it linked to the person's profile page. This

box requires two lines of output. Since these boxes are obviously design elements, the convention of Fabien, symfony's core developer, would likely have developers create a "partial" to output these lines. However, by this definition, many other elements on the page would also require partials, down to the point where dynamic links that appear on many pages, such as the links to the Video Game Music Database, need to be separated out to partials. With partials required to be stored as separate files in each module, hundreds or thousands of these small partials could emerge, and finding the exact module where the required one is located would be a nightmare.

I resolved this issue by actually including the call in the model class. A simple ->getUsernameBox() function is extremely readable, and includes just two lines of code in the function itself. The name of the function clearly indicates what it does. While small functions like these may violate the MVC paradigm, I found that the significant increase in readability of the template code more than compensates for the shortfall. For more complicated outputs, or those that are associated with more than one model, I still use partials, of course. Sometimes programmers have a habit of creating complicated things for an unrealistic amount of flexibility. But why make things overly complicated when they can be exceedingly simple?

On the topic of the model classes, there is a great deal of debate within the symfony community as to whether Propel or Doctrine is better for the database abstraction layer. While more symfony code seems to be available for Propel, Doctrine is more advanced as a standalone product. I selected Propel for the remixsquared project, and reluctantly started writing queries using the hard-to-understand "Criteria" classes. It's difficult in symfony in general to find the actual queries being sent to the database, especially in AJAX requests where the debug interface cannot be present. Later, however, I discovered the "DbFinder" plugin, which builds queries in a more intuitive manner. For example, instead of creating queries with ten or more "$c->add(SongPeer::SONG_ID, $songid);" and other unusual statements, DbFinder allows the user to write more intuitive commands like "$finder->where('Song.SongId', $test);." The DbFinder plugin automatically parses column names and input variables to prevent SQL injection. It also provides the ability to skip the costly process of object hydration, returning an array of values instead for those instances where access to only a certain subset of raw values is necessary. Initially, I used Propel Criteria where its limited functionality was sufficient, and dangerous raw queries where Propel was inadequate, but DbFinder can be used for almost all queries while still preserving the advantages of both of the aforementioned types. But if you're expecting DbFinder to provide you with the actual queries generated by the PHP code, don't get your hopes up - the feature to get the last query is unreliable and buggy.

Propel: unintuitive queries, but fast

I am surprised by the speed at which Propel executes its queries. Initially, I believed that a significant amount of overhead would be incurred by the numerous database connections required to instantiate Propel model objects. The front page of remixsquared, at the time of this writing, issues 162 queries to the database per pageview; yet, its performance is exceptional. The 162 queries have actually been programmed on purpose because the previous version of the page, which issued only 5 widely joined queries, took over 10 seconds to load in many cases. There are obviously many optimizations in Propel and symfony which I do not fully understand that are occurring behind the scenes. While it may sound counterintuitive, I've found the best performance often comes by simply using the model objects and the DbFinder plugin as if the database doesn't exist. For example, instantiating 10 Song objects and then calling "->getReviews()" on each song in turn seems to be much more efficient than writing a lengthy query that joins between all tables at once. Even though "->getReviews()" results in a database query, calling it is faster than joining to the "Review" table along with the songs initially.

I gave up with attempting to maintain database independence early in the project. Today's databases contain much more than basic SQL can accomplish, so why not take advantage of those features? Some features, like MySQL's "MATCH" keyword, are not available in standard SQL, and other keywords differ between PL-SQL and T-SQL. Developers who attempt to maintain database independence on a complicated project will frequently find themselves writing complex PHP code to accomplish what can be easily done on the database end. Furthermore, I have never been sold on why maintaining database independence is important. If MySQL can scale to handle one of the busiest databases in the world - the English Wikipedia - then it can certainly compete against the established names in the industry. And if a database change is intended for the future because company standards mandate a different database now, then the company should take a hard look at how its procedures are impacting the quality of new products. In short, Propel and Doctrine are great for the other features they provide, but the creators of symfony make it sounds as if achieving database independence requires no additional work by the programmer.

Plugins: not all they're cracked up to be

symfony's plugins are perhaps a weakness to its community. The majority of plugins on symfony's website are obsolete and should be removed from the listings. However, with enough searching, it is possible to find some real gems. For example, the profile images on remixsquared are handled with the sfImageThumbnailPlugin, and the "usermap" is handled by the Google Maps plugin. The sfEasyGoogleSitemapPlugin creates a sitemap.xml file automatically. On the other hand, I had to discard the sfDatagridPlugin because I could not get it working, and I had to "hack" the Google Maps plugin to force it to output actual HTML because a bug caused it to escape the map code itself. There are also quite a few plugins that are missing. It would be useful if I hadn't had to write an entire module to handle ffmpeg conversion of lossless music files to *.MP3 format, for example. If you expect symfony to provide plugins for everything, or if you expect every plugin to work as advertised, then think again. On the other hand, search to see if a plugin is available before you implement a new feature, because you might be surprised at how much time can be saved.

Finally, one of the most annoying aspects of symfony is that deployment is a nightmare. Deploying an application is nowhere near as simple as the authors of the "symfony Cookbook" would have one believe. Recently, I tried to upgrade symfony from version 1.2.8 to version 1.2.9, and the site was offline for an entire day until I had to revert. Yet, the site functions fine on the newer version in the Dev environment. About every third release, I find something that doesn't work the same way in Dev as it does in production. Some issues were caused by Apache or PHP configuration errors which should be warned about by symfony. This sort of trouble almost never happens with other languages, and is a significant detractor from symfony. Development on Windows with production in Linux is particularly frustrating, because of the case-sensitivity of Linux filenames. Including a helper file using a lowercase letter (i.e. "use_helper('form');") works great on Windows, and crashes the site with a file not found error on Linux when Linux can't find the helper file, which actually begins with a capital letter. It is a shame that symfony's core developers are unsympathetic to this issue - when I reported a bug, the team declared that it was "an OS problem" and closed the ticket. Adding code to make the case-sensitivity or case-insensitivity consistent on both platforms is easy and degrades performance negligibly. Until such a check is added, however, be sure that you always refer to filenames with the correct case, because capitalization is important when moving across systems. Making everything lowercase doesn't help, because symfony itself is not consistent with case-sensitivity in its own filenames.

Conclusion

In conclusion, the most obvious benefit of symfony is that it is free. Some people assume that products that are less expensive are obviously inferior to products that cost more, but I have not found this statement to be true in the world of open-source software. Oracle Database, for example, is actually far less user-friendly and much more difficult to use than MySQL, it lacks the extensive "live profiling" features of SQL Server that allow the user to watch what queries are being executed in real time, and its licenses cost hundreds of thousands of dollars for some standard installations. Likewise, ASP .Net is a programming environment that is supported by a major corporation with excellent technical support, but almost all of its features can be acquired by using free PHP debuggers and frameworks. With symfony, provided that the developer owns an old computer that can be used as a web server, it is literally possible to start a business for zero cost. Should the business model prove unsuccessful, the entrepreneur can simply shut down the site, and should the idea take off, the site can easily be migrated to a commercial host without being crippled by exponentially increasing licensing fees.

If I were given the opportunity to use .Net for free or to pay to use symfony, I would still select symfony as the superior product. However, the framework does have a steep learning curve. Hopefully this article will help you avoid some of the most common pitfalls that exist in this otherwise great application framework.

Copyright

Media posted on this site is created and owned by its authors.  Contact us
Whiskey Media