PHP frameworks, Part 3: User authentication

Exploring MVC-style architectures in Zend, symfony, and CakePHP

A common criticism of early versions on PHP was that they did not support Model-View-Controller (MVC)-style architectures. Today, developers can chose from many PHP frameworks. This "PHP frameworks" series takes a look at three widely used PHP frameworks — Zend, symfony, and CakePHP — examining their similarities and differences while building and extending a sample application in each of the three frameworks. Part 1 lays out the scope for the series and gets the prerequisites out of the way. In Part 2, you build the sample application in each of the three frameworks. Here in Part 3, you will extend the application and look at exceptions to the rule.

Duane O'Brien, PHP developer, Freelance

Photograph of Duane O'BrienDuane O'Brien has been a technological Swiss Army knife since the Oregon Trail was text only. His favorite color is sushi. He has never been to the moon.



04 December 2007

Also available in Japanese Vietnamese

About this series

This series is designed for PHP developers who want to start using a framework, but have not examined the available frameworks in detail. This series examines why the three frameworks under examination were chosen, how to install each, and you'll get a good handle on the test application that you're going to extend in each framework. It might sound like a lot, but don't worry — we break it down into manageable chunks.

Part 1 of this series lays out the scope for the series, introduces the frameworks being examined, covers their installation, and scopes out the first test application you will build. (Phew!)

Part 2 walks you through building the sample application in each of the three frameworks, highlighting their similarities and differences.

Part 3 starts with extending the test application, then deals with exceptions to the rule. All frameworks work well when doing the tasks for which they were designed. Needing to do something the framework wasn't built to do happens on every project. This article looks at such instances.

Part 4 focuses primarily on Ajax support. The use of Ajax, using native code and third-party libraries, is examined — specifically, how each framework behaves and accepts specific popular libraries.

Part 5 deals with working outside the frameworks. A single task is identified (nightly update script), and the process for accomplishing this task is examined in each framework.


About this article

This article walks through extending the sample application, Blahg, in each of the three frameworks (Zend, symfony, and CakePHP). Then we look at dealing with exceptions to the rule. Most frameworks perform well when the application is designed for the framework. However, what happens when an application just won't conform?

You should have already gone through and understood Part 1 and Part 2 of this series.


Extending Blahg

At the end of Part 2, it was suggested that you might try extending Blahg to add a comments controller, to allow readers to respond to a particular post with their own insightful, creative, and meaningful input. It was also suggested that you didn't need to break your back integrating the comments and posts controller, since you'd get some code in this article to outline how that works. Of course, it works differently, depending on the framework you're using at any given time. But regardless of the framework, we need a comments table.


Creating the comments table

For the Zend Framework and symfony, use the following SQL to create a comments table.

Listing 1. Creating a comments table in Zend Framework and symfony
CREATE TABLE 'comments' ( 
'id' INT( 10 ) NOT NULL AUTO_INCREMENT PRIMARY KEY , 
'post_id' INT( 10 ) NOT NULL , 
'text' TEXT NOT NULL , 
'created' TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 
) ENGINE = MYISAM ;

For CakePHP, use the following SQL.

Listing 2. Creating a comments table in CakePHP
CREATE TABLE 'comments' ( 
'id' INT( 10 ) NOT NULL AUTO_INCREMENT PRIMARY KEY , 
'post_id' INT( 10 ) NOT NULL , 
'text' TEXT NOT NULL , 
'created' DATETIME DEFAULT NULL 
) ENGINE = MYISAM ;

With comments tables in place, we can get down to business by adding the comments functionality.


Adding comments with the Zend Framework

Adding comments with the Zend Framework looks similar to what we did when we created the posts functionality. We create a comments model to handle the database interaction for our comments and, of course, a comments controller to do the actual work. But we don't need any views, since the comments will be displayed when we read a post.

All that doesn't necessarily have to be the case. We could look at comments as a logical part of posts and decide to include them all in the same controller, model, etc. For that matter, we could look at posts as comments without a parent and do everything in one table. We could also write Blahg completely in one PHP file if we wanted, but that doesn't mean it's a good idea. The idea here is to break our application into distinct functional pieces.

The comments model is going to look just like the posts model, name changes aside. We can do everything we need with the methods it inherits from Zend_Db_Table. And as mentioned, we don't need any comment-specific views, but we need to modify the posts read view to output any comments and include the form to let people post comments. This form will point to the write action of the comments controller.

The comments controller will look like a slimmed-down version of the posts controller, since all we need is a writeAction. But there will be an important difference: After the data is written, we need to redirect the user to the post he was reading when he submitted the comment. And speaking of that posts controller, we need to modify the readAction to retrieve the comments for each post and assign them to the view.

Find examples of how to do all this in the code archive. You may have come up with a different solution. That's good. Now that we've added comments with Zend, we can take a look at how to do it in symfony.


Adding comments with symfony

When it comes to adding comments with the symfony version of Blahg, we need to start in our schema.yml (it should be in /column/protected/sf_column/config) and add some definition for the comments table. We need to add the following lines.

Listing 3. Adding comments with symfony
  comments :
    _attributes: {phpName: Comment }
    id:
    post_id:
    text:    longvarchar
    created: timestamp

As before, if you copied and pasted that text, make sure comments is indented two spaces and that the elements under comments are indented four spaces. Whitespace is important.

Note: Like CakePHP, symfony will perform some magic for you on the created and modified columns if you name them created_at and modified_at, respectively. In this case, that magic is being picked up by the MySQL database, instead.

Once we're done, we need to build the propel model and clear the cache. At the command line, in /column/protected/sf_column, execute the following commands:

php /column/src/symfony/data/bin/symfony propel-build-model
php /column/src/symfony/data/bin/symfony clear-cache

Once we run these commands, you should see the files Comment.php and CommentPeer.php in the /column/protected/sf_column/lib/model directory. Now that our models are sorted out, we can whip up the rest of the comments functionality.

Note: This should have been mentioned in Part 2, but if you installed symfony using the PHP Extension and Application Repository (PEAR), you can just run symfony propel-build-model without calling PHP first or providing the full path to the binary. Not everyone has the option of using the PEAR installation in their environment, which is why we described the alternate installation instructions.

Now that your model is in place, we need to modify the actions.class.file.php in /column/protected/sf_column/apps/blahg/modules/post/actions/ to add some selection criteria for the comments and assign comments to the post. We also need to make some modifications to the readSuccess.php file in the /column/protected/sf_column/apps/blahg/modules/post/templates/ directory to output the comments for a given post. Look at the files in the code archive for details on both of these.

Next, we need to init the comment module for Blahg. Run the following command from the /column/protected/sf_column directory: php /column/src/symfony/data/bin/symfony init-module blahg comment.

Next, we add the executeWrite action to the actions.class.php in the /column/protected/sf_column/apps/blahg/modules/comment/actions/ directory. This will look similar to the executeWrite action from the post module, except that after the write, we want to redirect back to the post for reading.

Finally, we need to modify the readSuccess.php file for the posts module, adding a form that allows the reader to post his comments. All of this code is included in the code archive.


Adding comments with CakePHP

To add comments to the CakePHP version of Blahg, you will need a comments controller (to handle writing out comments), but more importantly, you need to write a comment model and define an association between comment and post. Once you specify that a post "has many" comments and a comment "belongs to" a single post, the same query that retrieves the post for reading will retrieve the comments for that post. These are called model associations in CakePHP.

If you look in the code archive for the updated CakePHP version of Blahg, we see a fairly typical comments controller (comments_controller.php), but with a significant difference from your posts controller: The class variable autoRender set to false. This allows you to use actions from the controller without views. Not setting autoRender to false will result in "missing view" errors when you submit comments. Also in the code archive are the new comment model and changes to the post model to reflect the model association. As you can see, when you get your model associations straightened out, CakePHP will do a lot of the heavy lifting for you.


Exceptions to the rule

So far, Blahg has been written to fit with each framework, taking advantage of the strengths offered by each framework. That's pretty easy. It works great as long as the framework we're working with can account for everything our application needs to do. But what if an application doesn't fit within the framework?

If you ever find yourself in this predicament, the first question you should ask yourself, even if you've already thought it through once, is this: Why am I using a framework that doesn't let me define my application? The answer could be one of several. Perhaps the framework is pre-existing and replacement is out of the question. The framework may have been the only one your bosses would authorize. Or it could simply be that your application is different and none of the frameworks on the market really fits your model. If that's the case, before you start trying to force your square peg of an application into some differently shaped framework, make sure your application makes sense and is structured in a meaningful way.

Are you sure it needs to work that way? If so, that's good. Let's talk about extending the frameworks.


The problem

You can post items in Blahg, and now readers have an opportunity to respond to what you've said. But it would really be nice to know at a glance what your Blahg is talking about. You could give it a snazzy title that tries to encapsulate the topics you intend to cover, like "World of Spatulas" or "Polka for the Rest of Us," but you don't have any control over what your readers might be talking about in their posts. And sometimes you might feel like talking about something else. Or maybe you might just be a math geek and you want some numbers.

A possible solution to this might be something that takes incoming content, builds up a count of words used, and gives you a neat little list of the most common words in your Blahg. The problem: Where do you do that? You could argue it's a task to be accomplished in the Model layer, since you're categorizing the data. You could also argue that it's a task for the Controller, since you need to do some analysis, and analysis implies logic. And if you want to see these numbers, won't you need something in the View? But the user won't ever actually visit a words controller. How do you extend the framework to do this?

To highlight the basic approach, we need a table to hold words (let's call that table "words"). This table will hold a word and the number of times that word has occurred. We might use the syntax in Listing 4 to define this table.

Listing 4. Table to hold words
CREATE TABLE 'words' ( 
'id' INT( 10 ) NOT NULL AUTO_INCREMENT PRIMARY KEY , 
'word' varchar(255) NOT NULL,
'occurrences' INT( 10 ) NOT NULL
) ENGINE = MYISAM ;

The code to do the word analysis will need to:

  1. Strip any HTML (which you should be doing anyway)
  2. Strip out nonalpha characters that are not whitespace (which messes up apostrophes, but it's a good place to start)
  3. Replace multiple whitespace characters with single whitespace characters
  4. Lowercase everything
  5. Explode on whitespace
  6. Generate word counts from the resulting array
  7. Update the words table with new counts, adding rows for new words and updating rows for words already there

Don't worry if that sounds like a lot. It will come together as MVC as you write the code. What you will end up with in the end is, essentially, a Model to handle the words table, a View that lets you see the word counts, and a Controller to perform some logic.


Adding word stats in Zend

If all we wanted to do was compile word counts without displaying them, we could probably solve this problem using Zend_Controller_Plugin_Abstract. But since we want to actually be able to view the word counts, it probably makes more sense to build a words module with a Model and a Controller. The Model will be fairly basic, like others we've done for Zend. The Controller will have an indexAction method that will retrieve and display the word counts, but it will also need a method to process the text: processText. This method does not end in Action. This is intentional. The index view is also very straightforward: It needs to iterate the list of words and dump them to screen.

The code for indexAction is fairly simple. Just get the words, order by count (descending). But the code for the processText method has to do a little more. It needs to strip the text, break it into an array, and count the occurrences of each word. Then the code needs to get the words from the words table and put them into a list. If a word is in the master list, we need to update the row with a new count. If a word is new, we need to insert a row with that word. It's all fairly straightforward. You can see all the code in the code archive.

To use the processText method in the posts and comments controllers, we first need to require the WordController.php file. In the writeAction, we need to create an instance of the WordController. When we do this, we need to pass in Request and Response objects. Then we just call the processText method as if it were a method on any old object. For posts, don't forget to pass in the title and the text.

Tip: If we pass in the text after we passed it through the StripTags filter, we don't have to strip tags later. It might look something like this:

$wc = new WordController($this->_request, $this->_response);
$wc->processText($data['title'] . ' ' . $data['text']);

Check the code archive for details. Once we have everything in place and we modify the posts and comments controllers, we should be able to enter some posts and comments, and watch the word counts update at http://localhost/zend/word.

Note: You would probably write some of this code a bit differently in a high-volume setup. The intent here isn't to show you how to do awesome text-parsing word count-stats analysis, but to use it as an example in the context of trying to do something the frameworks handle awkwardly.


Adding word stats In symfony

As you might have guessed, the first thing we need to do is add the definition of the words table to the schema.yml configuration file. Keeping in mind the proper indentation, the definition should look like Listing 5.

Listing 5. Adding the definition of the words table to the schema.yml file
  words 
    _attributes: {phpName: Word }
    id:
    word:    varchar(255)
    occurrences:   integer

What do we do next? That's right — we need to build the model and clear the cache, exactly like we did when we added comments above. Then we need to init a word module. Make sure to run all those commands from the /column/protected/sf_column directory.

Go ahead and do the easy part first. We know that the word module is going to do only one thing that's user-accessible: show the list of words in the words table. You can probably figure out what the executeIndex action and indexSuccess templates will look like by looking at the posts module.

Since the easy stuff is out of the way, we need to get the text from posts and comments parsed, counted, and so forth. One way to do this would be to create an action called executeParsetext in the actions.class.php for the word module and forward to that action after a post or comment has been saved. Before doing that, we should set an ID to hold the post ID, so we know where to return. In the posts module's actions.class.php it might look like this:

$this->id = $post->getId();
$this->forward('word', 'Parsetext');

Once we execute the forward to the other module, we need to extract the variables passed. This is actually a little tricky. The code to get to the variables looks something like this:

$vars =
$this->request->getContext()->getActionStack()->getFirstEntry()->\
getActionInstance()->getVarHolder()->getAll();

Note: It really seems like there should be an easier way to do this. Another technique is to set a value in the flashMessage and forward it along, but that doesn't seem much better. Feel free to submit feedback if there's a more elegant way to do this.

Once you get to the variables, break up the text as before and use the word model to save appropriate rows. To use this one action to parse text for posts and comments, make sure to init the title variable to something. Once we're done, we can redirect back to the posts module, and since we kept the ID of the post, we can go straight to reading it.

All of this is included in the code archive. Take some time to review it before moving on to CakePHP.


Adding word stats in CakePHP

In CakePHP, we implement this functionality in two parts. We build what's called a component, which is a sort of controller helper, and this component will process the text. Then we make a standard model, controller, and view for words to view the list of words from the database. The posts and comments controllers will use the words model to save the word counts after they've been processed.

To create the WordcountComponent, create a the file called wordcount.php in the /column/protected/cakephp/app/controllers/components directory. Define the class WordcountComponent as an extension of object and create the method processText using the now-familiar code to strip, split, and count the text into an array. This method should return the array of counted words.

Using what we already know about CakePHP, create a basic Model, View, and Controller that will get the content of the words table and display them at the index action. No other actions should be necessary.

In our posts and comments controllers, we need to add a couple lines to specify that we are using the word model and our new component:

var $uses = array ('Post', 'Word');
var $components = array('Wordcount');

After we have all that in place, we need to add the code to the write actions in both controllers to strip the tags, pass in the text to the WordcountComponent, get the content of the words table, iterate the array of counted words returned by the WordcountComponent, and do the appropriate saves. By now, you should have a pretty good picture of how all that will fit together, but as usual, the code to do all of this is included in the code archive.


Summary

No matter how much work is put into a framework, there's just no way to make one framework that can handle every possible application a PHP developer can concoct. The more rigid a framework is, the less it will flex to deal with exceptions to the rules. Very early in the process of choosing a framework, try to see how the framework will handle your unique needs. Forget trying out the Create, Read, Update, Delete (CRUD). Most frameworks handle that well enough. It's the framework's ability to bend to your needs that will show you its strength. You may need to bend your thinking to meet halfway, but frameworks that are overly rigid will break when you require versatility.

See if you can come up with another unique problem that the frameworks won't like. Then dive into the documentation and see if you can solve the problem. Or, if you're having trouble thinking of a problem, come up with a good way to omit words from the word-count process that will be too common to be useful, such as articles, prepositions, pronouns, etc.

In Part 4, we use Ajax and see how to use native code and third-party libraries, specifically how each framework behaves and accepts specific popular libraries.


Download

DescriptionNameSize
Part 3 sample codeos-php-fwk3.source.zip21KB

Resources

Learn

  • For a good tutorial series on using the Zend Framework, read "Understanding the Zend Framework, Part 1."
  • Get the CakePHP Manual (Note: The manual is written to CakePHP V1.1. There may be some variance as you are using CakePHP V1.2).
  • PHP.net is the central resource for PHP developers.
  • Check out the "Recommended PHP reading list."
  • Browse all the PHP content on developerWorks.
  • Expand your PHP skills by checking out IBM developerWorks' PHP project resources.
  • To listen to interesting interviews and discussions for software developers, check out developerWorks podcasts.
  • Using a database with PHP? Check out the Zend Core for IBM, a seamless, out-of-the-box, easy-to-install PHP development and production environment that supports IBM DB2 V9.
  • Stay current with developerWorks' Technical events and webcasts.
  • Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
  • Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
  • Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.

Get products and technologies

  • Innovate your next open source development project with IBM trial software, available for download or on DVD.
  • Download IBM product evaluation versions, and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=272054
ArticleTitle=PHP frameworks, Part 3: User authentication
publish-date=12042007