Practical Web 2.0 Applications With PHP

laux123

贡献于2011-08-30

字数:0 关键词: PHP开发 PHP

this print for content only—size & color not accurate spine = 1.1163" 592 page count Books for professionals by professionals® Practical Web 2.0 Applications with PHP Dear Reader, Many programming books on the market today focus specifically on a particu- lar methodology or software package, and although you will gain a solid under- standing of the subject matter from these books, you won’t always know how to apply what you’ve learned in a real-world situation. This book is designed to show you how to bring together many different ideas and features by start- ing with a clean slate and gradually building the code base so it evolves into a complete web application. The premise of the application we build in this book is that it is a “Web 2.0” application. What this means is that (among other things) our application gen- erates accessible and standards-compliant code while making heavy of use of Ajax. We achieve this by using the Smarty™ Template Engine and Cascading Style Sheets, as well as the Prototype JavaScript library. Additionally, we create a fun and intuitive interface by applying simple visual effects on various pages using the Script.aculo.us JavaScript library. To help with the development of the extensive PHP code in this book, we use the Zend Framework. This is an open source PHP 5 library that contains many different components that you can easily use in any of your day-to-day development. We use many of the Zend Framework components in this book, such as database abstraction (with a focus on MySQL® and PostgreSQL), logging, authentication, and search. The “Web 2.0” application that we build in this book is a collaborative blogging tool. It will allow users to register and create a personal blog. When creating blog posts, users will be able upload images, apply tags, and assign locations (using Google Maps). We will also look at how to use microformats when displaying user blog posts. Quentin Zervaas US $44.99 Shelve in PHP User level: Intermediate–Advanced Zervaas Web 2.0 Applications with PHP The EXPERT’s VOIce® in Web Development Practical Web 2.0 Applications with PHP cyan maGENTA yelloW   BLACK panTONE 123 C Quentin Zervaas Companion eBook Available www.apress.com SOURCE CODE ONLINE Companion eBook See last page for details on $10 eBook version ISBN-13: 978-1-59059-906-8 ISBN-10: 1-59059-906-3 9 781590 599068 5 4 4 9 9 Develop a complete PHP web application from start to finish Related Titles     Practical Quentin Zervaas Practical Web 2.0 Applications with PHP 9063CH00CMP3 11/19/07 8:39 PM Page i Practical Web 2.0 Applications with PHP Copyright © 2008 by Quentin Zervaas All rights reserved. No part of this work may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval system, without the prior written permission of the copyright owner and the publisher. ISBN-13 (pbk): 978-1-59059-906-8 ISBN-10 (pbk): 1-59059-906-3 ISBN-13 (electronic): 978-1-4302-0474-9 ISBN-10 (electronic): 1-4302-0474-5 Printed and bound in the United States of America 9 8 7 6 5 4 3 2 1 Trademarked names may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. Lead Editor: Ben Renow-Clarke Technical Reviewer: Jeff Sambells Editorial Board: Steve Anglin, Ewan Buckingham, Tony Campbell, Gary Cornell, Jonathan Gennick, Jason Gilmore, Kevin Goff, Jonathan Hassell, Matthew Moodie, Joseph Ottinger, Jeffrey Pepper, Ben Renow-Clarke, Dominic Shakeshaft, Matt Wade, Tom Welsh Project Manager: Richard Dal Porto Copy Editors: Andy Carroll, Kim Wimpsett Assistant Production Director: Kari Brooks-Copony Production Editor: Liz Berry Compositor: Diana Van Winkle Proofreader: Lisa Hamilton Indexer: Broccoli Information Management Artist: Diana Van Winkle Cover Designer: Kurt Krames Manufacturing Director: Tom Debolski Distributed to the book trade worldwide by Springer-Verlag New York, Inc., 233 Spring Street, 6th Floor, New York, NY 10013. Phone 1-800-SPRINGER, fax 201-348-4505, e-mail orders-ny@springer-sbm.com, or visit http://www.springeronline.com. For information on translations, please contact Apress directly at 2855 Telegraph Avenue, Suite 600, Berkeley, CA 94705. Phone 510-549-5930, fax 510-549-5939, e-mail info@apress.com, or visit http://www.apress.com. The information in this book is distributed on an “as is” basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author(s) nor Apress shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work. The source code for this book is available to readers at http://www.apress.com. 9063CH00CMP3 11/19/07 8:39 PM Page ii Contents at a Glance About the Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv About the Technical Reviewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii ■CHAPTER 1 Application Planning and Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 ■CHAPTER 2 Setting Up the Application Framework . . . . . . . . . . . . . . . . . . . . . . . . . . 9 ■CHAPTER 3 User Authentication, Authorization, and Management . . . . . . . . . . . . . 45 ■CHAPTER 4 User Registration, Login, and Logout . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 ■CHAPTER 5 Introduction to Prototype and Scriptaculous . . . . . . . . . . . . . . . . . . 123 ■CHAPTER 6 Styling the Web Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 ■CHAPTER 7 Building the Blogging System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 ■CHAPTER 8 Extending the Blog Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 ■CHAPTER 9 Personalized User Areas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 ■CHAPTER 10 Implementing Web 2.0 Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 ■CHAPTER 11 A Dynamic Image Gallery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 ■CHAPTER 12 Implementing Site Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427 ■CHAPTER 13 Integrating Google Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 ■CHAPTER 14 Deployment and Maintenance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519 ■INDEX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547 iii 9063CH00CMP3 11/19/07 8:39 PM Page iii 9063CH00CMP3 11/19/07 8:39 PM Page iv Contents About the Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv About the Technical Reviewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii ■CHAPTER 1 Application Planning and Design . . . . . . . . . . . . . . . . . . . . . . . . . . 1 What Is Web 2.0? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Database Connectivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Web Site Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Web Site Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Main Home Page and User Home Page . . . . . . . . . . . . . . . . . . . . . . . . 3 User Registration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Account Login and Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 User Blogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Web Site Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Application Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Other Aspects of Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Search-Engine Optimization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 PHPDoc-Style Commenting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Application Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Maintainability and Extensibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Version Control and Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 ■CHAPTER 2 Setting Up the Application Framework . . . . . . . . . . . . . . . . . . . . 9 Web Server Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Operating System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Installing the Apache HTTP Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Installing MySQL 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Installing PHP 5.2.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 v 9063CH00CMP3 11/19/07 8:39 PM Page v ■CONTENTSvi Application Filesystem Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Web Root Directory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Data Storage Directory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 PHP Classes Directory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Templates Directory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Full Directory Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Installing the Zend Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Configuring the Web Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Creating a Virtual Host in Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Creating a Virtual Host in Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Restarting Your Web Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Setting Up the Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Using the Model-View-Controller Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Separating Application Logic from Presentation Logic . . . . . . . . . . . 19 Directing All Requests to index.php . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Introduction to the Zend_Controller Class . . . . . . . . . . . . . . . . . . . . . 22 How Requests Work with Zend_Controller . . . . . . . . . . . . . . . . . . . . . 23 Creating the IndexController . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Defining Application Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Connecting to the Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Testing the Database Connection . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 The Smarty Template Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Why Not Use a Different Template Engine? . . . . . . . . . . . . . . . . . . . . 33 Downloading and Installing Smarty . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Automatic View Rendering with Zend_Controller . . . . . . . . . . . . . . . 36 Integrating Smarty with the Web Site Controllers . . . . . . . . . . . . . . . 39 Adding Logging Capabilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Writing to the Log File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 ■CHAPTER 3 User Authentication, Authorization, and Management . . . . . . 45 Creating the User Database Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Timestamps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 User Profiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 Introduction to Zend_Auth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Instantiating Zend_Auth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Authenticating with Zend_Auth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Introduction to Zend_Acl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 A Zend_Acl Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 9063CH00CMP3 11/19/07 8:39 PM Page vi Combining Zend_Auth, Zend_Acl, and Zend_Controller_Front . . . . . . . . 57 Managing User Records with DatabaseObject . . . . . . . . . . . . . . . . . . . . . . 61 The DatabaseObject_User Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Using DatabaseObject_User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Managing User Profiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Using Profile_User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Integrating Profile_User with DatabaseObject_User . . . . . . . . . . . . 69 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 ■CHAPTER 4 User Registration, Login, and Logout . . . . . . . . . . . . . . . . . . . . . 73 Adding User Registration to the Application . . . . . . . . . . . . . . . . . . . . . . . . 73 Creating the Form Processor for User Registration . . . . . . . . . . . . . 74 Displaying the Registration Form and Processing Registrations . . . 81 Adding CAPTCHA to the User Registration Form . . . . . . . . . . . . . . . . 88 Adding E-mail Functionality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 Implementing Account Login and Logout . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Creating the Login Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 Adding the Account Controller Login Action . . . . . . . . . . . . . . . . . . . 102 Logging Successful and Failed Login Attempts . . . . . . . . . . . . . . . . 105 Logging Users Out of Their Accounts . . . . . . . . . . . . . . . . . . . . . . . . 107 Dealing with Forgotten Passwords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Resetting a User’s Password . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 Functions for Resetting Passwords . . . . . . . . . . . . . . . . . . . . . . . . . . 112 Implementing Account Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 Creating the Account Home Page . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 Updating the Web Site Navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 Allowing Users to Update Their Details . . . . . . . . . . . . . . . . . . . . . . . 120 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 ■CHAPTER 5 Introduction to Prototype and Scriptaculous . . . . . . . . . . . . 123 Downloading and Installing Prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Prototype Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 Selecting Objects in the Document Object Model . . . . . . . . . . . . . . . . . . . 124 The $() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 The getElementsByClassName() Function . . . . . . . . . . . . . . . . . . . . 125 The $$() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 The getElementsBySelector() Function . . . . . . . . . . . . . . . . . . . . . . . 129 Prototype’s Hash Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 ■CONTENTS vii vii 9063CH00CMP3 11/19/07 8:39 PM Page vii Other Element Extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 Showing and Hiding Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 Retrieving Dimensions of Elements . . . . . . . . . . . . . . . . . . . . . . 131 Managing Classes of Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 Manipulating Strings with Prototype . . . . . . . . . . . . . . . . . . . . . . . . . 133 Ajax Operations in Prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Ajax Request Options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Ajax Callback Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 JavaScript Object Notation (JSON) . . . . . . . . . . . . . . . . . . . . . . . . . . 138 An Ajax.Request Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 Event Handling in Prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 Observing an Event . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 Finding Out Which Element an Event Occurred On . . . . . . . . . . . . . 146 Canceling an Event . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 Creating JavaScript Classes in Prototype . . . . . . . . . . . . . . . . . . . . . . . . . 147 Creating a Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 Binding Function Calls to Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 From Prototype to Scriptaculous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 Prebuilt Controls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 Drag and Drop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 Visual Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 DOM Element Builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 JavaScript Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Downloading and Installing Scriptaculous . . . . . . . . . . . . . . . . . . . . . . . . . 154 Combining Prototype, Scriptaculous, Ajax, and PHP in a Useful Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 Creating the Main HTML Page: index.php . . . . . . . . . . . . . . . . . . . . 156 Styling the Application: styles.css . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 Creating and Populating the Database: schema.sql . . . . . . . . . . . . 158 Managing the List Items on the Server Side: items.php . . . . . . . . 159 Processing Ajax Requests on the Server Side: processor.php . . . 161 Creating the Client-Side Application Logic: scripts.js . . . . . . . . . . . 163 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 ■CHAPTER 6 Styling the Web Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Adding Page Titles and Breadcrumbs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 The Breadcrumbs Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 Generating URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 Setting the Title and Trail for Each Controller Action . . . . . . . . . . . . 178 Creating a Smarty Plug-In to Output Breadcrumbs . . . . . . . . . . . . 180 Displaying the Page Title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 ■CONTENTSviii 9063CH00CMP3 11/19/07 8:39 PM Page viii 809b8b6f91d5ff50033254241f3132ed Integrating the Design into the Application . . . . . . . . . . . . . . . . . . . . . . . . 183 Creating the Static HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 Moving the HTML Markup into Smarty Templates . . . . . . . . . . . . . 188 Constructing the CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192 Specifying Media Types and Loading the CSS File . . . . . . . . . . . . . 192 Creating the Application CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 Creating a Print-Only Style Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 The Full Application Style Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 Styling the Application Web Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 Loading Prototype and Scriptaculous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 Implementing Client-Side Form Validation . . . . . . . . . . . . . . . . . . . . . . . . . 208 Adding JSON Support to CustomControllerAction . . . . . . . . . . . . . . 209 Modifying the Form Processor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 Modifying the Registration Controller Action . . . . . . . . . . . . . . . . . . 210 Creating the JavaScript Form Validator . . . . . . . . . . . . . . . . . . . . . . . 212 Loading the UserRegistrationForm Class . . . . . . . . . . . . . . . . . . . . . 216 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 ■CHAPTER 7 Building the Blogging System . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 Creating the Database Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 Setting Up DatabaseObject and Profile Classes . . . . . . . . . . . . . . . . . . . . 221 Creating the DatabaseObject_BlogPost Class . . . . . . . . . . . . . . . . . 221 Creating the Profile_BlogPost Class . . . . . . . . . . . . . . . . . . . . . . . . . 223 Creating a Controller for Managing Blog Posts . . . . . . . . . . . . . . . . . . . . . 223 Extending the Application Permissions . . . . . . . . . . . . . . . . . . . . . . . 223 The BlogmanagerController Actions . . . . . . . . . . . . . . . . . . . . . . . . . 225 Linking to Blog Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 Creating and Editing Blog Posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 Creating the Blog Post Submission Form Template . . . . . . . . . . . . 228 Instantiating FormProcessor_BlogPost in editAction() . . . . . . . . . . 231 Implementing the FormProcessor_BlogPost Class . . . . . . . . . . . . . 233 Generating a Permanent Link to a Blog Post . . . . . . . . . . . . . . . . . . 240 Filtering Submitted HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 Creating a New Blog Post . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 Previewing Blog Posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 Creating the Preview Action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Implementing the Preview Template . . . . . . . . . . . . . . . . . . . . . . . . . 249 Requesting Confirmation for User Actions . . . . . . . . . . . . . . . . . . . . 252 ■CONTENTS ix 9063CH00CMP3 11/19/07 8:39 PM Page ix Updating the Status of a Blog Post . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 Completing setstatusAction() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 Notifying the User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 ■CHAPTER 8 Extending the Blog Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 Listing Blog Posts on the Blog Manager Index . . . . . . . . . . . . . . . . . . . . . 265 Fetching Blog Posts from the Database . . . . . . . . . . . . . . . . . . . . . . 266 Assigning Recent Posts and the Monthly Summary to the Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 Displaying Recent Posts in the Template . . . . . . . . . . . . . . . . . . . . . 276 Displaying the Monthly Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 Ajaxing the Blog Monthly Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 Creating the Ajax Request Output . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 The BlogMonthlySummary JavaScript Class . . . . . . . . . . . . . . . . . . 285 Installing the BlogMonthlySummary Class . . . . . . . . . . . . . . . . . . . . 287 Notifying the User About the Content Update . . . . . . . . . . . . . . . . . . 287 Integrating a WYSIWYG Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 Downloading and Installing FCKeditor . . . . . . . . . . . . . . . . . . . . . . . 292 Configuring FCKeditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293 Loading FCKeditor in the Blog Editing Page . . . . . . . . . . . . . . . . . . . 294 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 ■CHAPTER 9 Personalized User Areas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 Controlling User Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 Presenting Customizable Settings to Users . . . . . . . . . . . . . . . . . . . 298 Processing Changes to User Settings . . . . . . . . . . . . . . . . . . . . . . . . 299 Creating Default User Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 The UserController Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 Routing Requests to UserController . . . . . . . . . . . . . . . . . . . . . . . . . . 303 Handling Requests to UserController . . . . . . . . . . . . . . . . . . . . . . . . . 309 Displaying the User’s Blog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313 Displaying the Blog Index Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313 Displaying Individual Blog Posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318 Generating Blog Archive Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322 Displaying the Monthly Archive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324 Populating the Application Home Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326 Loading Recent Public Posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326 Implementing the Application Home Page . . . . . . . . . . . . . . . . . . . . 327 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333 ■CONTENTSx 9063CH00CMP3 11/19/07 8:39 PM Page x ■CHAPTER 10 Implementing Web 2.0 Features . . . . . . . . . . . . . . . . . . . . . . . . . . 335 Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 Implementing Tagging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 Managing Blog Post Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340 Displaying a User’s Tags on Their Blog . . . . . . . . . . . . . . . . . . . . . . . 344 Displaying a Tag Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 Displaying Tags on Each Post . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 Web Feeds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 Data Formats for Web Feeds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352 Creating an Atom Feed with Zend_Feed . . . . . . . . . . . . . . . . . . . . . . 352 Adding the Feed to UserController . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 Linking to Your Feed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 Other Feed Options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 Microformats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 An Example of Using Microformats . . . . . . . . . . . . . . . . . . . . . . . . . . 358 Why Use Microformats? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360 Microformatting Your Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362 Allowing Users to Create a Public Profile . . . . . . . . . . . . . . . . . . . . . . . . . . 363 Allowing Users to Create a Public Profile . . . . . . . . . . . . . . . . . . . . . 363 Displaying a User’s Profile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369 ■CHAPTER 11 A Dynamic Image Gallery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 Storing Uploaded Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372 Creating the Database Table for Image Data . . . . . . . . . . . . . . . . . . 373 Controlling Uploaded Images with DatabaseObject . . . . . . . . . . . . 373 Uploading Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374 Setting the Form Encoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 Adding the Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 Specifying the File Input Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 Setting the Maximum File Size . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378 Handling Uploaded Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 Sending Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387 Resizing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390 Creating Thumbnails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390 Linking the Thumbnailer to the Image Action Handler . . . . . . . . . . 395 ■CONTENTS xi 9063CH00CMP3 11/19/07 8:39 PM Page xi Managing Blog Post Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399 Automatically Loading Blog Post Images . . . . . . . . . . . . . . . . . . . . . 399 Displaying Images on the Post Preview . . . . . . . . . . . . . . . . . . . . . . 401 Deleting Blog Post Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403 Using Scriptaculous and Ajax to Delete Images . . . . . . . . . . . . . . . 406 Deleting Images when Posts Are Deleted . . . . . . . . . . . . . . . . . . . . . 411 Reordering Blog Post Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412 Displaying Images on User Blogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 Extending the GetPosts() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 Displaying Thumbnail Images on Blog Index . . . . . . . . . . . . . . . . . . 418 Displaying Images on the Blog Details Page . . . . . . . . . . . . . . . . . . 420 Displaying Larger Images with Lightbox . . . . . . . . . . . . . . . . . . . . . . 422 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425 ■CHAPTER 12 Implementing Site Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427 Introduction to Zend_Search_Lucene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427 Comparison to MySQL Full-Text Indexing . . . . . . . . . . . . . . . . . . . . . 428 Zend_Search_Lucene Field Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 429 Field Naming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430 Indexing Application Content . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430 Indexing Multiple Types of Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431 Creating a New Zend_Search_Lucene_Document . . . . . . . . . . . . . 431 Retrieving the Index Location . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433 Building the Entire Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434 Indexing and Unindexing a Single Blog Post . . . . . . . . . . . . . . . . . . 435 Triggering Search Index Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439 Creating the Search Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442 Adding the Search Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442 Handling Search Requests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443 Querying the Search Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444 Displaying Search Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448 Types of Searches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 Adding Autocompletion to the Search Tool . . . . . . . . . . . . . . . . . . . . . . . . 452 Providing Search Suggestions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452 Creating an Action Handler to Return Search Results . . . . . . . . . . 453 Retrieving Search Suggestions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454 Loading the SearchSuggestor Class . . . . . . . . . . . . . . . . . . . . . . . . . 457 Displaying Search Suggestions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457 Adding Mouse Navigation to Results . . . . . . . . . . . . . . . . . . . . . . . . . 460 Adding Keyboard Navigation to Results . . . . . . . . . . . . . . . . . . . . . . 462 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467 ■CONTENTSxii 9063CH00CMP3 11/19/07 8:39 PM Page xii ■CHAPTER 13 Integrating Google Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 Google Maps Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 Geocoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 Displaying Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470 Controlling Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 Planning Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 Limitations of Google Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 Browser Compatibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 Documentation and Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 Creating a Google Maps API Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 Adding Location Storage Capabilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475 Creating the Database Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475 Creating the DatabaseObject_BlogPostLocation Class . . . . . . . . . 475 Modifying Blog Posts to Load Locations . . . . . . . . . . . . . . . . . . . . . . 477 Creating Our First Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478 Creating a New Blog Manager Controller Action . . . . . . . . . . . . . . . 479 Displaying Your First Google Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481 Managing Locations on the Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 487 Handling Location Management Ajax Requests . . . . . . . . . . . . . . . 487 Creating the Address Lookup Form . . . . . . . . . . . . . . . . . . . . . . . . . . 492 Extending the BlogLocationManager JavaScript Class . . . . . . . . . 493 Using BlogLocationManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508 Displaying the Map on Users’ Public Blogs . . . . . . . . . . . . . . . . . . . . . . . . 509 Outputting Locations Using the Geo Microformat . . . . . . . . . . . . . . 509 Creating the BlogLocations Class . . . . . . . . . . . . . . . . . . . . . . . . . . . 511 Updating the Blog Post Display Template . . . . . . . . . . . . . . . . . . . . . 514 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516 ■CHAPTER 14 Deployment and Maintenance . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519 Application Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519 E-mailing Critical Errors to an Administrator . . . . . . . . . . . . . . . . . . 519 Using Application Logs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523 Site Error Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524 Objectives of Error Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526 Handling Predispatch Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526 Application Runtime Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 531 Web Site Administration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535 Administrator Section Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535 Implementing Administration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 536 ■CONTENTS xiii 9063CH00CMP3 11/19/07 8:39 PM Page xiii Application Deployment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538 Different Configurations for Different Servers . . . . . . . . . . . . . . . . . 538 Deploying Application Files with Rsync . . . . . . . . . . . . . . . . . . . . . . . 542 Backup and Restore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543 Exporting a Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543 Importing a Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 544 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545 ■INDEX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547 9063CH00CMP3 11/19/07 8:39 PM Page xiv About the Author ■QUENTIN ZERVAAS is a web developer based in Adelaide, South Australia, where he has been self-employed since 2003. After receiving his bachelor’s degree in computer science from the University of Adelaide in 2001, Quentin worked for several web development firms before branching out on his own, developing a wide range of custom web applications for customers all around the world. Quentin has recently started a new company called Recite Media (http://www.recite. com.au) with two partners. Recite Media develops web applications primarily for other devel- opment or design companies to resell. Its flagship product, Recite CMS, is being used by some of Australia’s largest companies. Quentin also runs and writes for his PHP development resource site, PhpRiot (www.phpriot.com), which provides a number of useful articles on a wide variety of PHP-related topics. After completing his role as the technical reviewer for Beginning Ajax with PHP: From Novice to Professional (Apress, 2006), he decided to undertake writing this book. xv 9063CH00CMP3 11/19/07 8:39 PM Page xv About the Technical Reviewer ■JEFFREY SAMBELLS is a graphic designer and self-taught web application developer best known for his unique ability to merge the visual world of graphics with the mental realm of code. After obtaining his bachelor’s of technology degree in graphic communications manage- ment with a minor in multimedia, Jeffrey originally enjoyed the paper-and-ink printing industry, but he soon realized the world of pixels and code was where his ideas would prosper. Jeffrey has previously published articles related to print design and has contributed to award-winning graphical and Internet software designs. His latest book, AdvancED DOM Scripting: Dynamic Web Design Techniques (friends of ED, 2007), was an instant success. In late 2005, Jeffrey also became a PHP 4 Zend Certified Engineer; he updated the certification to PHP 5 in September 2006 to become one of the first PHP 5 Zend Certified Engineers. Jeffrey also maintains a blog at http://jeffreysambells.com where he discusses his thoughts about everything from web development to photography. He currently lives and plays in Ontario, Canada, with his wife, Stephanie; his daughter, Addison; and their little dog, Milo. xvi 9063CH00CMP3 11/19/07 8:39 PM Page xvi Introduction Many of today’s web development books and articles cover single aspects of the development life cycle, delving only into specific features rather than looking at the whole picture. In this book, we will develop a complete web application. Although we will be using various third-party libraries and tools to aid in development, we will be developing the application from start to finish. The focus of this book is on Web 2.0, a catchphrase that has been in use for a few years now and is typically used to refer to web sites or web applications that have particular charac- teristics. Some of these characteristics include the following: •Correctly using HTML/XHTML, CSS, and other standards •Using Ajax (Asynchronous JavaScript and XML) to provide a responsive application without requiring a full refresh of pages • Allowing syndication of web site content using RSS •Adding wikis, blogs, or tags Although not everybody is an advocate of the “Web 2.0” phrase, the term does signify forward progress in web development. And although not everybody has the need to provide a wiki or a blog on their web site, the other characteristics listed (such as correct standards usage) provide a good basis for a web site and should be used by all developers, regardless of how they want their web site or application categorized. I wrote this book because I want to share with other users how I build web sites. Having been a web developer for ten years now (full-time for the past seven), I have a solid under- standing of a wide range of web-related topics and have much to offer newer developers or developers looking to expand their own knowledge. Who This Book Is For This book has been written primarily for intermediate to expert PHP programmers. Although programmers of all levels will benefit from this book, we do jump in to the deep end very quickly, so some prior knowledge of PHP is assumed. Having said that, if you’re relatively new to PHP, you will definitely benefit from this book because it will formalize some of the techniques you have already learned and will show you some different ways of approaching various problems. In this book, I have made the assumption that you are familiar with HTML and CSS, although since most of the code developed in this book is PHP and JavaScript, an advanced knowledge of HTML and CSS is not critical. All JavaScript code is explained thoroughly, which, in combination with the Prototype JavaScript library we will be using, makes the listings rela- tively straightforward. xvii 9063CH00CMP3 11/19/07 8:39 PM Page xvii How This Book Is Structured We will start the book by determining which features to implement in our web application and then implement each one as we progress through the book. Each chapter will add a new set of features to the application, until reaching the final chapter where we look at strategies for deploying the application. The specific type of application we develop in this book (a multiuser blogging system) is not particularly important; rather, it is used simply as a tool to show you the process of devel- oping a web application. Each chapter is specifically designed to demonstrate particular aspects of development that may arise regardless of the type of application: • Chapter 1, Application Planning and Design. We begin the book by looking at what defines Web 2.0, as well as looking briefly at the features that will be implemented in the application. Additionally, this chapter covers various aspects of the web develop- ment life cycle that should be considered when planning and implementing web applications. • Chapter 2, Setting Up the Application Framework. In this chapter, we begin to imple- ment the web application. This process begins by correctly setting up the environment (that is, installing the correct web server software) and then by creating the initial file structure of the site. In addition to connecting to the database with PHP, we will handle user requests with the Zend Framework and manage HTML code using the Smarty Template Engine. • Chapter 3, User Authentication, Authorization, and Management. This chapter gives the first look at using a database. We look at how to easily manage database data when we implement the user system. Additionally, we look at how a role-based permissions system works and then implement it into the application. • Chapter 4, User Registration, Login, and Logout. Continuing from Chapter 3, this chapter shows how to implement a user registration system. Since this is the first time the book deals with user-submitted data, this chapter looks at how to correctly deal with such data when we create the registration and login forms. • Chapter 5, Introduction to Prototype and Scriptaculous. Since we make heavy use of JavaScript and Ajax in later chapters, we move away from the main application in this chapter while we explore two of the most useful JavaScript libraries available. Prototype helps programmers develop easily maintainable cross-platform JavaScript code, while Scriptaculous simplifies the process of adding appealing visual effects to web pages. • Chapter 6, Styling the Web Application. In this chapter, we step back slightly from the web application in that we focus more on the user experience rather than on the main application features. We first look at implementing various navigational items (which also gives us a first taste of developing custom Smarty plug-ins), and we then complete the chapter by implementing a simple and clean web design into the application. ■INTRODUCTIONxviii 9063CH00CMP3 11/19/07 8:39 PM Page xviii • Chapter 7, Building the Blogging System. This chapter moves on to beginning the implementation of the blogging system. In this chapter, we give users the ability to add, edit, and delete their blog posts. One of the key concepts covered is how to correctly allow user-submitted HTML while keeping the site safe and secure for visitors. • Chapter 8, Extending the Blog Manager. This chapter largely builds on what was implemented in Chapter 7. A comprehensive Ajax example is included in this chapter that we will use to help users manage their blogs. We also integrate an open source What You See Is What You Get (WYSIWYG) editor into a blog post creation form. • Chapter 9, Personalized User Areas. At this point in the book, users can create a new account as well as manage their very own blogs. In this chapter, we make their blogs public in the application. We give each user a public home page within our application web site in which all of their blog posts are shown. This chapter shows how to imple- ment more advanced URL schemes, as well as shows you how to enable users to customize their own experience by managing their own profiles and settings. • Chapter 10, Implementing Web 2.0 Features. Although several of the features we define as Web 2.0 (such as standards compliancy and Ajax) apply throughout web applications, a few concrete features are often defined as being part of the Web 2.0 movement. In this chapter, we will look at some of these, including microformats, web feeds (RSS and Atom), and tagging. • Chapter 11, A Dynamic Image Gallery. In this chapter, we expand the capabilities of the blogging system by allowing users to upload photos for each of their blog posts. This allows us to see how to correctly handle not only file uploads but also image- specific issues, such as dynamically generating thumbnails. • Chapter 12, Implementing Site Search. This chapter is essentially split into two parts: creating search indexes based on user blog posts and then allowing site visitors to search for posts. Indexing data can be a complicated topic, but by using the tools pro- vided by the Zend Framework, the task is made simpler. After implementing the basic search functionality, we extend it to use an intuitive Ajax-based autocompleter, similar to that of Google Suggest. • Chapter 13, Integrating Google Maps. You as a developer can use many freely available web services on the Internet to improve your own web site. In this chapter, we extend the blog capabilities further to allow users to add locations to their blog posts using Google Maps. We create an advanced sample implementation of Google Maps that combines the Google Maps API with our database using Ajax, as well as learn how to manage map data in real-time. • Chapter 14, Deployment and Maintenance. In this, the final chapter, we cover a num- ber of miscellaneous topics related to developing a polished application. This is partly an extension of some functionality implemented in Chapter 2 but also introduces sev- eral new ideas (such as application deployment). ■INTRODUCTION xix 9063CH00CMP3 11/19/07 8:39 PM Page xix Prerequisites A number of third-party applications and libraries are used in this book. We discuss down- loading and installing each of these as required, but for your reference, the following are used: •PHP 5.2.3 •Apache 2.2 on Linux (and its variants) or Windows (earlier versions of Apache may also work) •MySQL 5 or PostgreSQL 8 •Prototype 1.5.1.1 • Scriptaculous 1.7.1 beta 3 •Zend Framework 1.0.2 or newer •Smarty Template Engine 2.6.18 • FCKeditor 2.4.3 (an open source JavaScript WYSIWYG editor) In addition to these applications and libraries, in this book I use several custom PHP classes that I have implemented. Each of these is available in the application source, which can be downloaded as per the following instructions. Downloading the Code All code listings in this book are available from the book’s web site at http://www.myphpbook.com. The source code for this book is also available to readers at http://www.apress.com on this book’s page on the Apress web site. You can download the full web application as it stands at the end of any of the chapters. Additionally, I’ve included a number of bonus add-ons in the source code, including an administration area and a blog post commenting system. Contacting the Author If you have any questions about the code in this book, your first stop should be the book’s web site at http://www.myphpbook.com. This web site contains answers to frequently asked ques- tions as well as various other web development resources. Alternatively, you can contact me directly at quentin.zervaas@apress.com. Please ensure your questions relate directly to the content of the book. It is likely I will publish your ques- tions and the answers on the FAQ section of the book’s web site. ■INTRODUCTIONxx 9063CH00CMP3 11/19/07 8:39 PM Page xx Application Planning and Design In this book we will be creating a blogging web application that will allow us to cover not only all of the different PHP and database considerations involved, but also a number of different Web 2.0 principles (such as Ajax and tagging). The blogging application will allow users to create and manage their own blog. Each user will have their own public page on which their blog posts are published. Figure 1-1 shows how the application will be structured. As you can see, we will use a data- base to store application data, and we will create separate logical areas in the application to manage each feature as required. Additionally, one of the core aspects of Web 2.0 applications is using standards-compliant XHTML and CSS. We will focus on developing clean markup and well-structured JavaScript classes to ensure maximum compatibility and accessibility. Figure 1-1. The basic structure of our web application There are a number of different aspects of the application that we must cover, including database connectivity, template management, user authentication and permissions, and con- sumption of third-party web services. In this chapter we will look at all features of the web application from a “black box” point of view. Each specific feature will be broken down in its respective chapter; here we will look at the application as a whole and discuss various options that need to be considered. In essence, this chapter can be viewed as an informal design document, including an analysis of all required features and a look at design from a high-level. In developing the web application, we will be using both custom-written code as well as various third-party libraries (such as Prototype for JavaScript development, Smarty for template management in PHP, and the Zend Framework for several other features). 1 CHAPTER 1 9063Ch01CMP2 10/20/07 1:47 PM Page 1 What Is Web 2.0? So exactly what defines a web site as being “Web 2.0”? There are many different opinions on this, making it difficult to pinpoint an exact definition; however, some of the features typically associated with Web 2.0 sites are as follows: • Using standards-compliant HTML and CSS. This allows sites to work across many plat- forms and helps with accessibility. This includes the use of microformats to generate friendly HTML that can be used across a variety of platforms (as we will see in Chapter 10). • Using Ajax to provide a rich user interface. By performing trivial operations in the background using XMLHttpRequest, web pages can be more functional and intuitive. ■Note XMLHttpRequest is a JavaScript API that allows a background HTTP request to occur while a user is viewing a web page. This means that the current page can be updated based on a response from the server without the user navigating to another page on the web site. The phrase “making an Ajax request” (or similar) typically refers to performing an HTTP request in the background using XMLHttpRequest. • Sharing data using web feeds and web services. Users like to aggregate many feeds to easily receive content updates from their favorite sites using web feeds (such as RSS or Atom). Additionally, web services can enable one site to use data from other sites (for instance, we will display maps on our site using Google Maps). • Incorporating social networking tools. Blogs and forums can enable users to commu- nicate with each other. While none of these features or aspects of development are new, we use the Web 2.0 term to describe the current generation of web sites that make good use of HTML and CSS while perhaps improving their interface with Ajax and social-networking tools. These are sites that “do things right.” However, that’s not to say that a site that uses any of these features is neces- sarily a good site. Database Connectivity In this application, we will need to save a number of different types of data, including •User accounts •User settings •User-submitted data (such as blog posts, images, tags) We will make use of a database abstraction layer to insert, update, and delete data from the database. This allows us to develop PHP code that will work regardless of the type of underlying database server. Within this book we will make use of MySQL, but if you want to use PostgreSQL instead, it would simply be a matter of changing the application’s database connection settings. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN2 9063Ch01CMP2 10/20/07 1:47 PM Page 2 We will be using the Zend Framework’s Zend_DB class to handle the database abstraction. This is essentially an interface to the PDO extension for PHP 5. We will cover the installation of all required software in Chapter 2. ■Note In this book, all “database code” (i.e., PHP code that interacts with the database) will be self- contained within its relevant class or function. This means that if you want to use a different database abstraction layer (such as PEAR DB, ADOdb, or your own custom layer), it will be fairly straightforward to implement in place of Zend_Db. Web Site Templates One of the reasons PHP has become so popular is that you can easily include PHP code directly within the HTML code you want to output. This makes developing simple and small web applications very easy; however, this typically doesn’t scale well. When an application grows large, it becomes difficult either to add new functionality within a bunch of HTML markup or to change the site design by sifting through the PHP code. To deal with this, we aim to separate our application logic from our display logic. Essen- tially, this means the code that does the hard work (such as processing forms, reading data from the database, or checking user permissions) is performed in one place, while the HTML that will be output to the end user is stored in its own template file. In Chapter 2 we will look at Model-View-Controller (MVC), which is a design pattern specifically describing this separation of application and display logic. We will be using the Smarty Template Engine to manage the display of templates, as this is a very popular and powerful template engine (Smarty will essentially make up the “view” portion of MVC, as we will see in Chapter 2). Web Site Features So far we have only looked at peripheral aspects of web application development, so let’s take a look at some specifics. Let’s look at what the end users of the web application would see. Main Home Page and User Home Page The home page of our web application will display blog posts from all users in a single journal. Registered users will be able to decide whether or not their posts are public and therefore are displayed on the home page. In addition to the main home page, each user will have a public home page. This will dis- play all of their blog posts in a single listing. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN 3 9063Ch01CMP2 10/20/07 1:47 PM Page 3 User Registration We will need to create an account registration tool so new users can sign up and create a blog with our web application. Essentially, this tool will need to do the following: •Validate their details (we will use Ajax to help us with this). •Use CAPTCHA to prevent automated registrations. ■Note A CAPTCHA is typically an image made up of a series of random characters that must be entered by the user when submitting a form. This technique is used to differentiate between humans and computers. It is discussed further in Chapter 4. •Create their account in the database. • E-mail them to confirm their account details. Account Login and Management Once a user has created and confirmed their account, they will be able to log in to their account. This part of the application will allow them to do several things: •Manage their blog (see the next section). •Update their account details (such as their e-mail address). • Log out from their account. User Blogs The blog functionality is the core feature of the application, and we will use it to demonstrate a wide variety of web development and Ajax programming concepts. There are many features we must implement to make a useful blogging system. Users must be able to do the following: •Add, edit, and delete blog posts. •Tag posts. •Upload images to blog posts, and display an image gallery for the user’s account. •Tie geographical data (maps) to the blogs. Web Site Search A keyword search tool is vital in any content-based web site. As such, we need to provide users with a way of searching for any content that appears on the site. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN4 9063Ch01CMP2 10/20/07 1:47 PM Page 4 It needs to be easy to use and efficient, and it must provide meaningful results. To make it easier to use, we will develop an auto-completing search box (similar to that of Google Suggest—see http://www.google.com/webhp?complete=1). Application Management Administration of a web site or application is very important, and it is often overlooked or underdeveloped. An administration area is used to perform day-to-day management of the web application, such as viewing web site statistics or posting news to the site. It often doesn’t receive the attention it deserves because it requires spending development time (which means money) on an area of the site that the target demographic never sees. In Chapter 14 we will look at various strategies for application deployment, management, and maintenance. Because this area is not for “public consumption,” advanced features and a rich interface aren’t as important as they are on the main area of the site, and we won’t be focusing on the development of this area. However, we will look at the features you should consider when developing an administration area for the blogging application. Other Aspects of Development In addition to the specific features of our web application, there are some other aspects we must consider in the development process. No chapters are specifically devoted to any of these topics, but they do form the basis for content that is covered throughout the book. Search-Engine Optimization While we are not looking to achieve high search-engine rankings with this particular web application (after all, it’s not a real-world web site we are developing), we will still aim to develop our code in a way that is optimal for search engines. This means that if you choose to extend the application developed in this book, a strong basis for search-engine ranking will have been formed. Specifically, this means the following: •Using friendly URLs. A friendly URL is basically a URL that doesn’t contain a lot of extraneous characters. For example, if you had a document called “About Us,” a URL such as http://www.example.com/about-us would be user friendly, while a URL such as http://www.example.com/documents.php?id=1234 would not be so friendly. •Correctly using HTML markup (such as headings, paragraphs, and tables). •Correctly using HTTP status codes and content types (where relevant). PHPDoc-Style Commenting All classes we develop will be commented using PHPDoc-style comments, allowing us to easily build API documentation for all our classes. PHPDoc is based on Sun’s Javadoc system, which is a simple method of commenting all functions, arguments, variables, and packages so developers can easily reuse them. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN 5 9063Ch01CMP2 10/20/07 1:47 PM Page 5 While this is not essential for the development of our web application, it is a good habit to get into when developing. Additionally, you may find it useful when following code examples in this book to have a PHPDoc comment block before each function. ■Note The code displayed in this book typically won’t include any PHPDoc comments since listings will be described in the text; however, they will be included in the downloadable code for this web application where possible. PHPDoc works by placing a block of comments before each function, class, or variable definition. It is not mandatory in all situations—only where you feel it is necessary. Each comment block begins with a description, and then is followed by a series of one or more optional parameters. For example, when adding PHPDoc comments to a function, you specify the input parameters and return value data. Obviously, the PHPDoc comments you would write for a variable definition would contain different information. The following code shows an example of a PHPDoc comment for a simple user-defined function: The first thing to note is how the block of comments begins. The /** token indicates to the PHPDoc parser that a PHPDoc comment block is beginning. The first line of the block is a short description. My own personal preference here is to simply use the name of the function, class, or variable. The next section in the comment block is a longer description. Here I try to describe what the function, class, or variable does from a black-box perspective. That is, what it does, not how it works. Any specific functionality considerations or funky logic that takes place is dealt with in standard comments within the code. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN6 9063Ch01CMP2 10/20/07 1:47 PM Page 6 ■Note Although it is not required, the usual convention is to include an asterisk at the beginning of each line of the /** … */ block. This is primarily to improve readability and to easily identify entire PHPDoc blocks. The final section of the comment block contains the various PHPDoc parameters used by the parser to link the API documentation together better and to provide you with useful docu- mentation. Each parameter begins with @, directly followed by the name of the parameter. Following that is the information required for that particular parameter. In this example, you can see the @param and @return parameters. @param is used to specify aspects of the function arguments: first, the type of argument (in this case, our first argument is a string); next, its name (which in this case is $name); and finally, a brief description of what the input data should contain. The @return parameter is used to give information about the data returned from the function: the type of data is specified, followed by a brief description of what the return data contains. For more information about phpDocumentor, read the “phpDocumentor Guide to Creat- ing Fantastic Documentation” at http://www.phpdoc.org/tutorial.php. Security We will be looking closely at the security of our web site, as this very important aspect of web development is often overlooked or implemented incorrectly. For instance, we will focus on making sure attacks such as SQL injection, cross-site scripting (XSS), and cross-site request forgeries (CSRF) do not occur. This is especially important in sites that not only make use of JavaScript and Ajax, but also make heavy use of user-submitted data. We achieve this by correctly filtering submitted data while correctly “escaping” user- submitted data when it is returned to users’ browsers. Application Logging An aspect of development that ties in closely to both the security and performance considera- tions is that of logging. We will maintain a log file within our application to record significant events. For example, we will record a log entry whenever somebody tries to log in but provides incorrect information. Maintainability and Extensibility In addition to using some well-known third-party classes and libraries, we will also be devel- oping our own custom classes in such a way that they can easily be expanded upon in the future. In the next section, we will consider the use of unit testing. Note that unit testing aids greatly in developing applications that can easily be extended (as well as aiding in extending the application); however, this exceeds the scope of the book. You should keep unit testing in mind for your own future application development if you don’t already use it. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN 7 9063Ch01CMP2 10/20/07 1:47 PM Page 7 Some of the ways we will make our code easily maintainable and extensible include •Using a template engine to separate application logic from display logic. •Using database abstraction to handle database server interaction. •Making heavy use of the object oriented programming (OOP) features in PHP 5 to organize code. Version Control and Unit Testing There are two other reasonably important aspects of the web development process that we won’t be covering in this book, but that you should at least be aware of: version control and unit testing. While they are important, they don’t directly concern the concepts and libraries we will be looking at in this book. Almost all web development projects I undertake use some form of version control (typi- cally Subversion). This allows me to track any and all changes made to the files, and it also aids with code deployment. If you’re not familiar with Subversion, I encourage you to use it for your own development projects. You can download it from http://subversion.tigris.org, and you can download the free O’Reilly book on Subversion from http://svnbook.red-bean.com. Unit testing is another important tool that should be used when developing your own web sites (or when developing libraries you can use in multiple applications). A unit test is a script designed to test the functionality of a class (or of an entire package, or just individual methods inside a class). You can perform automated testing using multiple unit tests, which will assist in finding regression bugs if they occur (that is, bugs that occur incidentally as a result of changing code that previously worked). All of the code provided in this book has been tested, so including unit tests with all of the code would be somewhat redundant. For your own unit testing, you can use a package such as Simple Test (http://www.lastcraft.com/simple_test.php). Summary In this chapter, we have looked at the required features of our Web 2.0 application, and briefly at how they will be implemented. From here on in, we will work on the actual application development, starting with the initial setup in Chapter 2. CHAPTER 1 ■ APPLICATION PLANNING AND DESIGN8 9063Ch01CMP2 10/20/07 1:47 PM Page 8 Setting Up the Application Framework In the last chapter, we covered the features that we will be implementing in our web applica- tion. Before we can get started on these features, however, we must set up our development environment. In this chapter, we will be completing a number of tasks, beginning with setting up the required server software. Following that, we will create a filesystem structure that will serve as the basis for our web application. There are a number of different types of files in our web application, and we will keep them as organized as possible. For example, we need one directory for the web server to use as the base directory from which to serve files, we need another directory to hold custom and third-party PHP libraries, and we need another to hold web site templates. Next, we will set up the database. The actual creation of database schema and various queries will be covered in later chapters, but here we will write the PHP code required to con- nect to the database. Then we will write code to handle client requests to our web site. We will use the Model- View-Controller design pattern to handle requests, and we will look more closely at this model in this chapter. Finally, we will install the Smarty Template Engine into our application and set up some basic templates. We will expand on these templates as we continue through this book, but the material provided here should explain the basics of Smarty. Also in this chapter, we will create a configuration file for our web application. This file allows you to deploy the web applications to different servers easily. For example, we will be storing database connection settings in this file, meaning that you can switch databases or the database password simply by modifying this file. Web Server Setup Setting up a web server correctly can be a complex task, and I cannot cover all scenarios in this book. However, I will cover the setup used for all code in this book. I have used a somewhat typical LAMP setup (Linux/Apache/MySQL/PHP), broken down as follows: •Operating system: Linux •Web server: Apache 2.2 •Database server: MySQL 5 •Server-side scripting language: PHP 5.2.3 9 CHAPTER 2 9063Ch02CMP4 11/4/07 12:23 PM Page 9 Operating System The code in this book has been developed and tested on Linux, FreeBSD, and Microsoft Win- dows XP. There are no differences in code required for any of these platforms. Note also that references to Linux can typically also include similar platforms such as FreeBSD and Mac OS X. For Windows there are slight differences in the configuration of the web server, as well as in the application configuration file we will develop later in this chapter. Each of these differ- ences is noted in the relevant places. Installing the Apache HTTP Server Apache HTTP Server 2.2 is the web server of choice for this book—it is the latest stable release of Apache at the time of writing. This web server is available for Linux and Windows. Since I can’t guarantee all PHP code in this book will work correctly on IIS, you should use Apache if you are using Windows. Alternatively, you may choose to use an older version of Apache (such as 1.3 or 2.0). There should be no problems with doing so, but this cannot be guaranteed. You can download Apache 2.2 from http://httpd.apache.org. We will use a typical con- figuration, enabling all modules (including mod_rewrite, which we require in order to use Zend_Controller). You may also wish to include extra options that aren’t included by default (such as SSL). To install Apache on Windows, you can download the installer from the Apache web site, which will take you through the installation step by step. The easiest way to install Apache (as well as PHP and MySQL) on Linux is to use the packaging system that comes with your operating system (such as Ports on FreeBSD). However, if you do not use a packaging system, you can install Apache 2.2.4 on Linux by downloading the httpd-2.2.4.tar.gz file (or a newer version if one is available) and using the following commands: # tar -zxf httpd-2.2.4.tar.gz # cd httpd-2.2.4 # ./configure --enable-modules=all # make # make install Note that by default this will install Apache into the /usr/local/apache2 directory. Assuming each of these steps were successful, the Apache files should now be installed. You can configure the web server by editing the /usr/local/apache2/conf/httpd.conf file. Once that has been done, you can start the web server by issuing the following command: # /usr/local/apache2/bin/apachectl start If there is an error in the configuration, you will be notified. Alternatively, you can issue the configtest command instead of start with apachectl to ensure that the configuration is correct. We will look at the Apache configuration required for our web application in the “Config- uring the Web Server” section later in this chapter. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK10 9063Ch02CMP4 11/4/07 12:23 PM Page 10 Installing MySQL 5 Next you must install MySQL 5. You can download it from http://dev.mysql.com/downloads. Just like Apache, the Windows version of MySQL 5 is very straightforward to install as it uses an installer. If you are installing on Linux, it is recommended that you download the binary distribution, as MySQL can be a slow program to compile from source. I recommend installing MySQL to the /usr/local directory, although you may prefer a different setup. Assuming you have downloaded the 5.0.41 version, the commands to install MySQL on Linux are as follows: # cd /usr/local # tar -zxf /path/to/mysql-5.0.41-linux-i686.tar.gz # ln -s mysql-5.0.41-linux-i686 mysql # cd mysql # ./configure Setting up the server using a symbolic link to /usr/local/mysql allows you to upgrade the server version in the future much more easily. Once you have run the configure script, you can start the MySQL server with the following command: # ./bin/mysqld_safe & Note that this assumes you are already in the /usr/local/mysql directory. It is now recommended that you add /usr/local/mysql/bin to your system path so you can easily load MySQL programs when required (such as mysql, mysqladmin, and mysqldump). Installing PHP 5.2.3 The code developed in this book is designed to run on PHP 5.2.3 (or later). We will be using many PHP 5-specific features, so you will not be able to run the code in this book on PHP 4. Strictly speaking, you can use a version of PHP 5 earlier than 5.2.3, but it is best to use the lat- est available version. Note that the Zend Framework requires a minimum PHP version of 5.1.4. Download PHP 5.2.3 (or later) from the PHP web site (http://www.php.net/downloads.php), and use the following commands to compile a fresh version of PHP. Note that these commands only include the minimum options required for compatibility with the code in this book. # tar -zxf php-5.2.3.tar.gz # cd php-5.2.3 # ./configure --with-apxs2 \ --with-gd --with-curl \ --with-mysql --with-pdo-mysql \ --with-jpeg-dir --with-png-dir \ --with-freetype-dir --with-zlib # make # make install Once these commands have successfully executed, PHP should be compiled and installed, including the PEAR library in /usr/local/lib/php. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 11 9063Ch02CMP4 11/4/07 12:23 PM Page 11 ■Note Please ensure that your version of PHP is built with the GD library enabled, as we will use it in this book for generating CAPTCHA images (Chapter 4) and for resizing uploaded images (Chapter 11). When you run the make install command, the Apache httpd.conf file will be modified to load the PHP library; however, you may still need to add the following lines to ensure that Apache recognizes files with the extension .php as PHP files: AddType application/x-httpd-php .php AddType application/x-httpd-php-source .phps This second line is optional, but it is included with the PHP documentation, so I have included it here. You should also modify the DirectoryIndex directive in httpd.conf so index.php files are treated as index files. You can simply add index.php to this command so it looks something like the following: DirectoryIndex index.php index.html Application Filesystem Structure Let’s now take a look at the filesystem structure we will be using for the web application. The precise naming and organization of the directories in the web application is not in itself criti- cal—it is simply important that everything is easy to find and manage. In this book, we will develop the entire application within a directory called /var/www/ phpweb20 (with “phpweb20” referring to the title of this book). You can, of course, use whichever directory on your own server that you choose, although we will refer back to this directory name on several occasions. Web Root Directory We need to define a root directory for the web server to access. This is the directory specified in the Apache configuration, and it is where Apache looks for files when a user requests a page in the web site. I will call this directory htdocs (the full path is /var/www/phpweb20/htdocs). Most of the files in our application will exist outside of this directory (such as PHP classes and web site templates), which prevents users from directly accessing these files. Data Storage Directory Next, we need a directory for storing application data (that is, data in addition to that in the database). Here we will store log files (both from Apache, and those we create ourselves), files uploaded by users, as well as any other temporary data. I will call this directory data, and it will contain a number of subdirectories for each of the different types of data stored. These subdirectories are logs, uploaded-files, and tmp. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK12 9063Ch02CMP4 11/4/07 12:23 PM Page 12 PHP Classes Directory We next need a directory called include, which will be used to store all PHP functions and libraries. Any third-party scripts we use (such as Smarty) will also be stored in this directory in addition to our own code. Application controllers (scripts that define the different actions users can perform on the web site) will be stored in a directory called Controllers in the include directory. When we create the Apache virtual host for our application (in the “Configuring the Web Server” section of this chapter), we will include the include directory in the PHP include_path directive, so our application will know where to find this code. Templates Directory Finally, we need a directory to hold all the web site templates. We could put these directly inside either the htdocs directory or the include directory; however, they are not PHP code (although they do contain display logic), and they shouldn’t be directly accessible (although they do contain HTML markup). We will put them in a directory called templates. Full Directory Structure Putting this all together, the directory structure of our web application will look like this: / |- /data | |- /logs | |- /uploaded-files | |- /tmp |- /htdocs |- /include | |- /Controllers |- /templates To create this structure in Linux, you would issue the following commands: # mkdir /var/www/phpweb20 # cd /var/www/phpweb20 # mkdir data # mkdir data/logs # mkdir data/uploaded-files # mkdir data/tmp # mkdir htdocs # mkdir include # mkdir include/Controllers # mkdir templates When you view the directory listing, you should see the following: # ls data/ htdocs/ include/ templates/ CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 13 9063Ch02CMP4 11/4/07 12:23 PM Page 13 ■Note You will need sufficient permissions to create this directory structure. You may instead prefer to keep the code for this book in your home directory. I chose to use /var/www since it is a commonly used area on web servers to hold web sites, and it is short and easy to refer back to when required. (On a typical Windows setup, you won’t need any special permissions to create the required directories.) Installing the Zend Framework The Zend Framework is an open-source library of PHP 5 components that can be used to solve tasks that commonly arise in everyday web development. It is actively contributed to by a large number of developers, and it is backed by Zend (the company that writes the Zend Engine, which has powered PHP since PHP 4). We will be using this framework in our applica- tion, as it allows us to focus on developing a Web 2.0 application, rather than getting bogged down in the details of building an entire application infrastructure. These are some of the components we will be using: • Zend_Auth and Zend_Acl: Used to authenticate users when they try to log in and to check their permissions (see Chapter 3) • Zend_Controller: Used to handle client requests and direct the requests to the appro- priate classes (see later this chapter) • Zend_Db: Used to interact with the application MySQL database • Zend_Mail: Used to send e-mails to users • Zend_Validate and Zend_Filter: Used to check and sanitize user-submitted data in forms • Zend_Search: Used for full-text searching We will use more components, but, as you can see, we will be making heavy use of the framework. Download the Zend Framework from http://framework.zend.com. In this book, I used version 1.0.2, but you should use the most up-to-date version available. Use these commands to extract the library to the include directory: # cd /var/www/phpweb20 # wget http://framework.zend.com/releases/ZendFramework-1.0.2/ ZendFramework-1.0.2.tar.gz # tar -zxf ZendFramework-1.0.2.tar.gz # mv ZendFramework-1.0.2/library/Zend include The last command moves the actual library files from the extracted archive into the appli- cation directory. The additional files in the archive include documentation and unit testing and are not really required. You may wish to remove the downloaded files once you have installed the framework, as they are no longer needed. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK14 9063Ch02CMP4 11/4/07 12:23 PM Page 14 Configuring the Web Server A typical development setup is to use your normal computer (such as your Windows or Mac OS machine) to write your code, while running the web server on another server. In such a case, you need to access the web server over a network. For example, I use a Windows machine for my day-to-day work, while my web server is a FreeBSD machine elsewhere in the office. ■Tip I aim to keep my development web server configured identically to my production server, as this helps to eliminate any unforeseeable issues that may arise when deploying my code (such as different versions of linked libraries). For the purposes of this book, I assume the web application is accessible using the web address http://phpweb20. In order to access my web server using this hostname, I make a fake DNS entry in my Windows host file so my browser will resolve the phpweb20 hostname to 192.168.0.80. This is the entry I add in my Windows hostname file (c:\windows\system32\ drivers\etc\hosts in Windows XP): 192.168.0.80 phpweb20 ■Note Setting up a host as described here is not related to the development of the web application, but rather allows you to access it in your web browser. Creating fake hostnames is a simple trick for develop- ment purposes, eliminating the need for a DNS server or a real domain. Once you deploy your application live, you will need to use a real hostname so other people can access your web site. If you have control over a real DNS server, you may instead prefer to create your own hostname. (Just keep in mind that I continually refer to phpweb20 throughout this book.) ■Note You could use IP-based hosting, which would allow you to simply access http://192.168.0.80. Since name-based hosting in Apache is arguably the most common setup, I’ve chosen instead to use the method described previously (that is, setting up a fake hostname). Obviously, using a real hostname is better, but I’ve tried to simplify matters by not requiring it for this book. Creating a Virtual Host in Linux To configure the web server, we must first create the entry for Apache. I like to store this configuration data in its own file within my application directory, and then use the Include directive from the main Apache httpd.conf file. This means changes can be made to the local configuration, and the main configuration will pick up the changes automatically when the server is restarted. Listing 2-1 shows the contents of the /var/www/phpweb20/ httpd.conf file. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 15 9063Ch02CMP4 11/4/07 12:23 PM Page 15 Listing 2-1. Virtual Host Configuration for Apache on Linux (httpd.conf) ServerName phpweb20 DocumentRoot /var/www/phpweb20/htdocs AllowOverride All Options All php_value include_path .:/var/www/phpweb20/include:/usr/local/lib/pear php_value magic_quotes_gpc off php_value register_globals off In your main httpd.conf file (commonly found in /usr/local/apache2/conf/httpd.conf for a default Linux install), you would add the following line: Include /var/www/phpweb20/httpd.conf ■Note For this VirtualHost directive to work, you must have previously included the NameVirtualHost 192.168.0.80 in your main web server configuration before loading this virtual host. There may be other directives you wish to add to your configuration, but this is a pretty standard configuration. It allows you to override configuration per directory as required with a .htaccess file (because of the AllowOverride directive), and it tells the PHP module where to look for included files. In this example, it will first look in the current directory, then in the /var/www/phpweb20/include directory, then finally in the PEAR library. Note that the specific location of PEAR may change depending on your Linux distribution or operating system. ■Note As a general rule, the PHP register_globals setting should be set to off. If this setting is on, the form, URL, session, and cookie variables will be made into global variables, which is generally a bad thing. The problem is that for many years the default was to have this setting enabled, so some web servers will have it enabled while others won’t. All code in this book will work with register_globals turned off, just as all code you develop should (unless there’s a particular reason to do otherwise). The same applies to the magic_quotes_gpc setting, which is used to automatically escape submitted data. While it is not necessar- ily a bad thing in general, all the code we develop will escape data as required; this setting should not be relied upon and is therefore disabled. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK16 9063Ch02CMP4 11/4/07 12:23 PM Page 16 Creating a Virtual Host in Windows Creating a virtual host in Windows is similar to the process in the previous section, except that the paths must be adjusted. Note also that the PHP include_path directive uses a semicolon as the separator rather than a colon, since a colon is used to indicate a drive label. Listing 2-2 shows the Windows equivalent of Listing 2-1. Once again, you will need to include it in the main web server configuration file, typically found in C:\Program Files\Apache Software Foundation\Apache2.2\conf\httpd.conf on Windows. Listing 2-2. Web Server Configuration for Apache on Windows (httpd.conf) ServerName phpweb20 DocumentRoot "c:/www/phpweb20/htdocs" AllowOverride None Options All php_value include_path ".;c:/www/phpweb20/include;c:/program files/php/pear" php_value magic_quotes_gpc off php_value register_globals off Restarting Your Web Server After making changes to your web server configuration, you must restart your web server. In Linux, the typical way to do this is with the following command: # apachectl restart In Windows, you can restart Apache by going to Control Panel ➤ Administrative Tools ➤ Services and selecting restart on the Apache2 service. Once your server has been restarted, you should be able to access http://phpweb20 directly in your browser (or by entering the server IP address directly, although if you’re using a name-based virtual host system as described previously, this will not show files from the application directory). Setting Up the Database The next thing we need to do is create the MySQL database that we will be using in the web application. We will call this database phpweb20, and we will create a user called phpweb20 to access this database. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 17 9063Ch02CMP4 11/4/07 12:23 PM Page 17 To create the database, load the MySQL client program (mysql) and issue the CREATE DATABASE command as shown here: # mysql -u root Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 to server version: 5.0.27-standard mysql> CREATE DATABASE phpweb20; Query OK, 1 row affected (0.00 sec) mysql> use phpweb20 Database changed Next, we must create the phpweb20 user and assign a password to the account: mysql> grant all on phpweb20.* to phpweb20@localhost identified by 'myPassword'; Query OK, 0 rows affected (0.01 sec) ■Warning I use the password myPassword for this book, but if you plan on deploying this application and using it as a real-world site, it is essential that you use a different password than the one created here, as anybody who has read this book will be able to access your database if you don’t. To ensure that the database and user have been correctly created, try exiting from the MySQL client and connecting using the new details. To do so, type the following command and then enter your password when prompted: # mysql -u phpweb20 -p phpweb20 We will next take a quick look at handling client requests, and then we will return to our MySQL database and look at the PHP code for accessing the database. Using the Model-View-Controller Pattern The Model-View-Controller (MVC) design pattern is a commonly used method of designing web applications. In simple terms, it separates the presentation of the application from the underlying application logic. The three parts of the pattern work as follows: • Model: This represents the application logic. It performs the “hard work” of the applica- tion, such as interacting with the database, processing credit card transactions, or sending e-mails to users. • View: The view represents the user interface. In the case of our application, this will typically be HTML code. We will be using the Smarty Template Engine to manage the view aspect of our application. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK18 9063Ch02CMP4 11/4/07 12:23 PM Page 18 • Controller: The controller joins the view to the model. That is, it responds to events (such as when a user submits a web form), potentially updating the state of the applica- tion by interacting with the model. Figure 2-1 shows how the three parts of MVC fit together in a typical web application. Figure 2-1. How the Model-View-Controller design pattern fits together in our application We will be using the Zend_Controller class to handle the controller aspect of MVC. All user requests will be handled by this class, which will then result either in a new web page being displayed to the user (using Smarty), or in some update to the application (such as a new blog post being written to the database). Separating Application Logic from Presentation Logic To better demonstrate how MVC works, let’s use the example of a simple news-article publish- ing system both using MVC and not using it. The most basic way to retrieve a series of news articles from a database and display them would be to create a PHP script that connects to a database, queries the database, then loops over the results and outputs some HTML for each article. The following code shows what such a script might look like.

News Articles

headline ?>

body ?>

CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 19 9063Ch02CMP4 11/4/07 12:23 PM Page 19 In the preceding script, the application logic is the code that connects to the database server and retrieves the rows from the news table. The presentation logic is the HTML code that outputs the articles. The problem with a script like this is that it can be hard to maintain, especially if you change the way the news system works (for instance, if you wanted to rename the table to news_articles). While it appears that you only need to change the code in place, consider what would happen if you wanted to display your news articles on other pages also. You would need to duplicate this code and then maintain it accordingly. Now consider using the MVC pattern to implement this code. There are essentially two key changes that would be made. The first would be to move the code that retrieves articles from the database into a reusable component (either a PHP class or function). We would then call this new function to retrieve the articles so they could be output using HTML. In MVC terms, this new class or function is the model. The second change would be to separate the call to retrieve the articles from the actual HTML. While this change isn’t quite as important as the first change, it is still important as it allows you to change your HTML code without having to worry about how the data used in the HTML is generated. In MVC terms, this is separating the controller from the view. Figure 2-2 shows how the previous code would be structured to use MVC. Figure 2-2. The news article example represented in MVC In the MVC version, you would effectively have three files. The model: CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK20 9063Ch02CMP4 11/4/07 12:23 PM Page 20 The controller: ■Note display_template() is a fictional function that represents some mechanism used to render templates. And the view:

News Articles

headline ?>

body ?>

While this example is fairly trivial, considering how the news articles are maintained (that is, inserted, edited, or deleted) will highlight the advantages of MVC. It is a nightmare to maintain code that mixes SQL insert statements directly within the HTML output for the cor- responding page. Directing All Requests to index.php To implement our application using MVC, we will use the Zend_Controller class. First, though, we must alter our web server configuration to direct all page requests to Zend_Controller, even if the requested location is not a real file on the filesystem. All requests to files that do exist on the filesystem (such as our images and CSS files) will be handled normally by Apache; however, all other requests will be handled by the application bootstrap file (which will be located in /var/www/phpweb20/htdocs/index.php). The directives in Listing 2-3 should be placed in a file called .htaccess inside ./htdocs. Note that these could be placed in the httpd.conf file we created earlier, but doing it here allows us to make changes without restarting the web server. The RewriteRule directive in Listing 2-3 routes any request that doesn’t correspond to an actual file or directory through index.php. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 21 9063Ch02CMP4 11/4/07 12:23 PM Page 21 ■Note The AllowOverride directive in the Apache configuration we created earlier allows us to change the configuration within a .htaccess file. Listing 2-3. Routing All Web Site Requests Through the index.php File (.htaccess) RewriteEngine on RewriteCond %{SCRIPT_FILENAME} !-f RewriteCond %{SCRIPT_FILENAME} !-d RewriteRule ^(.*)$ index.php/$1 The first line in Listing 2-3 enables mod_rewrite for the directory in which .htaccess is located (including subdirectories). The second and third lines set up conditions for rewriting the request to index.php. The second line says “if the requested file doesn’t correspond to a file relative to the web root then use the rewrite rule,” while the third line says the same thing but for nonexistent directories. The final line is then executed if either of the conditions is satisfied. The requested filename is made available to index.php by adding it to the request string. Introduction to the Zend_Controller Class Let’s now begin with the Zend_Controller class. Since we have already installed the Zend Framework, we can access this class easily. You will learn how to use this class in this section. First, we will create the index.php file in the ./htdocs directory (to which requests are routed using mod_rewrite). This file will drive our entire web site. Every single user request will be handled by this file (aside from requests for files such as images or CSS). This file is the bootstrap file. ■Note From here onwards in the book, when I use the filesystem path ./ I am referring to /var/www/ phpweb20.For example, the path /var/www/phpweb20/htdocs/index.php will now be referred to as ./htdocs/index.php. All this bootstrap file needs to do is load and initialize the Zend_Controller_Front class, then call the dispatch() method, which will call the necessary code to handle the request. Note that Zend_Controller_Front is a singleton class, meaning that only one instance of the class may exist. This is why the getInstance() method is used to instantiate it. Listing 2-4 shows the contents of the index.php file. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK22 9063Ch02CMP4 11/4/07 12:23 PM Page 22 Listing 2-4. Handling Client Requests Using Zend_Controller (index.php) setControllerDirectory('../include/Controllers'); $controller->dispatch(); ?> We will use the registerAutoload() method from Zend_Loader to automatically load Zend Framework classes. Doing this means you don’t have to use require_once for any of the Zend Framework classes you use (apart from Zend_Loader). ■Note If you decide to use Zend Framework in any other apps that already use PHP’s class autoloading, you will either have to modify your autoloader or manually include the Zend Framework library files. The file- names correspond to classes simply by replacing underscores in the class name with a slash and appending .php.For instance, Zend_Controller_Front can be included using require_once('Zend/Controller/ Front.php'). How Requests Work with Zend_Controller If you were to run the code in Listing 2-4 (by visiting http://phpweb20), nothing useful would happen—an error would be shown. At this point, we need to look at how requests work with Zend_Controller. ■Note Depending on your PHP configuration, errors may in fact be logged to the filesystem rather than displayed on screen, so be sure to look for a log file if you encounter unexpected behavior but no error mes- sages. We will deal with error handling (such as “404 File Not Found”) in Chapter 14. In Listing 2-4 we called the setControllerDirectory() method. This is used to specify the directory that holds our web application’s controllers—that is, classes that are used to handle requests to the application. For example, you might have a controller called news, used for displaying both a summary of all news articles on your site, and for displaying individual articles. To create this controller, you would create a class called NewsController and save it in the Controllers directory (./include/Controllers/NewsController.php). CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 23 9063Ch02CMP4 11/4/07 12:23 PM Page 23 When Zend_Controller routes a user request, it automatically looks in the controller directory for a file called NameController.php, where Name corresponds to the controller name specified. The name is automatically capitalized, meaning a controller named news corre- sponds to a file called NewsController.php. ■Note The typical naming convention in PHP (including in the Zend Framework) is to capitalize each word in a class name (regardless of whether each word is separated by an underscore). Conversely, class meth- ods use camel caps, meaning all words in the method name begin with an uppercase letter except for the first word. As an extra caveat, I prefer to capitalize all words for static class methods. This lets me know instantly that the method is static without needing to understand the function. Other conventions include using two underscores for PHP’s magic method (these names are built into PHP, such as __get(), __set(), __unset(), and __isset()), while method names beginning with one underscore indicate private or protected methods (which can only be called with the class or package respectively). To then access this controller in your application, you would visit http://phpweb20/news. To view a specific news article, you might create an action called display, which would be accessed at http://phpweb20/news/display. To create this action, you would define a method called displayAction() inside of NewsController. Figure 2-3 shows how the URL is broken down to correspond to a controller class name and an action handler function within that class. Figure 2-3. Breaking down a URL into the controller and action The following code demonstrates this. We won’t be using this particular class in our appli- cation, but we will be creating similar classes. ■Note In addition to displaying the string echoed in the preceding function, an error message would also be displayed due to the way Zend_Controller automatically displays templates. We will look at this more closely later in the “Automatic View Rendering with Zend_Controller” section of this chapter. If we were to include this controller in our application (by saving it to ./include/ Controllers/NewsController.php), we would visit http://phpweb20/news/display to display the “New article details” text. In this URL, news is the controller, and display is the action. The default controller and action are both index. Here are some examples: • http://phpweb20 is equivalent to http://phpweb20/index, as is http://phpweb20/index/index • http://phpweb20/news is equivalent to http://phpweb20/news/index Creating the IndexController At this point in our application development, we must create a controller for the root of the site. That is, a controller called index that defines an action called index. Listing 2-5 shows the contents of IndexController.php, which we will save to the ./include/Controllers directory. ■Note As mentioned previously, Zend_Controller looks for the controller file by capitalizing the first let- ter of the controller name and appending Controller.php to it. So in this case, the index controller code belongs inside a file called IndexController.php. Listing 2-5. The Index Controller,Which Is Used for the Web Application Home Page (IndexController.php) CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 25 9063Ch02CMP4 11/4/07 12:23 PM Page 25 While this particular controller doesn’t yet do anything useful, we will be adding to it, as well as creating new controllers as we move on in this book. In fact, not only will we extend this controller, but we will add functionality that will extend to all controllers. To allow for this, we will extend the Zend_Controller_Action class in a new class called CustomControllerAction. Listing 2-6 shows the contents of CustomControllerAction.php, which should be stored in the ./include directory. Listing 2-6. The Controller Action That All of Our Application Controllers Will Extend from (CustomControllerAction.php) db = Zend_Registry::get('db'); } } ?> At this stage, we have only defined the init() function, which is automatically called by Zend_Controller_Front when a controller is loaded. Currently it simply fetches the database handle from the application registry and stores it in the db property. This allows us to refer to $this->db from any of our controllers. If we want an init() function in any of the child classes, we must also call parent::init() from that class so that the init() function in Listing 2-6 is also called. ■Note Listing 2-6 relies on the application database connection being in the variable registry that we will use Zend_Registry to manage. We create the database connection and look at the Zend_Registry com- ponent in the “Connecting to the Database” section. We now need to modify our IndexController class to extend CustomControllerAction instead of Zend_Controller_Action. Listing 2-7 shows the updated code for IndexController.php. Listing 2-7. Modifying the Index Controller to Use the New Controller Action (IndexController.php) Defining Application Settings Before we go any further in developing our application code, we’re going to define some appli- cation settings. We will store these settings in a file called settings.ini, and we will use the Zend_Config_Ini class to access them. ■Note Zend_Config also allows storage of settings in an XML file instead of an Ini file. The Zend_ Config_XML class would be used instead of Zend_Config_Ini.If you prefer, you can use the XML solution instead, since it makes no real difference to the functionality of the application. The initial settings we will be storing are the database connection details and application path settings. We will not be implementing any mechanism to update these settings—if you want to change application settings, you will need to edit the values in this file. We will add further settings to this file as required. Listing 2-8 shows the initial application settings we will be using (/var/www/phpweb20/ settings.ini). Update any of these values as you require. Listing 2-8. The Initial Application Settings (settings.ini) [development] database.type = pdo_mysql database.hostname = localhost database.username = phpweb20 database.password = myPassword database.database = phpweb20 paths.base = /var/www/phpweb20 paths.data = /var/www/phpweb20/data paths.templates = /var/www/phpweb20/templates logging.file = /var/www/phpweb20/data/logs/debug.log The first line of this file defines a section in the file. It is possible to have multiple configura- tions in the same file, and I have specified a section called development. You might also define sections called staging and production in the same file, allowing you to use different database details or a different path without having to edit the file when you deploy the application. Initially the logging file will not exist, but assuming the write permissions are correctly set on the logs directory, debug.log will automatically be created when required. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 27 9063Ch02CMP4 11/4/07 12:23 PM Page 27 ■Note You must define at least one section in a configuration file when using Zend_Config, as the section to load must be specified when the file is loaded. Once settings.ini is set up, we need to load it in the index.php file using the Zend_ Config_Ini class. Listing 2-9 shows an updated version of index.php, now including both the request handling code, as well as the code to load the configuration. Listing 2-9. Using the Zend_Config_Ini Class to Load the Application Settings (index.php) setControllerDirectory($config->paths->base . '/include/Controllers'); $controller->dispatch(); ?> ■Tip In Chapter 14 we will implement error handling in this code to deal with fatal errors (such as being unable to connect to the database server). In the meantime, Zend_Controller will suppress these errors, making potential debugging difficult. You may wish to add $controller->throwExceptions(true) to index.php after $controller has been created (and before the request is dispatched) to make identifying any potential errors easier. As you can see, the Zend_Config_Ini class is instantiated, passing the settings filename as the first argument and the settings section as the second argument. Following this, we use the Zend_Registry class. This allows us to store the $config object in a global registry so we can easily access this object again throughout the script’s execution without needing to reinstantiate Zend_Config_Ini. This is a technique we will also use with the database connection. Now, to access any of our configuration variables, we can simply use $config->key. For instance, to access the database.password setting, we would use $config->database->password in our code. Note that we have also updated the setControllerDirectory() call to use the path we set in the config to find the controller classes for Zend_Controller. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK28 9063Ch02CMP4 11/4/07 12:23 PM Page 28 Connecting to the Database Now that we have all of our database settings stored in the $config variable, we can easily cre- ate our database connection. For this, we use the Zend_Db class. We must first build an array with the database connection settings, and then call Zend_Db::factory() to find the appropri- ate database handler. What does this mean, exactly? In our configuration, we specified the database type as pdo_mysql, and the factory() method will find the appropriate handler for this database type. If you wanted to use PostgreSQL instead, you could simply update the database.type value in settings.ini to pdo_pgsql, and if you had this driver installed with your PHP installation, it would use that one instead. The following example code will connect to a database using the pdo_mysql driver: 'localhost', 'username' => 'phpweb20', 'password' => 'myPassword', 'dbname' => 'phpweb20'); $db = Zend_Db::factory('pdo_mysql', $params); ?> Note that I have hard-coded the connection settings in this example; the code in our applica- tion will call the appropriate settings we defined previously. ■Note Zend_Db doesn’t initiate a connection to the database until a query is actually executed, so, techni- cally speaking, in this example no connection is actually made. Our next step is to include the database connection code in our index.php file—there are two key additions we must make. The first is to fetch the connection values from $config instead of hard-coding them. The second is to write the $db object to the Zend_Registry so we can use it throughout our application. Listing 2-10 shows the updated index.php file, this time connecting to the database and writing the $db object to the registry. Listing 2-10. The index.php File, Now Connecting to the Application Database (index.php) $config->database->hostname, 'username' => $config->database->username, 'password' => $config->database->password, 'dbname' => $config->database->database); $db = Zend_Db::factory($config->database->type, $params); Zend_Registry::set('db', $db); // handle the user request $controller = Zend_Controller_Front::getInstance(); $controller->setControllerDirectory($config->paths->base . '/include/Controllers'); $controller->dispatch(); ?> Testing the Database Connection Now that we have written the database connection code, it is best to ensure that the connection actually works. As mentioned previously, a connection is not actually made to the database server until a query is executed, so to test the connection we need to execute a basic SQL query. Add an extra line of code after creating the $db object in index.php as follows: $db->query('select 1'); If you visit http://phpweb20 now, an error will be shown if the connection to the database could not be made (such as Zend_Db_Adapter_Exception: SQLSTATE…). Remember to remove this test query from your code afterwards. ■Note In Chapter 14 we will add code to handle application errors such as invalid database connections. The Smarty Template Engine Smarty is a template engine written for PHP that allows you to easily separate your application output and presentation logic from your application logic. We looked at what this means ear- lier in this chapter when covering the Model-View-Controller design pattern, but what does it actually mean in terms of using Smarty? Basically, anything we want to show to the user (that is, the HTML output) will be stored in a template file (which we will denote with a file extension of .tpl). After a user request has CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK30 9063Ch02CMP4 11/4/07 12:23 PM Page 30 been processed, whether that means processing a form or fetching a list of news articles to display, we will use Smarty to output that template file. A template file contains a series of placeholders used to dynamically output content. So in the case of displaying a list of news articles, the template file would loop over the articles and provide HTML code for each one. In addition, prior to displaying the template, we must tell the template about any data we want to be able to show in it. So in the case of news arti- cles, we must assign the articles to the template prior to displaying the template. To demonstrate this, I will return to the NewsController example we looked at above in the “How Requests Work with Zend_Controller” section. The following example shows the basic algorithm used to assign data to a template and then display that template. For this code to work, we must set template_dir and compile_dir accordingly. These settings indicate the filesystem paths where templates are stored and where compiled templates should be written, respectively. This is covered in more detail in the “Downloading and Installing Smarty” section later in the chapter. template_dir = '/var/www/phpweb20/templates'; $smarty->compile_dir = '/var/www/phpweb20/data/tmp/templates_c'; $smarty->assign('news', $articles); $smarty->display('news/index.tpl'); } } ?> The first thing to do is define some data to assign to the template. In this case, I’ve created a simple array called $articles, which contains some fake news headlines. After instantiating and configuring the $smarty object, I assign the $articles array to $smarty, and finally output the news/index.tpl file. Based on the specified template_dir, the full path of this template would be ./templates/news/index.tpl. Now let’s see what the news/index.tpl template might look like. There’s a lot going on in this template.

News

{if $news|@count == 0}

No news found!

CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 31 9063Ch02CMP4 11/4/07 12:23 PM Page 31 {else}
    {foreach from=$news item=article}
  • {$article|escape}
  • {/foreach}
{/if} The first thing to note is that I haven’t included all of the normal HTML tags (such as the document type and and tags). Typically we would include these tags, but I have tried to keep the clutter out of this template. Next is an if/else statement. Note that it is wrapped in curly braces. These are the default delimiters for Smarty template code. Note also that if expressions in Smarty are not wrapped in parentheses as they would be in PHP. Note also that in this template, I use $news to refer to the article data. In the previous news example, I assigned the $articles variables to the template using the name news. When processing the data, I first check whether the $news array is empty by using the PHP count() function. In fact, what I am doing is using a Smarty modifier. Modifiers are applied using a vertical pipe. Essentially, the variable is passed to the modifier as its first argument. Smarty comes with several built-in modifiers, but you can also use any PHP function as a modifier. Because PHP’s count() accepts an array as an argument, I put the @ character before count. If I didn’t, Smarty would loop over the array and pass each array element to count(), rather than the array as a whole. It is also possible to pass arguments to modifiers. For instance, if you wanted to retrieve the first three characters of a string using substr(), you could do so using $myStr|substr:0:3, which is equivalent to calling substr($myStr, 0, 3) in PHP. To output a variable, simply wrap the variable in curly braces. So to output the first three characters of a string in the template, you would use {$myStr|substr:0:3} in the template. ■Note You can also chain several modifiers together. In the preceding example, you could change the output to display the first three characters of a string in uppercase by also applying strtoupper() as a modifier. To do this, you would use {$myStr|substr:0:3|strtoupper}. Modifiers are applied in order from left to right. In the template, I next use the {foreach} tag to loop over the $news array. This behaves almost identically to foreach() in PHP. The array is passed in using the from argument, and the current element of the array is assigned to variable specified in the item argument. So in the preceding example, the PHP equivalent of {foreach from=$news item=article} is foreach ($news as $article). If I also wanted the array key, I would specify the key argument: {foreach from=$news item=article key=k} would be equivalent to foreach ($news as $k => $article) in PHP. Now I output each element of the array inside of the foreach loop. I could simply use {$article}, but I have improved this slightly by using the escape modifier (this is a Smarty modifier, not a PHP function). This modifier should be frequently used when outputting data CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK32 9063Ch02CMP4 11/4/07 12:23 PM Page 32 inside of HTML documents, as it will escape HTML entities to make the document valid. In other words, it will turn > into >, < into <, and & into &, among others. Finally, I close the foreach loop using {/foreach}. Note how this is similar to how HTML tags work. Similarly, the {if} clause is closed using {/if}. Why Not Use a Different Template Engine? Smarty is certainly not the only choice as far as template engines go. Most PHP developers will have a different opinion as to which template engine to use. The concerns with Smarty gener- ally consist of the following: • The Smarty code is large (approximately 150KB of code for Smarty.class.php and Smarty_Compiler.class.php combined) and expensive (in terms of processing power) to use for every request on your web site. • Why use a metalanguage to output content when PHP is designed to do exactly this? Certainly, these are both valid concerns. We’ll take a quick look at each of these and prac- tical ways to deal with them. Improving Smarty Performance First, let me say that in real terms, unless you have a high-traffic web site, and/or a slow web server, the overhead caused by using Smarty will typically not be noticeable. Regardless, it is always good to look at ways of improving the performance of your web applications. Smarty compiles templates into native PHP code whenever they are changed. When a web site is in production, templates will generally not be modified and therefore not be recompiled. This means that the Smarty_Compiler.class.php class is not loaded, effectively reducing the amount of code to be parsed by about 90KB. Next, you can always use code accelerators (such APC or PHP Accelerator) to decrease the overhead of loading the Smarty library. Additionally, you can cache the output from any or all of your web pages (using Smarty’s caching functionality, or using something like Zend_Cache). ■Note The Alternative PHP Cache (APC) is free to download and can easily be installed using the PECL installer. It is used for caching and optimizing PHP code on the web server, thereby improving server per- formance. If you’re using Linux, you can simply type pecl install apc from the command line, add extension="apc.so" to your php.ini, and then restart your web server. Check the output from phpinfo() to confirm that it is correctly installed. Using a Metalanguage for Templates While using PHP code directly for templates is a perfectly viable solution, it can be very useful to use a metalanguage for templates instead. Here are some of the advantages of using Smarty templates over native PHP code: • The code is shorter and more easily readable. For example, using {$foo} to output the $foo variable provides less clutter than or . CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 33 9063Ch02CMP4 11/4/07 12:23 PM Page 33 •Smarty provides built-in security features, which when activated will control what can be done in a template. That is, it heavily restricts access to normal PHP functions. Tech- nically speaking, using native PHP for templates could result in unrelated operations taking place in a template (such as writing to a file or sending an e-mail). Take, for example, a content management system (CMS). In addition to being able to update web site content, a CMS will typically allow users to modify the web site templates. Enforcing control over what can and can’t be contained in a template has huge benefits in this type of situation, where user-submitted data is used. •It can be less daunting for non-programmers to create templates. For example, if you employ somebody to convert a web design into HTML and CSS, it will be simpler for them to use Smarty than PHP. •Smarty can be extended in so many ways that some really powerful effects can be achieved. The most obvious example is in the use of modifiers. Another powerful (but often overlooked) feature is creating custom blocks. For example, you could make a custom Smarty block called roundedbox, which you could use to output content inside a box with rounded corners. Although Firefox can provide this in CSS (using the -moz-border-radius selector), it is not available in Internet Explorer (border-radius is included in CSS3, not yet implemented in major browsers). You could then use tem- plate code as follows in your template: {roundedbox} some content {/roundedbox}. Since drawing rounded boxes without a native CSS solution requires the use of HTML tables or nested divs, you can hide the implementation details away in the roundedbox block handler. Of course, it would be unfair to ignore the disadvantages of using a metalanguage for templates. Here are some of the disadvantages of using Smarty templates over native PHP code: • There is extra overhead in parsing and compiling the templates in PHP code. Note, however, that this is only ever done when a template is changed, and therefore the overhead is almost zero in the long term. •Users must learn an extra language, and while Smarty is really good at some things, there are some drawbacks. For example, if you want to output an array into a three- column table, you will generally end up with a clutter of {assign}, {math}, and {section} tags. However, you can also extend to create built-in functions or include a separate template to hide this clutter. The Zend Framework does, in fact, provide a templating solution that uses native PHP files. While we looked at the Zend_Controller component earlier in this chapter (the controller part of MVC), there is also the Zend_View component (the view part of MVC). This component works similarly to Smarty, except that the templates it uses are written in native PHP code. If you prefer to use this instead of Smarty, you will need to adapt the templates we create accordingly. Downloading and Installing Smarty You can download the Smarty code from the Smarty web site (http://smarty.php.net). The latest version at the time of writing is 2.6.18, but you should use the most current version. The CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK34 9063Ch02CMP4 11/4/07 12:23 PM Page 34 following commands can be used in Linux to download Smarty and move it to the application include directory (./include). # cd /var/www/phpweb20 # wget http://smarty.php.net/do_download.php?download_file=Smarty-2.6.18.tar.gz # tar -zxf Smarty-2.6.18.tar.gz # cd Smarty-2.6.18 # mv libs ../include/Smarty # cd ../include/Smarty The contents of the directory should look like this: # ls Config_File.class.php Smarty_Compiler.class.php internals/ Smarty.class.php debug.tpl plugins/ ■Note You may wish to remove the downloaded and extracted files that are left over after installing Smarty, as they are no longer required. In order to use Smarty, we need to configure the template_dir and compile_dir properties of each instantiated Smarty object. • template_dir is the location where all of our application templates are stored. We earlier specified this when creating our directory structure and settings file to be /var/www/phpweb20/templates. • compile_dir is a directory where Smarty saves compiled templates. Since Smarty tem- plates use their own metalanguage, Smarty compiles each template to native PHP code in order to speed subsequent execution. Whenever a template file is modified, Smarty automatically recompiles that template and saves it to the compile directory. The compile_dir directory needs to be writable by the web server. We will be using the /var/www/phpweb/data/tmp/templates_c directory for this (it is convention to use templates_c as the directory name for compiled Smarty templates). We earlier created the ./data/tmp directory, but we must now create the templates_c directory and give write permissions to it. The following commands can be issued to do so: # cd /var/www/phpweb20/data/tmp/ # mkdir templates_c # chmod 777 templates_c/ In order to render a template with Smarty, we would now use code similar to the follow- ing. Note that the foo.tpl template doesn’t really exist (but if it did its full path would be /var/www/phpweb20/templates/foo.tpl). template_dir = '/var/www/phpweb20/templates'; $smarty->compile_dir = '/var/www/phpweb20/data/tmp/templates_c'; $smarty->display('foo.tpl'); ?> We shouldn’t be hard-coding these paths—we have them stored in our configuration file, so we should use them. Let’s look at the same code using the paths from settings.ini. (Note that I am assuming that the $settings variable has already been created and set up as in our index.php bootstrap file.) template_dir = $config->paths->templates; $smarty->compile_dir = $config->paths->data . '/tmp/templates_c'; $smarty->display('foo.tpl'); ?> Automatic View Rendering with Zend_Controller When using Zend_Controller, a plug-in called ViewRenderer is automatically loaded, and it displays a view script (that is, a template) based on the names of the requested controller and action. This means that when we use Smarty we don’t have to instantiate the Smarty class or call the display() method to output templates; ViewRenderer will do all of this for us. In order for this to work, we must extend the Zend_View_Abstract class to interact with the Smarty class. We will create a class called Templater, and we must then tell Zend_Controller about this class in the index.php bootstrap file. We will store this class in the application ./include directory in a file called Templater.php. Additionally, we will create the ./include/Templater/plugins directory, in which we will store any custom Smarty plug-ins that we write throughout this book. By storing all of our own exten- sions in a separate directory, we can easily upgrade to the latest version of Smarty without having to track which of our files need moving. To create the required directories, use the following commands: # cd /var/www/phpweb20/include/ # mkdir -p Templater/plugins ■Tip The -p argument to mkdir results in intermediate directories being created as required. That is, if the Templater directory doesn’t exist, it will be created before creating the plugins directory. We can now create the Templater class, in which we specify template_dir and compile_dir. Additionally, we must tell Smarty to look in the Templater/plugins/ directory for plug-ins (in addition to Smarty’s own plugins directory). CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK36 9063Ch02CMP4 11/4/07 12:23 PM Page 36 To implement this class, we must implement several key methods so that ViewRenderer can interact with Smarty. The most important of these methods are as follows: • getEngine(): This must return an instance of Smarty. Since this may be called multiple times, we should cache the Smarty instance so it is only created once. We do this by cre- ating the Smarty object in the constructor. • __set(): This assigns a variable to the template. Essentially this means we can replace $smarty->assign('foo', 'bar') with $this->view->foo = 'bar' in any controller action. • __get(): This returns a variable that has previously been assigned to a template. • render(): This method renders a template. This is effectively the same as calling $smarty->display(), except that this method should return the output (not display it directly), so we must use fetch() instead of display() on the Smarty object. Listing 2-11 shows the code for Templater.php, which in keeping with Zend Framework’s class naming structure means we must store this class in the ./include directory. Listing 2-11. Extending Smarty for Use with Our Web Application (Templater.php) _engine = new Smarty(); $this->_engine->template_dir = $config->paths->templates; $this->_engine->compile_dir = sprintf('%s/tmp/templates_c', $config->paths->data); $this->_engine->plugins_dir = array($config->paths->base . '/include/Templater/plugins', 'plugins'); } public function getEngine() { return $this->_engine; } CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 37 9063Ch02CMP4 11/4/07 12:23 PM Page 37 public function __set($key, $val) { $this->_engine->assign($key, $val); } public function __get($key) { return $this->_engine->get_template_vars($key); } public function __isset($key) { return $this->_engine->get_template_vars($key) !== null; } public function __unset($key) { $this->_engine->clear_assign($key); } public function assign($spec, $value = null) { if (is_array($spec)) { $this->_engine->assign($spec); return; } $this->_engine->assign($spec, $value); } public function clearVars() { $this->_engine->clear_all_assign(); } public function render($name) { return $this->_engine->fetch(strtolower($name)); } public function _run() { } } ?> CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK38 9063Ch02CMP4 11/4/07 12:23 PM Page 38 Integrating Smarty with the Web Site Controllers Finally, we need to make Zend_Controller use the Templater class instead of its default Zend_View class. To do this, we must use the following code, which we will shortly add to the application bootstrap file: $vr = new Zend_Controller_Action_Helper_ViewRenderer(); $vr->setView(new Templater()); $vr->setViewSuffix('tpl'); Zend_Controller_Action_HelperBroker::addHelper($vr); Note that we must call setViewSuffix() to indicate that templates finish with a file exten- sion of .tpl. By default, Zend_View will use the extension .phtml. Listing 2-12 shows how the controller part of index.php looks once this code has been added. Listing 2-12. Telling Zend_Controller to Use Smarty Instead of its Default View Renderer (index.php) setControllerDirectory($config->paths->base . '/include/Controllers'); // setup the view renderer $vr = new Zend_Controller_Action_Helper_ViewRenderer(); $vr->setView(new Templater()); $vr->setViewSuffix('tpl'); Zend_Controller_Action_HelperBroker::addHelper($vr); $controller->dispatch(); ?> ■Note Viewing the web site now will still display the “Web site home” message. However, a Smarty error will occur, since we haven’t yet created the corresponding template file for the index action of the index controller. Now, whenever a controller action is executed, Zend_Controller will automatically look for a template based on the controller and action name. Let’s use the index action of the index controller as an example, as shown in Listing 2-13. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 39 9063Ch02CMP4 11/4/07 12:23 PM Page 39 Listing 2-13. Our New Index Controller, Now Outputting the index.tpl File (IndexController.php) When you open http://phpweb20 in your browser, the action in Listing 2-13 will now be executed, and the Templater class we just created will automatically render the template in ./templates/index/index.tpl. Since the index.tpl template doesn’t yet exist, however, we must now create it. Again, we will simply output the “Web site home” message, but we will also create header (header.tpl) and footer (footer.tpl) templates that will be included in all web site templates. This allows us to make modifications to the web site in one place and have them carry over to all pages in the site. To include the header.tpl and footer.tpl templates in index.tpl, we use Smarty’s {include} tag. Listing 2-14 shows the contents of index.tpl, which can be found in ./templates/index/index.tpl. Listing 2-14. The Template for the Index Action of the Index Controller (index.tpl) {include file='header.tpl'} Web site home {include file='footer.tpl'} If you try to view this page in your browser without creating the header.tpl and footer.tpl files, an error will occur, so let’s now create these templates. Listing 2-15 shows the contents of header.tpl, while Listing 2-16 shows footer.tpl. These files are both stored in the ./templates directory (not within a subdirectory, as they don’t belong to a specific controller). Listing 2-15. The HTML Header File,Which Indicates a Document Type of XHTML 1.0 Strict (header.tpl) Title
CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK40 9063Ch02CMP4 11/4/07 12:23 PM Page 40 Listing 2-16. The HTML Footer File,Which Simply Closes Off Tags Opened in the Header (footer.tpl)
As you can see, the header and footer are straightforward at this stage. We will develop them further as we move along, such as by adding style sheets, JavaScript code, and relevant page titles. The Content-Type tag was included here because the document will not val- idate correctly without it (using the W3C validator at http://validator.w3.org). You may need to specify a different character set than iso-8859-1, depending on your locale. Note that I have specified a document type of XHTML 1.0 Strict. All HTML developed in this book will conform to that standard. We can achieve this by correct use of cascading style sheets, inclusion of JavaScript, and correctly escaping user-submitted data in the HTML (an example of this is the Smarty escape modifier we looked at earlier in this chapter). If you now load the http://phpweb20 address in your web browser, you will see the simple “Web site home” message. If you view the source of this document, you will see that message nested between the
open tag from header.tpl, and the
close tag from footer.tpl. Note that the
is included as it violates the standard to have text directly inside the tag. Adding Logging Capabilities The final thing we will look at in this chapter is adding logging capabilities to our application. To do this, we will use the Zend_Log component of the Zend Framework, which we will use in various places in our application. For example, we will record an entry in the log every time a failed login occurs in the members section. Although it is possible to do some pretty fancy things with logging (such as writing entries to a database, or sending e-mails to a site administrator), all we will do now is create a single log file to hold log entries. This file can then be used to debug any possible problems that arise not only during development of the web application, but also in its day-to-day operation. We will store the log file in the /var/www/phpweb20/data/logs directory that we created earlier. This directory must be writable by the web server: # cd /var/www/phpweb20/data/ # chmod 777 logs The procedure for using Zend_Log is to firstly instantiate the Zend_Log class, and then add a writer to it. A writer is a class that does something with the log messages, such as writing them to a database or sending them straight to the browser. We will be using the Zend_Log_ Writer_Stream writer to write log messages to the file specified in our settings.ini file (the logging.file value). The following code shows this procedure. First, a filesystem writer is created, which is then passed as the only argument to the constructor of the Zend_Log class: CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK 41 9063Ch02CMP4 11/4/07 12:23 PM Page 41 We can now add this code to our index.php bootstrap file. We want to create the Zend_Log object as soon as possible in the application, so we can record any problems that occur in the application. Since we rely on the logging.file value from settings.ini, we can create our logger as soon as this configuration file has been loaded. ■Note It is possible to have multiple writers for a single logger. For example, you might use Zend_Log_ Writer_Stream to write all log messages to the filesystem and use a custom e-mail writer to send log messages of a critical nature to the system administrator. In Chapter 14 we will implement this specific functionality. Listing 2-17 shows the new version of index.php, which now creates $logger, an instance of Zend_Log. The path of the log file is found in the $config->logging->file variable. Addition- ally, it is written to the registry so it can be accessed elsewhere in the application. Listing 2-17. The Updated Version of the Application Bootstrap File, Now with Logging (index.php) logging->file)); Zend_Registry::set('logger', $logger); // connect to the database $params = array('host' => $config->database->hostname, 'username' => $config->database->username, 'password' => $config->database->password, 'dbname' => $config->database->database); $db = Zend_Db::factory($config->database->type, $params); Zend_Registry::set('db', $db); // handle the user request $controller = Zend_Controller_Front::getInstance(); CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK42 9063Ch02CMP4 11/4/07 12:23 PM Page 42 $controller->setControllerDirectory($config->paths->base . '/include/Controllers'); // setup the view renderer $vr = new Zend_Controller_Action_Helper_ViewRenderer(); $vr->setView(new Templater()); $vr->setViewSuffix('tpl'); Zend_Controller_Action_HelperBroker::addHelper($vr); $controller->dispatch(); ?> Writing to the Log File To write to the log file, we call the log() method on the $logger object. The first argument is the message we want to log, and the second argument is the priority level of the message. The following is a list of the built-in log priorities (from the Zend Framework manual): • Zend_Log::EMERG (Emergency: system is unusable) • Zend_Log::ALERT (Alert: action must be taken immediately) • Zend_Log::CRIT (Critical: critical conditions) • Zend_Log::ERR (Error: error conditions) • Zend_Log::WARN (Warning: warning conditions) • Zend_Log::NOTICE (Notice: normal but significant condition) • Zend_Log::INFO (Informational: informational messages) • Zend_Log::DEBUG (Debug: debug messages) ■Note It is also possible to create your own logging priorities, but for development in this book we will only use these built-in priorities. So, if you wanted to write a debug message, you might use $logger->log('Test', Zend_Log::DEBUG). Alternatively, you could use the priority name as the method on $logger, which is essentially just a simple shortcut. Using this method, you could use $logger- >debug('Test') instead. As a test, you can add that line to your index.php file after you instantiate Zend_Log, as follows: logging->file)); Zend_Registry::set('logger', $logger); $logger->debug('Test'); // ... other bootstrap code ?> Now, load http://phpweb20 in your browser and then check the contents of debug.log. You will see something like this: # cat debug.log 2007-04-23T01:19:27+09:00 DEBUG (7): Test As you can see, the message has been written to the file, showing the timestamp of when it occurred, as well as the priority (DEBUG, which internally has a code of 7). Remember to remove the line of code from index.php after trying this! ■Note It is possible to change the formatting of the log messages using a Zend_Log formatter. By default, the Zend_Log_Formatter_Simple formatter is used. Zend Framework also comes with a formatter that will output log messages in XML. Not all writers can have their formatting changed (such as if you write log messages to a database—each event item is written to a separate column). At this stage, we won’t be doing anything further with our application logger. However, as mentioned, we will use it to record various events as we continue with development, such as recording failed logins. Summary In this chapter we’ve begun to build our web application. After setting up the development environment, we set up the application framework, which includes structuring the files in our web application, configuring application settings, connecting to the database, handling client requests, outputting web pages with Smarty, and writing diagnostic information to a log file. In the next chapter, we will begin to implement the user management and administration aspects of our web application. We will be making heavy use of the Zend_Auth and Zend_Acl components of the Zend Framework. CHAPTER 2 ■ SETTING UP THE APPLICATION FRAMEWORK44 9063Ch02CMP4 11/4/07 12:23 PM Page 44 User Authentication, Authorization, and Management In Chapter 2 we looked at the Model-View-Controller design pattern, which allowed us to easily separate our application logic from the display logic, and we implemented it using Zend_Controller_Front. We will now extend our application controller to deal with user authentication, user authorization, and user management. At this stage, you may be wondering what the difference between authentication and authorization is. • Authentication: Determines whether a user is in fact who they claim to be. This is typi- cally performed using a unique username (their identity) and a password (their credentials). • Authorization: Determines whether a user is allowed to access a particular resource, given that we now know who they are from the authentication process. Authorization also determines what an unauthenticated user is allowed to do. In our application, a resource is essentially a particular action or page, such as the action of submitting a new blog post. In this chapter, we will set up user authentication in our application using the Zend_Auth component of the Zend Framework. This includes setting up database tables to store user details. We will then use the Zend_Acl component to manage which resources in the applica- tion each user has access to. Additionally, we must tie in our permissions system to work with Zend_Controller_Front. Creating the User Database Table Since our application will hold user accounts for multiple users, we need to track each of these user accounts. To do so, we will create a database table called users. This table will contain one record for each user, and it will hold their username and password, as well as other impor- tant details. There will be three classes of users that access our web application: guests, members, and administrators. A user visiting the application will be automatically classed as a guest until they log in as a member. In order to distinguish members from administrators, the users table will include a column that denotes the role of each user. We will use this column when imple- menting the access control lists with Zend_Acl. 45 CHAPTER 3 9063Ch03CMP4 11/13/07 9:37 PM Page 45 ■Note In a more complex system, you might assign multiple roles to users; however, for the sake of sim- plicity we will allow only one role per user. Any user classed as an administrator will also be able to perform all functions that a member can. Additionally, you could also use another table to store user types, but once again, for the sake of simplicity we will forego this and keep a static list of user types in our code. The core data we will store for each user in the users table will be as follows: • user_id: An internal integer used to represent the user. • username: A unique string used to log in. In effect, this will be a public identifier for the user. We will display the username on blog posts and other publicly available content, rather than their real name, which many users prefer to keep anonymous. • password: A string used to authenticate the user. We will store passwords as a hash using the md5() function. Note that this means passwords cannot be retrieved; instead they must be reset. We will implement all code required to do this. • user_type: A string used to classify the user (either admin or member, although you will easily be able to add extra user types in the future based on what you learn in this book). • ts_created: A timestamp indicating when the user account was created. • ts_last_login: A timestamp indicating when the user last logged in. We will allow this field to have a null value, since the user won’t have yet logged in when the record is created. Listing 3-1 shows the SQL commands required to create the users table in MySQL. All SQL schema definitions are stored in the schema-mysql.sql file in the main application directory. If you’re using PostgreSQL, you can find the corresponding schema in schema-pgsql.sql instead. ■Note How you choose to store the database schema for your own web applications is entirely up to you. I’ve simply structured it this way so you can easily refer to it as required (and so you have easy access to it when downloading the code for this book). Listing 3-1. SQL Used to Create the Users Table in MySQL (schema-mysql.sql) create table users ( user_id serial not null, username varchar(255) not null, password varchar(32) not null, user_type varchar(20) not null, ts_created datetime not null, CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 46 9063Ch03CMP4 11/13/07 9:37 PM Page 46 ts_last_login datetime, primary key (user_id), unique (username) ) type = InnoDB; The user_id column is defined as type serial, which is the same as using bigint unsigned not null auto_increment. I personally prefer using serial, as it is shorter and simpler to type, and it also works in PostgreSQL. The username column can be up to 255 characters in length, although we will put a restric- tion on this length in the code. The password will be stored as an MD5 encrypted string, so this column only needs to be 32 characters long. Next is the user_type column. The length of this column isn’t too important, although any new user types you add will be limited to 20 characters (this is only an internal name, so it doesn’t need to be overly descriptive). This string is used when performing ACL checks. Finally, there are the two timestamp columns. MySQL does in fact have a data type called timestamp, but I chose to use the datetime type instead, as MySQL will automatically update columns that use the timestamp type. In PostgreSQL, you need to use the timestamptz data type instead (see the schema-pgsql.sql file for the table definition). The following “Time- stamps” section provides more details about how timestamps work in PHP. ■Tip Listing 3-1 instructs MySQL to use the InnoDB table type when creating a table, thereby providing us with SQL transaction capability and enforcing foreign key constraints. The default table type used otherwise is MyISAM. You must now create this table in your database. There are two ways to do this. First, you can pipe the entire schema-mysql.sql file into your database using the following command: # mysql -u phpweb20 -p phpweb20 < schema-mysql.sql When you type this command you will be prompted to enter your password. This will create the entire database from scratch. Alternatively, you can connect directly to the database, and copy and paste the table schema using the following command: # mysql -u phpweb20 -p phpweb20 Since we will be building on the database as we go, I recommend the second method for simply adding each new table as required. Timestamps The way dates and times are handled in PHP, MySQL, and PostgreSQL is often misunderstood. Before we go any further, I will quickly cover some important points to be aware of when using dates and times in MySQL. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 47 9063Ch03CMP4 11/13/07 9:37 PM Page 47 MySQL does not store time zone information with its date and time data. This means that your MySQL server must be set to use the same time zone as PHP; otherwise you may notice odd behavior with timestamps. For example, if you want to use the PHP date() function to format a timestamp from a MySQL table, be cautious—if you use the MySQL unix_timestamp() function when retrieving that timestamp, the incorrect date will be retrieved if the time zones do not match up. There are three major drawbacks to using the date field types in MySQL: •If you need to move your database to another server (let’s say you change web hosts), the moved data will be incorrect if the server uses a different time zone. The server con- figuration would need to be modified, which most web hosts will not do for you. •Various issues can arise concerning when daylight savings starts and finishes (assum- ing your location uses daylight savings). •It is difficult to store timestamps from different time zones. You must convert all time- stamps to the server time zone before inserting them. If you think these aren’t problems that will occur often, you are probably right, although here’s a practical example. A web application I wrote stored the complete schedule for a sports league (among other things). Week to week, all games took place in different cities, and there- fore in different time zones. For accurate scheduling data to be output on the web application (for instance, “3 hours until game time”), the time zone data needed to be accurate. PostgreSQL does not have the datetime data type. Instead, I prefer to use the timestamptz column, which stores a date, time, and time zone. If you don’t specify the time zone when inserting a value into this column, it uses the server’s time zone (for instance, both 2007-04-18 23:32:00 and 2007-04-18 23:32:00+09:30 are valid; the former will use the server’s time zone and the latter will use +09:30). In the sports schedule example, I used PostgreSQL, which allowed me to easily store the time zone of the game. PostgreSQL’s equivalent of unix_timestamp(ts_column) is extract(epoch from ts_column). Using timestamptz, this returns an accurate value that can be used in PHP’s date() function. It also seamlessly deals with daylight savings. User Profiles You may have noticed that the users table (Listing 3-1) didn’t store any useful information about the user, such as their name or e-mail address. To store this data, we will create an extra table called users_profile. By using an extra table to store this information, we can easily store an arbitrary amount of information about the user without modifying the users table at all. For instance, we can store their name, e-mail address, phone number, location, favorite food, or anything else. Additionally, we can use this table to store preferences for each user. Each record in the users_profile table corresponds to a single user profile value. That is, one record will correspond to a user’s e-mail address, while another record will hold their name. There is slightly more overhead in retrieving this data at runtime, but the added flexibil- ity makes it well worth it. All that is required in this table is three columns: • user_id: This column links the profile value to a record in users. • profile_key: This is the name of the profile value. For instance, we would use the value email here if the record holds an e-mail address. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 48 9063Ch03CMP4 11/13/07 9:37 PM Page 48 • profile_value: This is the actual profile value. If the profile_key value is email, this column would hold the actual e-mail address. ■Tip We use the text field type for profile_value because this allows us to store a large amount of data if required. There is no difference in performance between the varchar and text types in MySQL and PostgreSQL. In fact, MySQL internally creates a varchar field as the smallest possible text field based on the specified precision. Listing 3-2 shows the MySQL table definition for users_profile. We will implement code to manage user profiles later in this chapter. Listing 3-2. SQL Used to Create the users_profile Table in MySQL (schema-mysql.sql) create table users_profile ( user_id bigint unsigned not null, profile_key varchar(255) not null, profile_value text not null, primary key (user_id, profile_key), foreign key (user_id) references users (user_id) ) type = InnoDB; As mentioned previously, the serial column type (used for the user_id column in Listing 3-1) is an alias for an auto-incrementing unsigned bigint column. Since the user_id column in this table refers back to the users table, we manually use the bigint unsigned type because we don’t want this column to auto-increment. We use the user_id and profile_key columns as the primary key for the users_profile table, as no profile values can be repeated for each user. However, a user can have several dif- ferent profile values. ■Note If you’re using PostgreSQL, the int data type is used for user_id, as this is what the PostgreSQL serial type uses. Once again, the PostgreSQL version of the table can be found in schema-pgsql.sql. Introduction to Zend_Auth Now that we’ve created the users table, we have something to authenticate against using Zend_Auth. Before we get to that, though, we must understand exactly how Zend_Auth works. First, we must understand the terminology Zend_Auth uses. The unique information that identifies a user is referred to as their identity. After a user successfully authenticates, we store their identity in a PHP session so we can identify them in subsequent page requests. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 49 9063Ch03CMP4 11/13/07 9:37 PM Page 49 ■Note It is possible to write custom storage methods, but the most common storage method will arguably be in a PHP session. Zend_Auth provides the Zend_Auth_Storage_Session class for this. This class, in turn, uses the Zend_Session component, which is essentially a wrapper to PHP’s $_SESSION variable (although it does provide greater functionality). To create some other storage method, you simply implement the Zend_Auth_Storage_Interface interface. For example, if you wanted to “remember a user” in between sessions, you could create a storage class that writes identity data to a cookie. You would then create an adapter (discussed shortly) to authenticate against cookie data. Be careful with this though, as it could potentially be dangerous if done incorrectly, since cookie data can be forged. One safeguard against this could be to give them a restricted role until they provide their credentials again, as Amazon.com does: it will remember your identity but not allow you to make any changes to your account unless you re-enter your password. Another example of using custom session storage is in a load-balanced environment (where mul- tiple web servers are used for a single site). Disk-based sessions will not typically be available across all servers, so a subsequent user request may be handled on a different server than the previous request. Storing session data in the database alleviates this problem. In order to authenticate a user, they must provide credentials. In the case of the applica- tion we are writing, we will use the password column from the users table as the user’s credentials. We use an adapter to check the given identity and credentials against our database. Adapters in Zend_Auth implement the Zend_Auth_Adapter_Interface interface. Thankfully, the Zend Framework comes with an adapter that we can use to check our MySQL database. If we wanted to authenticate users against a different storage method (such as LDAP or a password file generated by Apache’s htpasswd), we would need to write a new adapter. We will be using the Zend_Auth_Adapter_DbTable adapter, which is designed to work with the Zend_Db component. If you choose instead to write your own adapter, the only method you need to implement is the authenticate() method, which returns a Zend_Auth_Result object. This object contains information about whether authentication was successful, as well as diagnostic messages (such as whether the provided credentials were incorrect, or authentica- tion failed because the identity wasn’t found or for some other reason). By default, Zend_Auth_Adapter_DbTable returns only the submitted username in the Zend_Auth_Result object. However, we need to store additional information about the user (such as their name and, more importantly, their user type). When we look at processing user logins with Zend_Auth, we will deal with this. Instantiating Zend_Auth Zend_Auth is a singleton class, which means only one instance of it can exist (like the Zend_ Controller_Front class we used in Chapter 2). As such, we can use the static getInstance() method to retrieve that instance. We must then set the storage class (remember, we are using sessions) using the setStorage() method. If you use multiple storage methods, you will need to call this every time you want to access identity data in each storage location. Typically though, you will only need to call this once: at the start of each request. The following code is used to set up the Zend_Auth instance. As you can see, it is fairly straightforward in its initial usage: CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 50 9063Ch03CMP4 11/13/07 9:37 PM Page 50 setStorage(new Zend_Auth_Storage_Session()); ?> We will be using the $auth object in several places in our web application. First, it will be used when we check user permissions with Zend_Acl (in the “Introduction to Zend_Acl” sec- tion later in this chapter). It will also be used in application login and logout methods, as we need to store and then clear the identity data for each of these methods. As we did with our application configuration and database connection, we will store the $auth object in the application registry using Zend_Registry. Listing 3-3 shows the index.php bootstrap file as it stands with Zend_Auth. Listing 3-3. The Application Bootstrap File, Now Using Zend_Auth (index.php) logging->file)); Zend_Registry::set('logger', $logger); // connect to the database $params = array('host' => $config->database->hostname, 'username' => $config->database->username, 'password' => $config->database->password, 'dbname' => $config->database->database); $db = Zend_Db::factory($config->database->type, $params); Zend_Registry::set('db', $db); // setup application authentication $auth = Zend_Auth::getInstance(); $auth->setStorage(new Zend_Auth_Storage_Session()); // handle the user request $controller = Zend_Controller_Front::getInstance(); $controller->setControllerDirectory($config->paths->base . '/include/Controllers'); $controller->registerPlugin(new CustomControllerAclManager($auth)); CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 51 9063Ch03CMP4 11/13/07 9:37 PM Page 51 // setup the view renderer $vr = new Zend_Controller_Action_Helper_ViewRenderer(); $vr->setView(new Templater()); $vr->setViewSuffix('tpl'); Zend_Controller_Action_HelperBroker::addHelper($vr); $controller->dispatch(); ?> Authenticating with Zend_Auth In Chapter 4 we will be implementing the login and logout forms for our web application, but before we get to that we will take a look at how the login and logout process actually work. As mentioned previously, we will be using the Zend_Auth_Adapter_DbTable authentication adapter. Prior to using this adapter, you must already have a valid Zend_Db object. Because Zend_Auth_Adapter_DbTable is flexible and is designed to work with any database configuration, you must tell it how your storage is set up. Thus, you must include the following when instantiating it: • The name of the database table being used (our table is called users). • The column that holds the user identity (we are using the username column in the users table). • The column that holds the user credentials (we are using the password column). • And finally, the treatment used on the credentials. This is essentially a function that (if specified) wraps around the credentials. Remember that we are storing an MD5 hash of the password in the password column. Therefore, we pass md5(?) as this final argument. The question mark tells Zend_Db where to substitute in the password value. Once Zend_Auth_Adapter_DbTable is instantiated (we will use the variable name $adapter), we can set the identity (username) and credentials (password). To do this, we use setIdentity() and setCredential(). Next, we will call the authenticate() method on the $auth object (the instance of Zend_Auth). The single argument passed to authenticate() is the adapter ($adapter). An instance of Zend_Auth_Result is then returned. We can call isValid() on this object to see whether the user successfully authenticated. If they didn’t, we can either call getMessages() on the result to determine why, or we can generate our own error message based on the result from getCode(). ■Note Although Zend_Auth_Result allows us to easily distinguish between an invalid username and an invalid password, this typically isn’t information you should present to the user. Doing so can implicitly let them know when a username exists or not, which can aid malicious users in gaining unauthorized access to your application. The example in Listing 3-4 differentiates between these errors purely to demonstrate how you can detect them. The code we add to our application will not inform users whether it was their user- name or their password that was incorrect. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 52 9063Ch03CMP4 11/13/07 9:37 PM Page 52 Listing 3-4 shows the code used to instantiate Zend_Auth_Adapter_DbTable and to authen- ticate against the users table. At this stage, we are simply providing a fake username and password, as we haven’t yet populated the users table. As you can see, we also handle authentication errors and output a message indicating the reason for failure. Listing 3-4. Authenticating Against a Database Table Using Zend_Auth and Zend_Db (listing-3-4.php) 'localhost', 'username' => 'phpweb20', 'password' => 'myPassword', 'dbname' => 'phpweb20'); $db = Zend_Db::factory('pdo_mysql', $params); // setup application authentication $auth = Zend_Auth::getInstance(); $auth->setStorage(new Zend_Auth_Storage_Session()); $adapter = new Zend_Auth_Adapter_DbTable($db, 'users', 'username', 'password', 'md5(?)'); // try and login the "fakeUsername" user $adapter->setIdentity('fakeUsername'); $adapter->setCredential('fakePassword'); $result = $auth->authenticate($adapter); if ($result->isValid()) { // user successfully authenticated } else { // user not authenticated switch ($result->getCode()) { case Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND: echo 'Identity not found'; break; case Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS: echo 'Multiple users found with this identity!'; CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 53 9063Ch03CMP4 11/13/07 9:37 PM Page 53 break; case Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID: echo 'Invalid password'; break; default: var_dump($result->getMessages()); } } ?> You can also check whether or not a user is authenticated using the $auth object. The hasIdentity() method indicates whether or not a user is authenticated. Then, to determine which user that is, you can use the getIdentity() method. Similarly, you can use the clearIdentity() method to log a user out. If you are using sessions as the storage method, this effectively unsets the identity from the session. As mentioned previously, when $auth->authenticate() succeeds using Zend_Auth_Adapter_DbTable, only the username is stored for the identity data. In Chapter 4, when we implement the user login form, we will alter the identity data to include other user details, such as the user type. Introduction to Zend_Acl Zend_Acl is a component of the Zend Framework that provides access control list (ACL) func- tionality. While it doesn’t fundamentally require the use of Zend_Auth, we will combine these two components to control what users can and cannot do in our web application. Essentially what Zend_Acl does is determine whether a role has sufficient privileges to access a resource. • Resource: Some object (not an object in the OOP sense, just some “thing”) in a web application to which access can be controlled. An example of a resource is an action in a web application, such as approving the content of an article before it is published, or deleting a user from the system. Additionally, you can provide finer-grained control over privileges to resources. So, in the example of approving an article, the resource would be the article-management system (or a particular article, depending on how you look at it), while the privilege would be the approve action. • Role: Some object that requests access to resources. In our web application, a role refers to a user of certain privileges. Although this language might be somewhat confusing, each user in our application (that is, each record in the users table) has a particular user type. We refer to this as a user’s role. ■Note It is possible to make a role or a resource inherit from another role or resource, respectively. For example, let’s say you assign certain privileges to Role A. If you make Role B inherit from Role A, it will get all of the privileges that Role A has, in addition to any extra privileges you add to Role B. This can make your permissions system confusing (especially when inheriting from more than one other role or resource), so we will try to keep it as simple as possible in our application. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 54 9063Ch03CMP4 11/13/07 9:37 PM Page 54 We will control access to particular resources (such as publishing a blog post or resetting a password) based on a user’s role. As mentioned when creating the users table, the three types of users (the three user roles) will be guest, member, and administrator. The typical flow for using Zend_Acl in a web application is as follows: 1. Instantiate the Zend_Acl class (let’s call this object $acl). 2. Add one or more roles to $acl using the addRole() method. 3. Add resources to $acl using the add() method. 4. Add the full list of privileges for each role (that is, use allow() or deny() to indicate which resources roles have access to). 5. Use the isAllowed() method on $acl to determine whether a particular role has access to a particular resource/privilege combination. 6. Repeat step 5 as often as necessary while the script executes. A Zend_Acl Example Let’s take a look at actually using the Zend_Acl class. In this example, I will use the role names we will be using in our application. The privileges I set up here should give you an idea of exactly what we will be doing when we integrate Zend_Acl into our application. The first thing I need to do to manage and check permissions is to instantiate the Zend_ Acl class. The constructor takes no arguments: $acl = new Zend_Acl(); Next, I create each of the roles that I’m checking permissions for. As mentioned previ- ously, we will be using three different roles: guest, member, and administrator. $acl->addRole(new Zend_Acl_Role('guest')); $acl->addRole(new Zend_Acl_Role('member')); $acl->addRole(new Zend_Acl_Role('administrator')); After creating the roles, I can create the resources. In fact, I could swap the order; the key thing is that both roles and resources must be added before defining or checking permissions. For this example, I will only add account and admin as the resources that will be granted permissions. There will be other resources in our application, but only items that will be granted permissions need to be added here, because when checking permissions, we check for the existence of the requested resource. It’s up to you as the developer how you handle a permissions check for a nonexistent resource. In this case, I will simply allow access to a requested resource if it hasn’t been added to $acl. $acl->add(new Zend_Acl_Resource('account')); $acl->add(new Zend_Acl_Resource('admin')); The next step is to define the different permissions required in the application. This is achieved by making a series of calls to allow() and deny() on the Zend_Acl instance. The first argument to this function is the role, and the second is the resource. You can add finer-grained control by specifying the third parameter (the permission name). CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 55 9063Ch03CMP4 11/13/07 9:37 PM Page 55 In the permissions system for our application, the name of the controller (in the context of Zend_Controller) is the resource, while the controller action is the permission name. As in the following example, we can allow or deny access to an entire controller (as we will do for guest in the admin controller), or we can open up one or two specific actions within a con- troller (as we will do for the login and fetchpassword actions for guest). $acl->allow('guest'); // allow guests everywhere ... $acl->deny('guest', 'admin'); // ... except in the admin section ... $acl->deny('guest', 'account'); // ... and the account management section $acl->allow('guest', 'account', // ... although let them log in array('login', 'fetchpassword')); In addition to defining what guests can do, I also want to define what members are allowed to do. Members are privileged users, so I allow them more access than guests: $acl->allow('member'); // members can go everywhere ... $acl->deny('member', 'admin'); // ... except for the site admin section Next I define the permissions for administrators, who are even more privileged than members: $acl->allow('administrator'); // administrators can go everywhere! Once all the permissions have been defined, they can be queried to determine what can and can’t be accessed. Here are some examples: // check permissions $acl->isAllowed('guest', 'account'); // returns false $acl->isAllowed('guest', 'account', 'login'); // true $acl->isAllowed('member', 'account'); // true $acl->isAllowed('member', 'account', 'login'); // true $acl->isAllowed('member', 'admin'); // false $acl->isAllowed('administrator', 'admin'); // true Note that in our application the role names will be dynamically determined based on the user that is logged in, and the resource and permission names will be determined by the requested controller and action. Realistically, the call to isAllowed() will be in an if statement, such as this: isAllowed('member', 'account')) { // display member account area } ?> CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 56 9063Ch03CMP4 11/13/07 9:37 PM Page 56 ■Tip If you try to check the permissions of an undefined resource, an exception will be thrown. It is up to you how you want to handle this. For example, you may choose to automatically deny the request, or you may choose to automatically allow it. Another option could be to fall back to a different resource if the given resource is not found; the has() function is used to check the existence of a resource. The same principle applies to roles. In our application, a user will fall back to guest if their role is not found (this would result from a bogus value in the user_type column of the users table). Our actual permissions system will be almost identical to this example, in that members can access the account resource, while guests cannot, and administrators can access all areas. ■Note The code uses both the term admin and administrator. The user type (that is, the role) is called administrator, while the controller (that is, the resource) is called admin. In other words, only users of type administrator will be able to access the http://phpweb20/admin URL. Combining Zend_Auth, Zend_Acl, and Zend_ Controller_Front The next step in developing our web application is to integrate the Zend_Auth and Zend_Acl components. In this section, we will change the behavior of the application controller (that is, the instance of Zend_Controller_Front), to check permissions using Zend_Acl prior to dis- patching a user’s request. When checking permissions, we will use the identity stored with Zend_Auth to determine the role of the current user. To control permissions, we will treat each controller as a resource, and treat the action handlers in these controllers as the permissions associated with the resource. For instance, later in this chapter we will create the AccountController.php file, which is used to control everything relating to user accounts (such as logging in, logging out, fetching passwords, and updating user details). The AccountController controller will be the resource for Zend_Acl, while the privileges associated with this resource are the actions just mentioned (login, logout, fetch password, update details). ■Note There are many ways to structure a permissions system. In this application, we will simply control access to action handlers in controller files. This is relatively straightforward, as we can automate all ACL checks dynamically based on the action and controller name in a user request. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 57 9063Ch03CMP4 11/13/07 9:37 PM Page 57 The way we achieve this setup of using controller and action names to dictate permis- sions is to write a plug-in for Zend_Controller (by extending the Zend_Controller_Plugin_ Abstract class). This plug-in defines the preDispatch() method, which receives a user request before the front controller dispatches the request to the respective action. Effectively, we are intercepting the request and checking whether the current user has sufficient privileges to execute that action. To register a plug-in with Zend_Controller, we call the registerPlugin() method on our Zend_Controller_Front instance. Before we do that, let’s create the plug-in, which we will call CustomControllerAclManager. We will create all roles and resources for Zend_Acl in this class, as well as checking permissions. Listing 3-5 shows the contents of the CustomControllerAclManager.php file, which we will store in the /var/www/phpweb20/include directory. Listing 3-5. The CustomControllerAclManager Plug-in,Which Checks Permissions Prior to Dispatching User Requests (CustomControllerAclManager.php) 'account', 'action' => 'login'); public function __construct(Zend_Auth $auth) { $this->auth = $auth; $this->acl = new Zend_Acl(); // add the different user roles $this->acl->addRole(new Zend_Acl_Role($this->_defaultRole)); $this->acl->addRole(new Zend_Acl_Role('member')); $this->acl->addRole(new Zend_Acl_Role('administrator'), 'member'); // add the resources we want to have control over $this->acl->add(new Zend_Acl_Resource('account')); $this->acl->add(new Zend_Acl_Resource('admin')); // allow access to everything for all users by default // except for the account management and administration areas $this->acl->allow(); $this->acl->deny(null, 'account'); $this->acl->deny(null, 'admin'); // add an exception so guests can log in or register // in order to gain privilege CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 58 9063Ch03CMP4 11/13/07 9:37 PM Page 58 $this->acl->allow('guest', 'account', array('login', 'fetchpassword', 'register', 'registercomplete')); // allow members access to the account management area $this->acl->allow('member', 'account'); // allows administrators access to the admin area $this->acl->allow('administrator', 'admin'); } /** * preDispatch * * Before an action is dispatched, check if the current user * has sufficient privileges. If not, dispatch the default * action instead * * @param Zend_Controller_Request_Abstract $request */ public function preDispatch(Zend_Controller_Request_Abstract $request) { // check if a user is logged in and has a valid role, // otherwise, assign them the default role (guest) if ($this->auth->hasIdentity()) $role = $this->auth->getIdentity()->user_type; else $role = $this->_defaultRole; if (!$this->acl->hasRole($role)) $role = $this->_defaultRole; // the ACL resource is the requested controller name $resource = $request->controller; // the ACL privilege is the requested action name $privilege = $request->action; // if we haven't explicitly added the resource, check // the default global permissions if (!$this->acl->has($resource)) $resource = null; // access denied - reroute the request to the default action handler if (!$this->acl->isAllowed($role, $resource, $privilege)) { $request->setControllerName($this->_authController['controller']); CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 59 9063Ch03CMP4 11/13/07 9:37 PM Page 59 $request->setActionName($this->_authController['action']); } } } ?> The class constructor is where we define roles, resources, and permissions. In Listing 3-5 we first make the administrator role inherit from the member role. This means that any permis- sion given to members is also given to administrators. Additionally, we can then give the administrator role privileges on its own to access the admin area. Next, we set up the default permissions (that is, permissions that apply to all roles). These allow access to everything except for the account and admin resources. Obviously, a guest needs the chance to authenticate themselves and become a privileged user, so we must open up access to the login and fetchpassword privileges. Additionally, if they are not yet registered, we need to grant them access to register and registercomplete (a helper action used to con- firm registration to a user). Once a guest becomes authenticated (thereby becoming either a member or an adminis- trator), they need to be able to access the account resource. Since the administrator role inherits from the member role, permitting members access to the account resource also gives access to administrators. Finally, we open up the admin areas to administrators only. In other words, guests and members cannot access this area. Now, let’s take a look at the preDispatch() method, which takes the user request as an argument. First, we set up the role and resource so the ACL check will work correctly. If the resource is not found, we set the $resource variable to null, which means the default permis- sion will be used for the given role. Based on the way we have set this up (that is, allowing access to everything), this effectively means the ACL check will return true. If the role is not found, we use the guest role instead. ■Note We are accessing the user_type property of the identity stored with Zend_Auth.We haven’t yet looked at storing this property with the identity when performing a login, but we will cover this in Chapter 4, when we implement the login action to our account controller. Finally, we call isAllowed() to determine whether the $role role has access to the $privilege privilege of resource $resource. If this returns true, we do nothing and let the front controller dispatch loop continue. If this returns false, we reroute the dispatcher to execute the login action of the account controller. In other words, when an unprivileged user tries to do something they are not allowed to do, they will be redirected to a login screen. ■Note One side effect of this behavior is that if a member tries to access the admin area, they will be shown a login screen, even though they are already logged in. You could modify the code to show the login screen if no identity is found in $auth, but show a different screen if the user is logged in but has insuffi- cient privileges. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 60 9063Ch03CMP4 11/13/07 9:37 PM Page 60 Managing User Records with DatabaseObject DatabaseObject is a class I developed several years ago that I make heavy use of in nearly all of my PHP development tasks. It acts as an extra layer on top of a database connection, which makes reading, writing, and deleting rows from a database very simple. You can find the full DatabaseObject.php file in the ./include directory of the downloadable source code. Essentially, I extend the abstract DatabaseObject class for each major table in an applica- tion. So to manage records in the users table of our web application, we will create a class called DatabaseObject_User. Once we instantiate this class, we can then call the load() method to fetch a record from the database, use the save() method to either insert or update data in the database (depending on whether or not a record has already been loaded), and call delete() to delete a loaded record. ■Note When I first wrote DatabaseObject, neither PHP 5 nor the Zend Framework were out yet, but I have since updated this class to use PHP 5 and to work with the Zend_Db component. If you are not using Zend_Db, you will have to make appropriate changes. Instead of looking at the implementation details, we will take a look at the available func- tions and exactly how DatabaseObject can be used: • load(): Loads a record by performing a select query. Returns true if the record is loaded. • isSaved(): Returns true if a record has previously been loaded with load(). • save(): Saves the current data to the database. If the record wasn’t previously loaded, an insert statement is used; otherwise the loaded record is updated with an SQL update. • delete(): If a record has been loaded, this function performs an SQL delete query. • getId(): Retrieves the database ID of a saved record. There are also a number of callbacks you can define, which are automatically called as required. The callbacks that can be defined are as follows: • postLoad(): Called after a record is successfully loaded. It could be used to load data from other tables as required. • preInsert(): Called prior to inserting a new record (note that in this case save() distin- guishes inserts from updates). It could be used to set values dynamically (such as a timestamp recording the date of insert). • postInsert(): Called after a new record is saved. In the case of our users table, we will use this to send an e-mail to the new user. • preUpdate(): Called prior to an existing record being updated. It could be used to set values dynamically (such as a timestamp recording the date of update). • postUpdate(): Called after an existing record is updated. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 61 9063Ch03CMP4 11/13/07 9:37 PM Page 61 • preDelete(): Called prior to an existing record being deleted. If other tables depend on this data, you would delete the data from those tables here, before the data is deleted from this table. • postDelete(): Called after a record has been deleted. It could be used to delete a file on the filesystem that relates to this record. All callbacks (except for postLoad()) must return either true or false. If false is returned, the entire transaction is rolled back. For example, if you return false from postDelete(), the record is not deleted, and any queries you perform in preDelete() are also rolled back. It is important to remember to define the return value if you implement any of these functions. ■Note Because of the way DatabaseObject works, all tables that use it must follow a similar structure. That is, the table must have a single primary key field, with an auto-incrementing sequence. The users table we created earlier in this chapter follows this structure by defining the user_id field as a serial. This wasn’t the case for users_profile, and we will be managing data in this table slightly differently. The DatabaseObject_User Class Now that we’ve looked at how DatabaseObject works, we will create a child class to manage records in the users table. Once we have created this class, we will look at how to actually use it. To create this class, all we really need to do is define the name of the database table and the name of its primary key field, and then define the list of columns in the table. If required, you can also set the types of the columns, which makes DatabaseObject treat the data accord- ingly. At this stage, all we will be using is the DatabaseObject::TYPE_TIMESTAMP type. Listing 3-6 shows the contents of User.php, which should be stored in the DatabaseObject directory (so the full path is /var/www/phpweb20/include/DatabaseObject). Note that naming it in this manner means the Zend Framework autoloader will automatically include this code when required. Listing 3-6. The Initial Version of the DatabaseObject_User Class (User.php) add('username'); $this->add('password'); $this->add('user_type', 'member'); $this->add('ts_created', time(), self::TYPE_TIMESTAMP); $this->add('ts_last_login', null, self::TYPE_TIMESTAMP); } } ?> CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 62 9063Ch03CMP4 11/13/07 9:37 PM Page 62 In Listing 3-6, we first call the parent constructor. This method accepts the database con- nection as the first argument (an instance of Zend_Db_Adapter), the database table name as the second argument, and the column name of the primary key as the third argument. Next, we add the list of fields using add(). The first argument is the name of the field, the second argument if specified is its default value, and the third argument is the type. If no type is specified, the value is simply treated as is. In the listing, you can see that the ts_created and ts_last_login fields are both time- stamps. We set the ts_created field to be the current time, and we set ts_last_login to null, as the user has not yet logged in. ■Note We could alternatively set the default value of ts_created to null, and then dynamically set the value in the preInsert() callback instead. There’s no real difference, unless there is a huge time difference between instantiating the object and calling its save() method. The other thing we have done is set the default value of the user_type field to member. Ear- lier in this chapter we covered the three types of users: guests, members, and administrators. By definition, a guest is somebody who doesn’t have a user account (and therefore has no row in the users table), so we set the default value to member. Now is a good time to define the user types in this code. Our code should allow us to add more user types in the future and to only ever have to change this one list (disregarding the fact that we would likely need to change the ACL permissions). We could alternatively store the list of user types in a database table, but for the sake of simplicity we will store them in a static array in the DatabaseObject_User class. Additionally, we can extend the __set() method to intercept the value being set so we can ensure that the value is valid. ■Note PHP 5 allows the use of a magic __set() method, which is automatically called (if defined) when code tries to modify a nonexistent property in an object. DatabaseObject uses this method to set values to be saved in the database table. We can also define this in the DatabaseObject_User child class in order to alter a value before calling __set() in the parent class. PHP 5 also allows a similar __get() method, which is automatically called if a nonexistent property is read. DatabaseObject also uses this method. Before we look at the code that does this, there is one further value we must intercept and alter before it is written to the database: the password. We mentioned earlier that we are sav- ing passwords as MD5 hashes of their original value. As such, we must call md5() on the password value prior to saving it to the database. ■Note You can use either the PHP version of md5() or you can call it in the SQL query. For the sake of sim- plicity and cross-database compatibility, we will use the PHP function. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 63 9063Ch03CMP4 11/13/07 9:37 PM Page 63 Listing 3-7 shows the new version of User.php, which now defines the list of user types, as well as ensuring that a valid user type is set. It also changes the password value, when it is set, to be an MD5 hash. Listing 3-7. The New Version of DatabaseObject_User, Now Setting the Password and User Type Correctly (User.php) 'Member', 'administrator' => 'Administrator'); public function __construct($db) { parent::__construct($db, 'users', 'user_id'); $this->add('username'); $this->add('password'); $this->add('user_type', 'member'); $this->add('ts_created', time(), self::TYPE_TIMESTAMP); $this->add('ts_last_login', null, self::TYPE_TIMESTAMP); } public function __set($name, $value) { switch ($name) { case 'password': $value = md5($value); break; case 'user_type': if (!array_key_exists($value, self::$userTypes)) $value = 'member'; break; } return parent::__set($name, $value); } } ?> Using DatabaseObject_User Now that we have created the DatabaseObject_User class, let’s look at how to use it. Listing 3-8 shows the typical usage of a DatabaseObject child class: we first set some properties and then call the save() method (which will perform an SQL insert). Next we modify some properties CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 64 9063Ch03CMP4 11/13/07 9:37 PM Page 64 on the same object and then call save() again (this time an SQL update will be performed). Finally, we try to load an existing record and then delete it from the database table. Listing 3-8. Sample Usage of the DatabaseObject_User Class (listing-3-8.php) 'localhost', 'username' => 'phpweb20', 'password' => 'myPassword', 'dbname' => 'phpweb20'); $db = Zend_Db::factory('pdo_mysql', $params); // Create a new user $user = new DatabaseObject_User($db); $user->username = 'someUser'; $user->password = 'myPassword'; $user->save(); // Now update that user and save new details $user->user_type = 'admin'; $user->ts_last_login = time(); $user->save(); // Find a user with user_id of 5 and delete them $user2 = new DatabaseObject_User($db); if ($user2->load(5)) { $user2->delete(); } ?> If we were to look at the users table after running this script, it might look something like this: mysql> select user_id, username, password from users; +---------+----------+----------------------------------+ | user_id | username | password | +---------+----------+----------------------------------+ | 7 | someUser | deb1536f480475f7d593219aa1afd74c | +---------+----------+----------------------------------+ CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 65 9063Ch03CMP4 11/13/07 9:37 PM Page 65 Managing User Profiles When we created the users table earlier in this chapter, we also created a table called users_ profile, which we use to hold user profile data. The way this table is structured, we can add any number of values to correspond with each user account. This may include personal details, such as the user’s name or e-mail address, or it may include other settings, such as whether or not the user wants to receive a monthly newsletter. Because I use a system like this for most web applications I work on, I have developed a generic class called Profile to manage data of this nature. Profile is an abstract class that must be extended for each table you want to write to. We will create a class called Profile_User to extend Profile. The profile is typically used as follows: 1. Create a new instance of Profile_User. One instance is responsible for the profile data of one user. 2. Set the user ID and load the existing profile data for that user. 3. Set new values, update existing values, or delete existing values as required. 4. Save the profile data. In order to autoload the classes with Zend_Loader, we can store the Profile.php file in the ./include directory, while we store User.php (which holds the Profile_User class) in ./include/Profile. No methods need to be implemented in the Profile_User class—all we need to do is specify the database table used to store profile data. Additionally, we need to add a single utility method to set the user ID. Since we are storing profile data for all users in a single table, we need to add a filter to the parent Profile class so it correctly reads and writes the profile data. Listing 3-9 shows the contents of User.php, which defines the Profile_User child class. Listing 3-9. The Profile_User Child Class, Used to Initialize Profile Management for Users (User.php) 0) $this->setUserId($user_id); } public function setUserId($user_id) { $filters = array('user_id' => (int) $user_id); $this->_filters = $filters; CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 66 9063Ch03CMP4 11/13/07 9:37 PM Page 66 } } ?> To instantiate Profile_User, the database connection is passed, as well as an optional user ID. If you don’t specify a user ID, you can call the setUserId() method. Once the user ID has been set, you can call the load() method to load existing profile data from the database. ■Note You must make a call to setUserId() before calling load() or save(); otherwise the data may be saved incorrectly or an error will occur. Using Profile_User Now that we have looked at the code for Profile_User, let’s take a look at an example of how to use the class. For this example, let’s assume a user has already been created in the users table with an ID of 1234 (remember from our schema that the user_id field in users_profile is a foreign key to users, so the corresponding record must exist). The first thing we must do is instantiate the class and load the data: load(); ?> Alternatively, we can call setUserId() instead of passing the ID in the constructor. We will be using this method when we integrate Profile_User with DatabaseObject_User. $profile = new Profile_User($db); $profile->setUserId(1234); $profile->load(); Now we can set a new profile value (or update an existing one) just by accessing the object property, like so: $profile->email = 'user@example.com'; We can delete a profile value by calling unset(): unset($profile->email); And we can check whether a profile value exists by calling isset(): if (isset($profile->email)) { // do something } Finally, we must save any changes that we make to the database by calling the save() method: $profile->save(); CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 67 9063Ch03CMP4 11/13/07 9:37 PM Page 67 Listing 3-10 shows a more complete example of using Profile_User, this time including the database creation code. Listing 3-10. A Complete Example of Setting Profile Data and Displaying a Simple Message (listing-3-10.php) 'localhost', 'username' => 'phpweb20', 'password' => 'myPassword', 'dbname' => 'phpweb20'); $db = Zend_Db::factory('pdo_mysql', $params); $profile = new Profile_User($db); $profile->setUserId(1234); $profile->load(); $profile->email = 'user@example.com'; $profile->country = 'Australia'; $profile->save(); if (isset($profile->country)) echo sprintf('Your country is %s', $profile->country); ?> If you were to check the data in the users_profile table after running this example, it would look something like the following: mysql> select * from users_profile where user_id = 1234; +---------+-------------+------------------+ | user_id | profile_key | profile_value | +---------+-------------+------------------+ | 1234 | country | Australia | | 1234 | email | user@example.com | +---------+-------------+------------------+ 2 rows in set (0.00 sec) CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 68 9063Ch03CMP4 11/13/07 9:37 PM Page 68 Integrating Profile_User with DatabaseObject_User Now that we have a way of managing user profiles, we must integrate this into our DatabaseObject_User class so that all user data can easily be managed in a single place. Essentially what we must do is as follows: •Instantiate the Profile_User class within DatabaseObject_User. • Load the profile data automatically when a user is loaded. •Save the profile data automatically when the user record is saved. •Delete the profile data automatically when the user record is deleted. Additionally, we must deal with the fact that the user ID is not known when creating a new user record with DatabaseObject_User. As such, we must correctly use the callbacks that DatabaseObject makes available. We will use them as follows: •In the load callback (postLoad()), we will set the user ID and load the profile data. •Before an insert occurs (preInsert()), we will generate a password for the user. For now, we will use the PHP uniqid() function to generate a password, but we will improve on this in Chapter 4 when we need to send an e-mail out to new users. • After an insert occurs (postInsert()), we will set the user ID and save the profile data. • After an update occurs (postUpdate()), we will save the profile data (the user ID is known at this point). •Before a delete occurs (preDelete()), we will delete all profile data. Note that this must occur before the user is deleted (as opposed to being done in postDelete()), because a foreign key constraint violation will occur if we do it the other way around (that is, users_profile depends on users, so data can’t be removed from users that is referenced in users_profile). Listing 3-11 shows the new version of DatabaseObject_User, which defines each of these callbacks. Importantly, the postInsert() and postUpdate() callbacks also return true, which is required for the database transaction to complete. Listing 3-11. DatabaseObject_User with Profile Management Functionality Built in (User.php) 'Member', 'administrator' => 'Administrator'); public $profile = null; public function __construct($db) { parent::__construct($db, 'users', 'user_id'); CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 69 9063Ch03CMP4 11/13/07 9:37 PM Page 69 $this->add('username'); $this->add('password'); $this->add('user_type', 'member'); $this->add('ts_created', time(), self::TYPE_TIMESTAMP); $this->add('ts_last_login', null, self::TYPE_TIMESTAMP); $this->profile = new Profile_User($db); } protected function preInsert() { $this->password = uniqid(); return true; } protected function postLoad() { $this->profile->setUserId($this->getId()); $this->profile->load(); } protected function postInsert() { $this->profile->setUserId($this->getId()); $this->profile->save(false); return true; } protected function postUpdate() { $this->profile->save(false); return true; } protected function preDelete() { $this->profile->delete(); return true; } public function __set($name, $value) { switch ($name) { case 'password': $value = md5($value); break; CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 70 9063Ch03CMP4 11/13/07 9:37 PM Page 70 case 'user_type': if (!array_key_exists($value, self::$userTypes)) $value = 'member'; break; } return parent::__set($name, $value); } } ?> In addition to the callbacks defined in this code, Profile_User is instantiated in the constructor. Note that because we have used the PHP 5 __set() and __get() overloaders in DatabaseObject, we must also define the $profile property in the class definition. ■Important When calling the save() method on the profile, we pass false as an argument, which prevents Profile from using a database transaction to save the data. We want to prevent this because DatabaseObject has already initiated a transaction, so the saving of profile data falls within this transac- tion. In other words, if we were to return false from postUpdate(), the transaction would be rolled back, meaning the changes to the user table wouldn’t be saved, and the profile data would remain unchanged in the database. With these new features added to DatabaseObject_User, we can now easily manipulate all user data as required. Listing 3-12 shows an example of creating a new user and setting the profile data all in one step. Listing 3-12. Creating a New User and Setting the Profile Data All in One Step (listing-3-12.php) 'localhost', 'username' => 'phpweb20', 'password' => 'myPassword', 'dbname' => 'phpweb20'); $db = Zend_Db::factory('pdo_mysql', $params); // Create a new user $user = new DatabaseObject_User($db); $user->username = 'someUser'; $user->password = 'myPassword'; CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 71 9063Ch03CMP4 11/13/07 9:37 PM Page 71 // Set their profile data $user->profile->email = 'user@example.com'; $user->profile->country = 'Australia'; // Save the user and their profile $user->save(); // Load some other user and delete them $user2 = new DatabaseObject_User($db); if ($user2->load(1234)) $user2->delete(); ?> Summary In this chapter we created the infrastructure for managing users in our web application. First, we looked at the Zend_Auth and Zend_Acl components from the Zend Framework. We discov- ered the differences between authentication and authorization, and how they apply to our application. Next, we integrated both of these components with Zend_Controller_Front, restricting access to our application based on the requested controller and action. We then looked at how database data can easily be managed using the DatabaseObject and Profile classes, which we extended in order to manage user data. In the next chapter, we will continue the process of building the application’s user system by allowing users to create new accounts, log in, and update their profiles using the code we have developed in this chapter. CHAPTER 3 ■ USER AUTHENTICATION, AUTHORIZATION, AND MANAGEMENT 72 9063Ch03CMP4 11/13/07 9:37 PM Page 72 User Registration, Login, and Logout In Chapter 3 we looked closely at the user authentication and authorization aspects of the web application. We learned that authentication is when a user proves they are who they say they are, while authorization determines what that user is and isn’t allowed to do. We created the necessary database tables to hold user details as well as the code to manage the database records. We then used the Zend_Auth and Zend_Acl components of the Zend Framework to control which areas of the web site users can access. In this chapter we will build on the code from Chapter 3 by implementing a user registra- tion system. Once registered, users will be able to log in and update their details. This chapter covers everything related to creating user accounts and authenticating (that is, logging in). This includes the use of CAPTCHA images as well as allowing users to reset their forgotten passwords. Adding User Registration to the Application Implementing a user registration system is a fairly involved process, not only because there’s a lot to do in setting up a user account, but also because it’s the first real interaction between the web application and the end-user that we’ve looked at in this book. The process of accepting user registrations will involve the following: •Adding navigation so the user can find the registration form •Displaying the registration form to the user, including a CAPTCHA image •Accepting and validating the submitted details, including checking availability of user- names •Displaying errors back to the user if something goes wrong •Saving the database record, e-mailing the user, and displaying a confirmation page if all went well We won’t do all of this in exactly this order, but we will build up the registration system until it incorporates all of these features. 73 CHAPTER 4 9063CH04CMP4 11/20/07 9:20 PM Page 73 The fields users will be filling in for registration are as follows: • A username. This value must be unique and contain only alphanumeric characters (letters and numbers). • Their name. We will split this up into first name and last name. • Their e-mail address. We require this so we have a valid point of contact for the user. To ensure that we have a real e-mail address, the account password is automatically gener- ated and sent to this address. This is a simple but effective way of preventing false e-mail addresses from being entered. Creating the Form Processor for User Registration In order to keep the code that is responsible for processing the user registration form separate from other parts of the application (such as the account controller that displays the registration form), we will create a class called FormProcessor_UserRegistration. This class will extend from FormProcessor, another utility class (available in the book’s code base in ./include/FormProcessor.php) that I wrote to aid in my own web application development. The FormProcessor class is fairly simple and doesn’t do anything aside from hold the form values you tell it to, and hold form error messages that you can display. The Initial FormProcessor_UserRegistration Class To extend FormProcessor, all we need to do is implement the abstract function process(), which accepts a Zend_Controller_Request_Abstract object as an argument and returns true if the form was successfully processed or false if an error occurred. The instance of Zend_ Controller_Request_Abstract is an object generated by Zend_Controller_Front, which holds all data relating to the current request, such as get and post data. ■Note In actual fact, the instance of Zend_Controller_Request_Abstract is an instance of Zend_ Controller_Request_Http that we will eventually pass to process(). The Zend_Controller_Request_ Http class extends from Zend_Controller_Request_Abstract. As mentioned above, FormProcessor also provides methods for storing error messages: • addError($name, $message): Sets a new error message with the given name. If the error message already exists, that error name is assigned an array with multiple messages. • hasError($name): Checks whether an error message with the specified name has been set. By omitting the $name parameter, this method can also be used to check whether any errors have been set at all. • getError($name): Retrieves the error message for the given name. If no corresponding error message has been set, null is returned. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT74 9063CH04CMP4 11/20/07 9:20 PM Page 74 Additionally, there is a function called sanitize() that is used to strip HTML tags from the string and trim whitespace from the start and end of the string. This is achieved primarily using Zend_Filter, a Zend Framework component that can manipulate strings with filters (we will look briefly at Zend_Filter in Chapter 7). ■Note The FormProcessor.php file is available from the downloadable source code for this book. It belongs in the ./include directory so it can be automatically loaded as required. Let’s now take a look at the FormProcessor_UserRegistration class. Listing 4-1 shows the beginnings of this class—we will add to it throughout this section. This file is located in ./include/FormProcessor/UserRegistration.php. Listing 4-1. The Beginnings of the User Registration Form Processor (UserRegistration.php) db = $db; $this->user = new DatabaseObject_User($db); $this->user->type = 'member'; } public function process(Zend_Controller_Request_Abstract $request) { // validate the username // validate first and last name // validate the e-mail address // validate CAPTCHA phrase // save database record if no errors // return true if no errors have occurred return !$this->hasError(); } } ?> CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 75 9063CH04CMP4 11/20/07 9:20 PM Page 75 The first thing this code does is define the constructor, in which the database connection is accepted and an instance of DatabaseObject_User is created. This object will remain unsaved until the form is successfully processed and $this->user->save() is called. Next the abstract method process() is implemented. This method returns true if the form was processed correctly and false if an error occurred. As such, we can use the hasError() method to determine the return value. To implement the process() method, we must fetch the submitted values from the $request object and process them accordingly. First, we must check the username by doing the following: 1. Check that a username was entered. If one wasn’t, we need to notify the user that the username is a required field. 2. If a username was entered, check that it is in a valid format. Our usernames will consist of only alphanumeric characters (that is, only letters and numbers). If an invalid user- name was entered, we should create an appropriate error message. 3. If the username is valid, check whether or not somebody else has already registered with this username. In order to check these conditions, we will implement two new functions in DatabaseObject_ User: usernameExists() and IsValidUsername(), as shown in Listing 4-2. Listing 4-2. New Functions Added to DatabaseObject_User (User.php) _table); $result = $this->_db->fetchOne($query, $username); return $result['num'] > 0; } static public function IsValidUsername($username) { $validator = new Zend_Validate_Alnum(); return $validator->isValid($username); } } ?> Let’s take a look at each of these changes before returning to the FormProcessor_UserReg- istration class. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT76 9063CH04CMP4 11/20/07 9:20 PM Page 76 The usernameExists() Method We call this method to determine whether or not the passed-in username already exists. If the username is in use, then true is returned; otherwise false is returned. The IsValidUsername() Method This method simply checks whether or not a username is valid, returning true if it is and false if not. To check the validity of the username, we use the Zend_Validate component of the Zend Framework. We are only checking for alphanumeric characters, so we can use the Zend_Validate_Alnum class. Obviously, we could write a simple regular expression (such as /^[a-z0-9]+$/i) to check this, but Zend_Validate allows us to easily chain different validators together, meaning that in the future you could easily change the method for validating a username. Additionally, using Zend_Validate is a good practice to get into, as we will be using it throughout this book when validating form data (we will see it again shortly when we check users’ e-mail addresses). This method is static, as it does not rely on an instance of DatabaseObject_User. Adding Username Validation to FormProcessor_UserRegistration Since we have the new username-related methods available in DatabaseObject_User, we can now proceed to validate and set a username according to the rules outlined previously. Listing 4-3 shows the new version of process(), which now takes the submitted username from the request post data (using the getPost() method on $request) and validates it. Listing 4-3. Validating the Submitted Username (UserRegistration.php) username = trim($request->getPost('username')); if (strlen($this->username) == 0) $this->addError('username', 'Please enter a username'); else if (!DatabaseObject_User::IsValidUsername($this->username)) $this->addError('username', 'Please enter a valid username'); else if ($this->user->usernameExists($this->username)) $this->addError('username', 'The selected username already exists'); else $this->user->username = $this->username; // return true if no errors have occurred return !$this->hasError(); } } ?> CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 77 9063CH04CMP4 11/20/07 9:20 PM Page 77 As you can see in this code, we first check that the username isn’t an empty string, then we check that it’s a valid username, and then we make sure that it doesn’t already exist. If we deter- mine the username is valid, we accept the value and update the DatabaseObject_User instance. ■Note The IsValidUsername() method will return false if the string is empty, thereby making the first check somewhat redundant. However, checking for an empty string separately allows us to generate a dif- ferent error message. Validating the User’s Name As mentioned earlier, we will require users to enter both a first name and last name (in sepa- rate fields) when registering. To keep things simple, we won’t do any validation on this data other than making sure they’re not empty strings. You may want to add further validation to this data yourself. We will also call the sanitize() method to ensure any HTML tags are stripped out. Listing 4-4 shows a stripped-down version of FormProcessor_UserRegistration, which retrieves, validates, and sets the first and last name of the user. Listing 4-4. Validating the User’s First and Last Name (UserRegistration.php) first_name = $this->sanitize($request->getPost('first_name')); if (strlen($this->first_name) == 0) $this->addError('first_name', 'Please enter your first name'); else $this->user->profile->first_name = $this->first_name; $this->last_name = $this->sanitize($request->getPost('last_name')); if (strlen($this->last_name) == 0) $this->addError('last_name', 'Please enter your last name'); else $this->user->profile->last_name = $this->last_name; // return true if no errors have occurred return !$this->hasError(); } } ?> CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT78 9063CH04CMP4 11/20/07 9:20 PM Page 78 Validating the User’s E-mail Address The final submitted item we must validate is the user’s e-mail address. We do this by first checking that an e-mail address was submitted, and then by checking that it is in the correct format for an e-mail address. To check this second condition, we will use the Zend_Validate_EmailAddress class. This class is a part of the Zend_Validate component and will tell us whether or not an e-mail address is valid. ■Note Zend_Validate_EmailAddress can even go one step further than checking for a valid e-mail format: it can also check that the given hostname in the e-mail address has valid DNS MX records. We won’t be using this feature, though, as it’s the user’s problem if they want to fool the system—they simply won’t receive their password if they enter a false address. Listing 4-5 shows the code for FormProcessor_UserRegistration, which validates the e-mail address using Zend_Validate_EmailAddress. Note once again that we first check for an empty string so we can generate a different error message. Listing 4-5. Using Zend_Validate_EmailAddress to Check the Validity of a Submitted E-mail Address (UserRegistration.php) email = $this->sanitize($request->getPost('email')); $validator = new Zend_Validate_EmailAddress(); if (strlen($this->email) == 0) $this->addError('email', 'Please enter your e-mail address'); else if (!$validator->isValid($this->email)) $this->addError('email', 'Please enter a valid e-mail address'); else $this->user->profile->email = $this->email; // return true if no errors have occurred return !$this->hasError(); } } ?> CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 79 9063CH04CMP4 11/20/07 9:20 PM Page 79 The Complete FormProcessor_UserRegistration Class We have now covered all of the validation tasks required for our FormProcessor_UserRegistration class. The final section of code we must insert is a call to $this->user->save() to save the record into the users table. We will first check whether or not an error has occurred before saving the record. If there is an error, no record will be saved and the user will be shown the error messages (that is, once we have created the registration form template). Listing 4-6 shows the entire FormProcessor_UserRegistration class. In the next section we will write the code responsible for using this class. Listing 4-6. The Complete FormProcessor_UserRegistration Class (UserRegistration.php) db = $db; $this->user = new DatabaseObject_User($db); $this->user->type = 'member'; } public function process(Zend_Controller_Request_Abstract $request) { // validate the username $this->username = trim($request->getPost('username')); if (strlen($this->username) == 0) $this->addError('username', 'Please enter a username'); else if (!DatabaseObject_User::IsValidUsername($this->username)) $this->addError('username', 'Please enter a valid username'); else if ($this->user->usernameExists($this->username)) $this->addError('username', 'The selected username already exists'); else $this->user->username = $this->username; // validate the user's name $this->first_name = $this->sanitize($request->getPost('first_name')); if (strlen($this->first_name) == 0) $this->addError('first_name', 'Please enter your first name'); else $this->user->profile->first_name = $this->first_name; CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT80 9063CH04CMP4 11/20/07 9:20 PM Page 80 $this->last_name = $this->sanitize($request->getPost('last_name')); if (strlen($this->last_name) == 0) $this->addError('last_name', 'Please enter your last name'); else $this->user->profile->last_name = $this->last_name; // validate the e-mail address $this->email = $this->sanitize($request->getPost('email')); $validator = new Zend_Validate_EmailAddress(); if (strlen($this->email) == 0) $this->addError('email', 'Please enter your e-mail address'); else if (!$validator->isValid($this->email)) $this->addError('email', 'Please enter a valid e-mail address'); else $this->user->profile->email = $this->email; // if no errors have occurred, save the user if (!$this->hasError()) { $this->user->save(); } // return true if no errors have occurred return !$this->hasError(); } } ?> Displaying the Registration Form and Processing Registrations The next step in creating the registration form is to create the account controller as well as the register action inside of it. In Chapter 3 we set up the access control lists so that only regis- tered members could access the account section. That permission refers specifically to this controller (in other words, if a user tries to access http://phpweb20/account, they can only access the actions in the specified controller if they have the necessary permissions). The other permissions we defined were exemptions so that unregistered users (guests) would be able to access the register, registercomplete, login, and fetchpassword actions. There’s nothing special we need to put in the controller to deal with these permissions—it has already been done in the CustomControllerAclManager class. The Initial AccountController Class Listing 4-7 shows the beginnings of the AccountController class, which extends CustomControllerAction. At this stage we will only define the registerAction() method—as we continue with development, we will add more actions to this controller (such as the index action, which will be executed when users successfully authenticate). The AccountController class is stored in the AccountController.php file, which belongs in the ./include/Controllers directory. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 81 9063CH04CMP4 11/20/07 9:20 PM Page 81 Listing 4-7. Creating the Account Controller and Defining the Register Action (AccountController.php) db); $this->view->fp = $fp; } } ?> ■Note Since we haven’t yet created the register.tpl template, loading http://phpweb20/account/ register in your browser will result in a Smarty error. In the registerAction() method, we first instantiate the FormProcessor_UserRegistration class. We then assign it to the displayed template. This template (register.tpl) will show the HTML form to the user trying to register. The reason we assign the form processor to this template is so that any errors can be displayed to the user. The template can then read the errors in the form processor using the hasError() and getError() methods. Additionally, when displaying errors in a form, you should prepopulate the fields the user has already entered. The form processor provides access to these values easily via the magic __get() method. For instance, to retrieve the username value, you would use $fp->username in the template. Developing the Templates Before we go any further, let’s quickly add some navigation to the header.tpl template we cre- ated in Chapter 2, so we can navigate to the registration page. Listing 4-8 shows the contents of ./templates/header.tpl with some basic navigation. We will improve on this later in the book, but for now this will suffice. Listing 4-8. Including Basic Navigation on the Header Template (header.tpl) Title
CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT82 9063CH04CMP4 11/20/07 9:20 PM Page 82 Home | Register
We can now start building the register.tpl template. There are some fundamental things we need to include in a form template: •A clearly labeled form so the user knows what the form is for. •A label for each field in the form. • The HTML form element with any submitted values prepopulating the field. Addition- ally, since this contains user-submitted data, we must escape the HTML entities accordingly (as we saw in Chapter 2). • Any errors that have occurred. •A clearly labeled submit button. The easiest way to lay out a form is to use HTML tables; however, these are not necessarily the best thing to use for accessibility and for good CSS practice. Instead, we are going to use the fieldset, legend, and label HTML tags to aid with layout. Additionally, each form element is wrapped in a div so it can be positioned properly. Figure 4-1 shows what this form looks like after the user has submitted it yet omitted some fields. At this stage, the page looks somewhat bland, but we will not concern ourselves with the CSS until Chapter 6 (eventually, errors will be highlighted and the form fields will be spaced so they can be more easily understood). Figure 4-1. The registration form displaying some data-entry errors CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 83 9063CH04CMP4 11/20/07 9:20 PM Page 83 Listing 4-9 shows the contents of register.tpl, which is stored in the ./templates/account directory (you will need to create this directory if you have not already done so). Listing 4-9. The HTML Template for User Registration (register.tpl) {include file='header.tpl'}
Create an Account
hasError()} style="display: none"{/if}> An error has occurred in the form below. Please check the highlighted fields and resubmit the form.
{include file='lib/error.tpl' error=$fp->getError('username')}
{include file='lib/error.tpl' error=$fp->getError('email')}
{include file='lib/error.tpl' error=$fp->getError('first_name')}
{include file='lib/error.tpl' error=$fp->getError('last_name')}
CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT84 9063CH04CMP4 11/20/07 9:20 PM Page 84
{include file='footer.tpl'} ■Note You will still need to create the error.tpl template in Listing 4-10 before register.tpl can be viewed without any PHP or Smarty errors. In Listing 4-9, the entire form is wrapped in a
tag, which is useful for splitting a form into separate parts. This form only contains a small number of fields though, so it only uses one part. For each element in the form, we essentially use the same markup: a named
con- taining a label for the element, as well as the form element. Finally the error.tpl template is included, which we use to output any errors for the respective element. We also include a global form error message at the top of the form. This is especially useful for long forms, where an individual error may go unnoticed. Listing 4-10 shows the contents of error.tpl, which we will store in ./templates/lib. There is no great significance to the name of this directory (lib), but as a general habit I like to store reusable templates that don’t directly correspond to a specific controller action inside a separate directory. ■Note If you were to create a controller called lib, you would need to use a different directory for these helper templates. Listing 4-10. A Basic Template Used to Display Form Errors (error.tpl) {if $error|@is_array || $error|strlen > 0} {assign var=hasError value=true} {else} {assign var=hasError value=false} {/if} CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 85 9063CH04CMP4 11/20/07 9:20 PM Page 85 The way we determine whether an error has occurred is to check the $error variable passed to this template (when called in register.tpl). If it is an empty string, there are no errors. Otherwise FormProcessor::getError() will return a single error as a nonempty string, and multiple error messages with the same name will be returned as an array. The other significant thing to notice in this template is that we still generate the HTML div even if there is no error. We do this to create a placeholder for error messages we might generate on the client side using JavaScript. Later in this book we will add some client-side validation to this form (such as checking the availability of a username in real time), so we will write error messages to this error container. Handling the Form Submission At this stage in the development of the registration form, if you were to click the submit button, nothing would happen other than the empty form being redisplayed. When the page reloads, the register action handler should process the request by either using the FormProcessor_ UserRegistration class to check the form and save the user data, or to simply display the form. ■Note If an error occurs while processing the form (such as the user entering a username already in use), the code is designed to fall through to displaying the form again. On this subsequent rendering of the form, the submitted values will be available to redisplay in the template, along with any generated error messages. We’ll accomplish this by first checking for a post request (using $request->isPost()), and then calling process() accordingly. Once the form has been successfully processed, the browser is redirected to the registercomplete action. This redirection to a new action prevents the user from refreshing the page (and therefore resubmitting their registration data, which would fail at this point since the username now exists). In order to show the user a custom thank-you message (that is, one that includes some part of their registration details), we need to first write the ID (this is the user_id column of the users table, which has a data type of serial) of the new user to the session before redirect- ing them to registercompleteAction(). Inside the registercomplete action, we look for a stored user ID, and if one exists we display a message. If a valid user ID is not found in the session, we simply forward their request back to the register page. Listing 4-11 shows the account controller with the call to process(), as well as the redirec- tion to the registercomplete action once a valid registration occurs. We use the _redirect() method provided by Zend_Controller_Front, as this performs an HTTP redirect (as opposed to _forward(), which forwards the request internally). The lines you need to add to your existing version of registerAction() are displayed in bold. Listing 4-11. Completing the Processing of a User’s Registration (AccountController.php) getRequest(); $fp = new FormProcessor_UserRegistration($this->db); if ($request->isPost()) { if ($fp->process($request)) { $session = new Zend_Session_Namespace('registration'); $session->user_id = $fp->user->getId(); $this->_redirect('/account/registercomplete'); } } $this->view->fp = $fp; } public function registercompleteAction() { // retrieve the same session namespace used in register $session = new Zend_Session_Namespace('registration'); // load the user record based on the stored user ID $user = new DatabaseObject_User($this->db); if (!$user->load($session->user_id)) { $this->_forward('register'); return; } $this->view->user = $user; } } ?> In the registerAction() method, we call $this->getRequest() to retrieve the request object from Zend_Controller_Front, which contains all the data related to the user’s request, such as get and post data. This is the object we pass to FormProcessor_UserRegistration when calling process(). Note that since process() will return false if an error occurs, the code will simply fall right through to displaying the register.tpl template again, which means the errors that occurred will be displayed. On the other hand, if the call to process() returns true, we can assume a new user was created in the database. As such, we can write the user’s ID to the session and redirect the browser to /account/registercomplete. ■Note We could write directly to the $_SESSION superglobal; however, Zend_Session provides a better way of managing session data. It allows fairly straightforward management of session namespaces, mean- ing the session is organized in a way that won’t cause data conflicts. Additionally, we are already using Zend_Session to store user authentication data (that is, their identity). CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 87 9063CH04CMP4 11/20/07 9:20 PM Page 87 In the registercompleteAction() method, we check for a stored user ID and then try to load a new DatabaseObject_User object accordingly. If the record isn’t found, we forward the request back to the registerAction(). This would happen if a user requested the /account/ registercomplete URL directly without completing the registration. ■Note After calling the _forward() method in this case, we return from the registercompleteAction() method. If we didn’t, the remainder of registercompleteAction() would be executed, since the new action would only be dispatched after the current one was complete. The first argument to _forward() is the action, and the second is the controller. If the second argument is omitted (as in this case), the current controller is used. Finally, we must create the registercomplete.tpl template (which also belongs in the ./templates/account directory). We will use this template to show a basic “thank you for regis- tering” message. Listing 4-12 shows this template, which makes mention of a password being sent to the user. We will add this e-mail functionality in the “Adding E-mail Functionality” section of this chapter. Listing 4-12. The Message Displayed to Users Upon Successful Registration (registercomplete.tpl) {include file='header.tpl'}

Thank you {$user->profile->first_name|escape}, your registration is now complete.

Your password has been e-mailed to you at {$user->profile->email|escape}.

{include file='footer.tpl'} Adding CAPTCHA to the User Registration Form Now that we have the core functionality of the user registration system working, we can improve it slightly by adding a simple yet effective security measure to ensure that registra- tions come only from real people and not computer programs. This security measure is called CAPTCHA, which stands for Completely Automated Public Turing test to tell Computers and Humans Apart. There are many different types of CAPTCHA tests available, but we will be using what is probably the most common one. This is where a series of characters are shown as an image, and the user is required to identify these characters by typing them in as part of the form they are submitting. We will be using the Text_CAPTCHA component from PEAR (the PHP Extension and Appli- cation Repository) to generate our CAPTCHA images. Note that we will be using a CAPTCHA test for several forms in our web application, not just the registration form. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT88 9063CH04CMP4 11/20/07 9:20 PM Page 88 An example of a CAPTCHA image that Text_CAPTCHA generates is shown in Figure 4-2. The random lines and shapes help to fool optical character recognition (OCR) software that may try to automatically decipher the CAPTCHA. Figure 4-2. A sample CAPTCHA image generated by PEAR’s Text_CAPTCHA Circumventing CAPTCHA Although the point of the CAPTCHA test is to tell computers and humans apart, it is techni- cally possible to write a program that can solve a CAPTCHA automatically. In the case of the text CAPTCHA we will be using, OCR software could be used to determine the characters in the image. Because of this, we try to distort the images to a point where using OCR software is not possible, but not too far so that humans cannot determine which characters are being dis- played. This means avoiding characters such as zero and the letter O completely, which can easily be confused. CAPTCHA and Accessibility Another important consideration when implementing a CAPTCHA test in your web applications is accessibility. If somebody is unable to pass the test, they will be unable to complete the form protected by the CAPTCHA test. As such, it is important to have alternative methods available. One possible solution is to implement an audio CAPTCHA in addition to the text CAPTCHA. This would involve generating an audio file that reads back letters, numbers, or words, which the user must then type in. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 89 9063CH04CMP4 11/20/07 9:20 PM Page 89 Another alternative is to have a manual registration system, where the user can e-mail their details to the site administrator who can then save their details on their behalf. In Chap- ter 14 we will discuss the implementation of an administration area in our web application. Part of this administration area will be a user management section where an administrator could manually create new users. PEAR’s Text_CAPTCHA To generate CAPTCHA images, we will be using the Text_CAPTCHA component from PEAR. Text_CAPTCHA will generate the series of characters to appear in the image and then create an image with those characters appearing at a random angles in random locations. It will also add some random noise to prevent OCR software from reading the letters. This noise is a series of lines and shapes that will be placed randomly on the image. Before you can use Text_CAPTCHA, you must install it. It is available for download from http://pear.php.net/package/Text_CAPTCHA, or you can use the PEAR installer to simplify installation. Text_CAPTCHA also relies on the Text_Password and Image_Text components, so you must also install them. To install these packages using the PEAR installer, use the following com- mands: # pear install -f Text_CAPTCHA # pear install -f Image_Text Because neither of these packages have a stable release at time of writing, I used the –f argument, which forces installation of a non-stable version. The first command should auto- matically install Text_Password, but if it doesn’t, use the following command: # pear install Text_Password Text_CAPTCHA also needs a TrueType font available in order to write letters to the CAPTCHA image. Any font will do for this, as long as its characters are easy to read. The font file I use in this book is the bold version of Vera (VeraBD.ttf), available from the Gnome web site (http://www.gnome.org/fonts/). I chose this font because its license terms allow it to be freely distributed. The font should be stored in the application data directory (/var/www/ phpweb20/data/VeraBD.ttf). Generating a CAPTCHA Image In order to add CAPTCHA capabilities to our application, we need to create a new controller action that will be responsible for outputting the image. The CAPTCHA is not specific to user registration, so we will call this controller utility, as there may be other utility actions we want to add later. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT90 9063CH04CMP4 11/20/07 9:20 PM Page 90 Listing 4-13 shows the contents of UtilityController.php, which we will store in ./include/Controllers. Presently there is just one action, which is responsible for generating and outputting the image. Listing 4-13. Generating a CAPTCHA Image Using Text_CAPTCHA (UtilityController.php) 20, 'font_path' => Zend_Registry::get('config')->paths->data, 'font_file' => 'VeraBd.ttf'); $captcha->init(120, 60, null, $opts); // disable auto-rendering since we're outputting an image $this->_helper->viewRenderer->setNoRender(); header('Content-type: image/png'); echo $captcha->getCAPTCHAAsPng(); } } ?> ■Important In Listing 4-13, we must disable the autorendering of templates that Zend_Controller_ Front will do. If we don’t include the call to setNoRender(), captchaAction() will try to render a tem- plate belonging in ./templates/utility/captcha.tpl. Since the captchaAction() method outputs the generated CAPTCHA image, there is no such template. In order to use Text_CAPTCHA, we first call the factory() method to use the Image driver. We then create an array of options to specify properties of the font that will be used. As men- tioned previously, the TrueType font is stored in the application data directory, so we use the application config to tell Text_CAPTCHA about this directory. Next we call the init() method, which specifies the height, width, and CAPTCHA phrase, as well as the font options. In this code we pass null as the third parameter, which means the phrase will be randomly generated by Text_Password. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 91 9063CH04CMP4 11/20/07 9:20 PM Page 91 ■Tip You may prefer to store some of the “magic values” in Listing 4-13 (such as font name and size) in the application settings (./settings.ini). Finally, we send the image to the browser using the getCAPTCHAAsPng() method. We must also send the correct Content-type header to the browser, so it knows to interpret the data as an image. As it stands, we cannot yet use this code in our registration form because FormProcessor_UserRegistration needs to know the CAPTCHA phrase in order to determine whether or not the user entered it correctly. We must modify captchaAction() so that it gener- ates a new phrase and writes it to the session. On subsequent requests to captchaAction(), we then check for the existence of the phrase in the session. If the value exists, we use that for the image rather than generating a new one. ■Note The way we are implementing CAPTCHA images is so that if a user enters the phrase incorrectly, they are shown the same CAPTCHA image again. An alternative is to generate a new phrase every time they get it wrong. The important thing to remember in this implementation is to clear the phrase once it has been successfully entered. We will cover this shortly. Listing 4-14 shows a modified version of captchaAction(), which now checks for an exist- ing phrase, and then writes the phrase that was used in the image back to the session. Listing 4-14. Storing CAPTCHA Phrases in the Session for Reuse (UtilityController.php) phrase) && strlen($session->phrase) > 0) $phrase = $session->phrase; // generate CAPTCHA $captcha = Text_CAPTCHA::factory('Image'); $opts = array('font_size' => 20, 'font_path' => Zend_Registry::get('config')->paths->data, 'font_file' => 'VeraBd.ttf'); CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT92 9063CH04CMP4 11/20/07 9:20 PM Page 92 $captcha->init(120, 60, $phrase, $opts); // write the phrase to session $session->phrase = $captcha->getPhrase(); // disable auto-rendering since we're outputting an image $this->_helper->viewRenderer->setNoRender(); header('Content-type: image/png'); echo $captcha->getCAPTCHAAsPng(); } } ?> You can now view the generated CAPTCHA image directly in your browser by visiting http://phpweb20/utility/captcha. (This is how I generated Figure 4-2.) Unlike all of the previ- ous controller actions we have implemented so far, which returned HTML code, this action returns image data (along with the corresponding headers so browsers knows how to display the data). Adding the CAPTCHA Image to the Registration Form The next step in integrating the CAPTCHA test is to display the image on the registration form. To do this, we simply use an HTML tag to show the image, and we add a text input so the user can enter the phrase. Listing 4-15 shows the relevant HTML code we need to add to the register.tpl form cre- ated earlier in this chapter (located in ./templates/account). The convention with CAPTCHA images is to add them at the end of the form, above the submit button. Listing 4-15. Displaying the CAPTCHA Image on the Registration Form (register.tpl) {include file='header.tpl'}
Create an Account
CAPTCHA image
CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 93 9063CH04CMP4 11/20/07 9:20 PM Page 93 {include file='lib/error.tpl' error=$fp->getError('captcha')}
{include file='footer.tpl'} One thing to notice in this code is that we still prepopulate the captcha field in this form. This is so the user only has to enter it successfully once. For example, if they enter an invalid e-mail address but a valid CAPTCHA phrase, they shouldn’t have to enter the CAPTCHA phrase again after fixing their e-mail address. Figure 4-3 shows the registration form with the CAPTCHA image and the corresponding text input field. Figure 4-3. The registration form with a CAPTCHA image and text input field to receive the phrase from the user CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT94 9063CH04CMP4 11/20/07 9:20 PM Page 94 Validating the CAPTCHA Phrase Finally, we must check that the submitted CAPTCHA phrase matches the one stored in the ses- sion data. To do this, we need to add a new check to the process() method in FormProcessor_ UserRegistration. We also need to clear the saved phrase once the form is completed. This is so a new phrase is generated the next time the user tries to do anything that requires CAPTCHA authentication. Listing 4-16 shows the additions to FormProcessor_UserRegistration that check for a valid phrase and clear out the phrase upon completion. Listing 4-16. Validating the Submitted CAPTCHA Phrase (UserRegistration.php) captcha = $this->sanitize($request->getPost('captcha')); if ($this->captcha != $session->phrase) $this->addError('captcha', 'Please enter the correct phrase'); // if no errors have occurred, save the user if (!$this->hasError()) { $this->user->save(); unset($session->phrase); } // return true if no errors have occurred return !$this->hasError(); } } ?> Adding E-mail Functionality The final function we must add to the user registration system is one that sends the newly reg- istered user a confirmation of their account, as well as their randomly generated password so they can log in. Sending them their password by e-mail is an easy way to validate their e-mail address. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 95 9063CH04CMP4 11/20/07 9:20 PM Page 95 To send e-mail from our application, we will use the Zend_Mail component of the Zend Framework. We could instead use the PHP mail() function, but by using a class such as this (or even PEAR’s Mail_Mime), we can do a whole lot more, such as attaching files (including images) and sending HTML e-mail. We won’t be doing either in this book, but if you ever wanted to add such functionality, the key code would already be in place. Listing 4-17 shows a basic example of using Zend_Mail. This script sends a single e-mail to the address specified with the call to addTo(). You can use this script to ensure that your e-mail server is correctly sending e-mail (remember to update the recipient address to your own). Listing 4-17. Example Usage of Zend_Mail to Send an E-mail (listing-4-17.php) setBodyText('E-mail body'); $mail->setFrom('from@example.com'); $mail->addTo('to@example.com'); $mail->setSubject('E-mail Subject'); $mail->send(); ?> Before we can make our user registration system send out an e-mail, we must first add functionality to DatabaseObject_User for sending e-mail to users—this will allow us to easily send other e-mail messages to users as well (such as instructions for resetting a forgotten password). We will use Smarty for e-mail templates, just as we do for outputting the web site HTML. Our e-mail templates will be structured so the first line of the template is the e-mail subject, while the rest of the file constitutes the e-mail body. Listing 4-18 shows the sendEmail() function, which we will add to the DatabaseObject_ User class. It takes the filename of a template as the argument, and feeds it through Smarty before using Zend_Mail to send the resulting e-mail body to the user. Listing 4-18. A Helper Function Used to Send E-mail to Users (User.php) user = $this; // fetch the e-mail body $body = $templater->render('email/' . $tpl); CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT96 9063CH04CMP4 11/20/07 9:20 PM Page 96 // extract the subject from the first line list($subject, $body) = preg_split('/\r|\n/', $body, 2); // now set up and send the e-mail $mail = new Zend_Mail(); // set the to address and the user's full name in the 'to' line $mail->addTo($this->profile->email, trim($this->profile->first_name . ' ' . $this->profile->last_name)); // get the admin 'from' details from the config $mail->setFrom(Zend_Registry::get('config')->email->from->email, Zend_Registry::get('config')->email->from->name); // set the subject and body and send the mail $mail->setSubject(trim($subject)); $mail->setBodyText(trim($body)); $mail->send(); } // ... other code } ?> In this code, we first instantiate the Templater class and assign to it $this, so we can access all user details (including the profile) from within the e-mail template passed in via the $tpl argument. Next, we use the render() method to retrieve the template output. In this function, we want the string returned, so we can extract the subject and then send it via e-mail. Addition- ally, this code forces all e-mail templates to be within the e-mail directory inside the template directory (./templates/email). The call to preg_split() is what we use to extract the subject. The regular expression used simply finds a newline (\n) or a carriage return (\r) to split on. The third argument (the num- ber 2) splits the string into a maximum of two items. The other important thing to notice in this code is how we set the from e-mail address and name: we add two new values in the application settings file (settings.ini). Listing 4-19 shows the updated version of settings.ini. The values here are somewhat generic; you can set them to reflect your own needs. Listing 4-19. The Updated Application Settings with System Administrator Contact Details (settings.ini) [development] database.type = pdo_mysql database.hostname = localhost CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 97 9063CH04CMP4 11/20/07 9:20 PM Page 97 database.username = phpweb20 database.password = myPassword database.database = phpweb20 paths.base = /var/www/phpweb20 paths.data = /var/www/phpweb20/data paths.templates = /var/www/phpweb20/templates logging.file = /var/www/phpweb20/data/logs/debug.log email.from.name = "System Administrator" email.from.email = "noreply@localhost" Now we can update the postInsert() method in DatabaseObject_User to send the user a welcome e-mail. As you may recall from Chapter 3, this callback method is executed after a new record has successfully been inserted into the database using DatabaseObject’s save() method. Listing 4-20 shows the updated version of postInsert(), which will send an e-mail using user-register.tpl once the user’s profile has been saved. Listing 4-20. Adding an Automated Call to sendEmail() when a New User is Added (User.php) profile->setUserId($this->getId()); $this->profile->save(false); $this->sendEmail('user-register.tpl'); return true; } // ... other code } ?> All that remains now is to create the e-mail template and make the new password avail- able from within that template. When we initially created DatabaseObject_User, we used the uniqid() function generate a random password. We will now update this to use the PEAR Text_Password class we installed for our CAPTCHA implementation to generate a better pass- word. Additionally, since passwords are stored in the database using MD5, we must record the password before it is encrypted so we can include it in the e-mail template. We will do this by storing the generated password as a property in the current DatabaseObject_User object so it is available from the template. We will also need to initialize this property at the top of the class. Listing 4-21 shows the changes to the preInsert() callback CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT98 9063CH04CMP4 11/20/07 9:20 PM Page 98 of DatabaseObject_User, and the new initialization of the $_newPassword property. This property must be public so the template can access its value. Listing 4-21. Creating a Pronounceable Password with Text_Password (User.php) _newPassword = Text_Password::create(8); $this->password = $this->_newPassword; return true; } // ... other code } ?> Finally, we can create the user-register.tpl template. As mentioned previously, the first line of this file will be used as the e-mail subject. This is useful, as it allows us to include tem- plate logic in the e-mail subject as well as in the body. We will include the user’s first name in the e-mail subject. Listing 4-22 shows the contents of user-register.tpl, which is stored in ./templates/ email. You may want to customize this template to suit your own requirements. Listing 4-22. The E-mail Template Used when New Users Register (user-register.tpl) {$user->profile->first_name}, Thank You For Your Registration Dear {$user->profile->first_name}, Thank you for your registration. Your login details are as follows: Login URL: http://phpweb20/account/login Username: {$user->username} Password: {$user->_newPassword} Sincerely, Web Site Administrator CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 99 9063CH04CMP4 11/20/07 9:20 PM Page 99 Figure 4-4 shows how the e-mail will look when received by the user. Hopefully the user’s e-mail client will make the login URL clickable. You could choose to use an HTML e-mail instead, but if the e-mail client can’t automatically highlight links in a text e-mail, it probably can’t render HTML e-mails either. Figure 4-4. An example of the e-mail sent to a user when they register Implementing Account Login and Logout Now that users have a way of registering on the system, we must allow them to log in to their account. We do that by adding a new action to the account controller, which we will call login. In Chapter 3 we looked at how to authenticate using Zend_Auth (see Listing 3-5). We will now implement this functionality. The basic algorithm for the login action is as follows: 1. Display the login form. 2. If the user submits the form, try to authenticate them with Zend_Auth. 3. If they successfully authenticate, write their identity to the session and redirect them to their account home page (or to the protected page they originally requested). 4. If their authentication attempt was unsuccessful, display the login form again, indicat- ing that an error occurred. In addition to this, we also want to make use of our logging capabilities. We will make a log entry for both successful and unsuccessful login attempts. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT100 9063CH04CMP4 11/20/07 9:20 PM Page 100 Creating the Login Template Before we implement the login action in our account controller, we’ll quickly take a look at the login form. Listing 4-23 shows the login.tpl template, which we will store in./templates/ account. Listing 4-23. The Account Login Form (login.tpl) {include file='header.tpl'}
Log In to Your Account
{include file='lib/error.tpl' error=$errors.username}
{include file='lib/error.tpl' error=$errors.password}
{include file='footer.tpl'} This form is very similar in structure to the registration form, except it only contains input fields for username and password. Additionally, we use the password type for the password field, instead of the text type. This template also relies on the presence of an array called $errors, which is generated by the login action. This form also includes a hidden form variable called redirect. The value of this field indicates the relative page URL where the user will end up once they successfully log in. This is necessary because sometimes a user will go directly to a page that requires authentication, but they will not yet be authenticated. If users were automatically redirected to their account CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 101 9063CH04CMP4 11/20/07 9:20 PM Page 101 home, they would then have to navigate back to the page they originally wanted, which they would find annoying. We will set the value for $redirect in the login action. Figure 4-5 shows the login form. Again, it is bland, but we will improve on it in Chapter 6. Figure 4-5. The user login form Adding the Account Controller Login Action Now we need to add the loginAction() method to the account controller. This is the most complex action handler we’ve created so far, although all it does is perform the four points listed at the start of the “Implementing Account Login and Logout” section. Listing 4-24 shows the code for loginAction(), which belongs in the AccountController.php file. Listing 4-24. Processing User Login Attempts (AccountController.php) hasIdentity()) $this->_redirect('/account'); $request = $this->getRequest(); // determine the page the user was originally trying to request $redirect = $request->getPost('redirect'); if (strlen($redirect) == 0) $redirect = $request->getServer('REQUEST_URI'); if (strlen($redirect) == 0) $redirect = '/account'; // initialize errors $errors = array(); // process login if request method is post if ($request->isPost()) { // fetch login details from form and validate them $username = $request->getPost('username'); $password = $request->getPost('password'); if (strlen($username) == 0) $errors['username'] = 'Required field must not be blank'; if (strlen($password) == 0) $errors['password'] = 'Required field must not be blank'; if (count($errors) == 0) { // setup the authentication adapter $adapter = new Zend_Auth_Adapter_DbTable($this->db, 'users', 'username', 'password', 'md5(?)'); $adapter->setIdentity($username); $adapter->setCredential($password); // try and authenticate the user $result = $auth->authenticate($adapter); if ($result->isValid()) { $user = new DatabaseObject_User($this->db); $user->load($adapter->getResultRowObject()->user_id); CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 103 9063CH04CMP4 11/20/07 9:20 PM Page 103 // record login attempt $user->loginSuccess(); // create identity data and write it to session $identity = $user->createAuthIdentity(); $auth->getStorage()->write($identity); // send user to page they originally request $this->_redirect($redirect); } // record failed login attempt DatabaseObject_User::LoginFailure($username, $result->getCode()); $errors['username'] = 'Your login details were invalid'; } } $this->view->errors = $errors; $this->view->redirect = $redirect; } } ?> The first thing this function does is check whether or not the user has already been authenticated. If they have, they are redirected back to their account home page. Next we try to determine the page they were originally trying to access. If they have sub- mitted the login form, this value will be in the redirect form value. If not, we simply use the $_SERVER['REQUEST_URI'] value to determine where they came from. If we still can’t determine where they came from, we just use their account home page as the default destination. We haven’t yet created the action to display their account home page; we will do that in the “Implementing Account Management” section later in this chapter. ■Note Because the ACL manager forwarded the request to the login handler (as opposed to using an HTTP redirect), the server variable REQUEST_URI will contain the location originally requested. If a redirect was used to display the login form, you could use the HTTP_REFERER value instead. We then define an empty array to hold error messages. This is done here so it can be assigned to the template whether a login attempt has occurred or not. Next we check whether or not the login form has been submitted by checking the $request object’s isPost() method (we also did this earlier when processing user registra- tions). If it has been submitted, we retrieve the submitted username and password values from the request data. If either of these is empty, we set corresponding error messages and proceed to display the login template again. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT104 9063CH04CMP4 11/20/07 9:20 PM Page 104 Once we have determined that both a username and password have been submitted, we try to authenticate the user. This code is very similar to that of Listing 3-4. If we determine that the login attempt was successful, we perform three actions: 1. Record the successful login attempt. When a user successfully logs in, we want to make a note of this in the application log file. To do so, we will add a utility function to DatabaseObject_User called loginSuccess(). This function will also update the ts_last_login field in the user table to record the timestamp of the user’s most recent login. We will look at the loginSuccess() function shortly. This function must be called after a user record has been loaded in DatabaseObject_User. 2. Update the identity data stored in session to include all of the values in the corre- sponding database row for this user. By default, only the supplied username will be stored as the identity; however, since we want to display other user details (such as their name or e-mail address) we need to update the stored identity to include those other details: •We can retrieve the data we want to save as the identity by using the createAuthIdentity() method in DatabaseObject_User. This function returns a generic PHP object holding the user’s details. • The storage object returned from Zend_Auth’s getStorage() method has a method called write(), which we can use to overwrite the existing identity with the data returned from createAuthIdentity(). 3. Redirect the user to their previously requested page. This is achieved simply by call- ing the _redirect() method with the $redirect variable as its only argument. Alternatively, if the login attempt failed, the code will continue on. At this point, we call the LoginFailure() method from the DatabaseObject_User class to write this failed attempt to the log file. We will look at this method shortly. We then write a message to the $errors array and continue on to display the template. As mentioned in Chapter 3, we can determine the exact reason why the login attempt failed, and we will record this reason in the log file. However, this isn’t information that should be provided to the user. ■Note Until you add the functions in the next section, a PHP error will occur if you try to log in. Logging Successful and Failed Login Attempts To log both successful and unsuccessful login attempts, we will implement two utility func- tions in DatabaseObject_User: loginSuccess() and LoginFailure(). Listing 4-25 shows these functions as they appear within the DatabaseObject_User class (User.php). Note that LoginFailure() is a static method, while loginSuccess() must be called after a user record has been loaded. I’ve also included the createAuthIdentity() method as described in the previous section. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 105 9063CH04CMP4 11/20/07 9:20 PM Page 105 Listing 4-25. Auditing Login Attempts by Writing Them to the Application Log (User.php) user_id = $this->getId(); $identity->username = $this->username; $identity->user_type = $this->user_type; $identity->first_name = $this->profile->first_name; $identity->last_name = $this->profile->last_name; $identity->email = $this->profile->email; return $identity; } public function loginSuccess() { $this->ts_last_login = time(); $this->save(); $message = sprintf('Successful login attempt from %s user %s', $_SERVER['REMOTE_ADDR'], $this->username); $logger = Zend_Registry::get('logger'); $logger->notice($message); } static public function LoginFailure($username, $code = '') { switch ($code) { case Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND: $reason = 'Unknown username'; break; case Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS: $reason = 'Multiple users found with this username'; break; case Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID: $reason = 'Invalid password'; break; default: $reason = ''; } CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT106 9063CH04CMP4 11/20/07 9:20 PM Page 106 $message = sprintf('Failed login attempt from %s user %s', $_SERVER['REMOTE_ADDR'], $username); if (strlen($reason) > 0) $message .= sprintf(' (%s)', $reason); $logger = Zend_Registry::get('logger'); $logger->warn($message); } // ... other code } ?> The first thing we do in LoginSuccess() is update the users table to set the ts_last_login field to the current date and time for the user that has just logged in. It is for this reason (updating the database) that we pass in the database connection as the first argument. We then fetch the $logger object from the application registry so we can write a message indicating that the given user just logged in. We also include the IP address of the user. LoginFailure() is essentially the same as loginSuccess(), except we do not make any data- base updates. Also, the function accepts the error code generated during the login attempt (retrieved with the getCode() method on the authentication result object in Listing 4-24), which we use to generate extra information to write to the log. We log this message as a warning, since it’s of greater importance than a successful login. Please be aware that if you try to log in now you will be redirected to the account home page (http://phpweb20/account) which we will be creating shortly. ■Tip The reason you want to track failed logins separately from successful logins (using different priority levels) is that a successful login typically indicates “normal operation,” while a failed login may indicate that somebody is trying to gain unauthorized access to an account. Being able to filter the log easily by the mes- sage type helps you easily identify potential problems that have occurred or are occurring. In Chapter 14 we will look at how to make use of this log file. Logging Users Out of Their Accounts It is important to give users the option of logging out of their accounts, as they may want to ensure that nobody can use their account (maliciously or otherwise) after they are finished with their session. It is very straightforward to log a user out when using Zend_Auth. Because the presence of an identity in the session is what determines whether or not a user is logged in, all we need to do is clear that identity to log them out. To do this, we simply use the clearIdentity() method of the instance of Zend_Auth. We can then redirect the user somewhere else, so they can continue to use the site if they please. I simply chose to redirect them back to the login page. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 107 9063CH04CMP4 11/20/07 9:20 PM Page 107 Listing 4-26 shows the logoutAction() method which is used to clear user identity data. Users can log out by visiting http://phpweb20/account/logout. Listing 4-26. Logging Out a User and Redirecting Them Back to the Login Page (AccountController.php) clearIdentity(); $this->_redirect('/account/login'); } } ?> ■Note You could use _forward('login') in Listing 4-26 instead of _redirect('/account/login') if you wanted to. However, if you forwarded the request to the login page, the $redirect variable in loginAction() would be set to load the logout page (/account/logout) as soon as a user logged in— they would never be able to log in to their account unless they manually typed in a different URL first! Dealing with Forgotten Passwords Now that we have added login functionality, we must also allow users who have forgotten their passwords to access their accounts. Because we store the user password as an MD5 hash of the actual password, we cannot send them the old password. Instead, when they complete the fetch-password form, we will generate a new password and send that to them. We can’t automatically assume that the person who filled out the fetch-password form is the account holder, so we won’t update the actual account password until their identity has been verified. We do this by providing a link in the sent e-mail that will confirm the password change. This has the added advantage of allowing them to remember their old password after filling out the form and before clicking the confirmation link. The basic algorithm for implementing fetch-password functionality is as follows: 1. Display a form to the user asking for their username. 2. If the supplied username is found, generate a new password and write it to their pro- file, and then send an e-mail to the address associated with the account informing them of their new password. 3. If the supplied username is not found, display an error message to the user. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT108 9063CH04CMP4 11/20/07 9:20 PM Page 108 So that we don’t have to mess around with application permissions, we will handle three different actions in the new fetch-password controller action: 1. Display and process the user form. 2. Display the confirmation message. 3. Update the user account when the password-update confirmation link is clicked and indicate to the user that this has occurred. Resetting a User’s Password Before we implement the required application logic for fetch password, let’s create the web page template we will use. Listing 4-27 shows the contents of fetchpassword.tpl, which we will store in the account template directory. This template handles each of the three cases out- lined previously. Listing 4-27. The Template Used for the Fetch-Password Tool (fetchpassword.tpl) {include file='header.tpl'} {if $action == 'confirm'} {if $errors|@count == 0}

Your new password has now been activated.

{else}

Your new password was not confirmed. Please double-check the link sent to you by e-mail, or try using the Fetch Password tool again.

{/if} {elseif $action == 'complete'}

A password has been sent to your account e-mail address containing your new password. You must click the link in this e-mail to activate the new password.

{else}
Fetch Your Password
CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 109 9063CH04CMP4 11/20/07 9:20 PM Page 109 {include file='lib/error.tpl' error=$errors.username}
{/if} {include file='footer.tpl'} This template is divided into three parts. The first is used when a user tries to confirm their new password. Within this section is a section for successful confirmation, and another to display a message if the confirmation URL is invalid. The next section (for the complete action) is used after the user submits the fetch-pass- word form with a valid username. The final section is the default part of the template, which is shown when the user initially visits the fetch-password tool, or if they enter an invalid user- name. Now let’s take a look at the new controller action. I called this action handler fetchpasswordAction(), as you can see in Listing 4-28. This code is to be added to the AccountController.php file in ./include/Controllers. Listing 4-28. Handling the Fetch-Password Request (AccountController.php) hasIdentity()) $this->_redirect('/account'); $errors = array(); $action = $this->getRequest()->getQuery('action'); if ($this->getRequest()->isPost()) $action = 'submit'; switch ($action) { CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT110 9063CH04CMP4 11/20/07 9:20 PM Page 110 case 'submit': $username = trim($this->getRequest()->getPost('username')); if (strlen($username) == 0) { $errors['username'] = 'Required field must not be blank'; } else { $user = new DatabaseObject_User($this->db); if ($user->load($username, 'username')) { $user->fetchPassword(); $url = '/account/fetchpassword?action=complete'; $this->_redirect($url); } else $errors['username'] = 'Specified user not found'; } break; case 'complete': // nothing to do break; case 'confirm': $id = $this->getRequest()->getQuery('id'); $key = $this->getRequest()->getQuery('key'); $user = new DatabaseObject_User($this->db); if (!$user->load($id)) $errors['confirm'] = 'Error confirming new password'; else if (!$user->confirmNewPassword($key)) $errors['confirm'] = 'Error confirming new password'; break; } $this->view->errors = $errors; $this->view->action = $action; } } ?> In this code, we first redirect the user back to the account home page if they are authenti- cated. Next we try to determine the action the user is trying to perform. When a user initially visits the fetch-password page (http://phpweb20/account/fetchpassword), no action will be set. As such, the entire switch statement will be skipped. If the request method for the current request is POST, we assume the user submitted the fetch-password form, so we update the $action variable accordingly. If the form has been filled out correctly and a valid username has been specified, the DatabaseObject_User:: CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 111 9063CH04CMP4 11/20/07 9:20 PM Page 111 fetchPassword() method is called. This is a utility function we will define shortly (along with confirmNewPassword()). Once this has been called, we redirect back to the fetch-password page, indicating that the action has completed by putting action=complete in the URL. As you can see in the switch statement, there is nothing to actually do for this action; it is just included there for completeness. The other action is the confirm action. This code is executed when the user clicks on the link we send them in the fetch-password e-mail (which we will look at shortly). We then try to confirm their new password using the submitted key value. Functions for Resetting Passwords There are two functions we need to add to DatabaseObject_User to implement the password resetting. The first is called fetchPassword(), which does the following: 1. Generates a new password using Text_Password. 2. Writes the new password to the user profile. 3. Writes the current date and time to the user profile, so we can ensure the new pass- word can only be confirmed within one day. 4. Generates a key that must be supplied by the user to confirm their new password. We also write this to the user profile. 5. Saves the profile. 6. Sends an e-mail to the user using the fetch-password.tpl e-mail template (separate from the fetchpassword.tpl page template created previously). The second function we will add is called confirmNewPassword(), which confirms the user’s new password after they click the link in the e-mail sent to them. This function works as follows: 1. Checks that the new password, timestamp, and confirmation key exist in the profile. 2. Checks that the confirmation is taking place within a day of the stored timestamp. 3. Checks that the supplied key matches the key stored in the user profile. 4. Updates the user record to use the new password. 5. Removes the values from the profile. 6. Saves the user (which will also save the profile). Listing 4-29 shows these two new functions, which belong in the DatabaseObject_User class (User.php). Listing 4-29. Utility Functions Used for Resetting a User’s Password (User.php) isSaved()) return false; // generate new password properties $this->_newPassword = Text_Password::create(8); $this->profile->new_password = md5($this->_newPassword); $this->profile->new_password_ts = time(); $this->profile->new_password_key = md5(uniqid() . $this->getId() . $this->_newPassword); // save new password to profile and send e-mail $this->profile->save(); $this->sendEmail('user-fetch-password.tpl'); return true; } public function confirmNewPassword($key) { // check that valid password reset data is set if (!isset($this->profile->new_password) || !isset($this->profile->new_password_ts) || !isset($this->profile->new_password_key)) { return false; } // check if the password is being confirm within a day if (time() - $this->profile->new_password_ts > 86400) return false; // check that the key is correct if ($this->profile->new_password_key != $key) return false; // everything is valid, now update the account to use the new password // bypass the local setter as new_password is already an md5 parent::__set('password', $this->profile->new_password); unset($this->profile->new_password); unset($this->profile->new_password_ts); unset($this->profile->new_password_key); // finally, save the updated user record and the updated profile CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 113 9063CH04CMP4 11/20/07 9:20 PM Page 113 return $this->save(); } // ... other code } ?> Now we just need to create the e-mail template. In this e-mail, we will generate the URL that the user needs to click on in order to reset their password. If you refer back to the fetchpasswordAction() function in AccountController.php (Listing 4-28), you will see that the arguments required are the action parameter (set to confirm), the id parameter (which corresponds to the user_id column in the users table), and the key parameter (which is the new_password_key value we generated in DatabaseObject::fetchPassword()). Listing 4-30 shows the e-mail template, which we will store in user-fetch-password.tpl in the ./templates/email directory. Remember that the first line is the e-mail subject. Listing 4-30. The E-mail Template Used to Send a User Their New Password (user-fetch-password.tpl) {$user->profile->first_name}, Your Account Password Dear {$user->profile->first_name}, You recently requested a password reset as you had forgotten your password. Your new password is listed below. To activate this password, click this link: Activate Password: http://phpweb20/account/fetchpassword? ➥ action=confirm&id={$user->getId()}&key={$user->profile->new_password_key} Username: {$user->username} New Password: {$user->_newPassword} If you didn't request a password reset, please ignore this message and your password will remain unchanged. Sincerely, Web Site Administrator Figure 4-6 shows a sample of the e-mail that is sent when a new password is requested. Take special note of the URL that is generated, and the different parts in the URL that we use in fetchpasswordAction(). ■Note One small potential problem is the length of the URL in the e-mail. Some e-mail clients may wrap this URL across two lines, resulting in it not being highlighted properly (or if the user manually copies and pastes the URL, they may miss part of it). You may prefer to generate a shorter key or action name to reduce its length. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT114 9063CH04CMP4 11/20/07 9:20 PM Page 114 Figure 4-6. The fetch password e-mail sent to a user There’s one more small issue we must now address: if a user requests a new password, and then logs in with their old password without using the new password, we want to remove the new password details from their profile. To do this, we update the loginSuccess() method in DatabaseObject_User to clear this data. Listing 4-31 shows the updated version of this method as it appears in the User.php file. We place the three calls to unset() before calling the save() method, so the user record only needs saving once. Listing 4-31. Clearing the Password Reset Fields if They Are Set (User.php) ts_last_login = time(); unset($this->profile->new_password); unset($this->profile->new_password_ts); unset($this->profile->new_password_key); $this->save(); $message = sprintf('Successful login attempt from %s user %s', CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 115 9063CH04CMP4 11/20/07 9:20 PM Page 115 $_SERVER['REMOTE_ADDR'], $this->username); $logger = Zend_Registry::get('logger'); $logger->notice($message); } // ... other code } ?> Finally, as shown in Listing 4-32, we must add a link to the original login form (login.tpl in ./templates/account) so the user can access the fetch-password tool if required. Listing 4-32. Linking to the Fetch-Password Tool from the Account Login Page (login.tpl)
Log In to Your Account
Implementing Account Management Earlier in this chapter we implemented the login and logout system for user accounts. When a user successfully logged in, the code would redirect them to the page they initially requested. In many cases, this will be their account home page (which has the URL http://phpweb20/account). So far, however, we haven’t actually implemented this action in the AccountController class. In this section, we will first create this action (indexAction()), although there isn’t terribly much that this will do right now. Next, we will update the site header template so it has more useful navigation (even if it is still unstyled). This will include additional menu options for logged-in users only. Finally, we will allow users to update their account details. Creating the Account Home Page After a user logs in, they are allowed to access their account home page by using the index action in the account controller. Listing 4-33 shows the code for indexAction() in AccountController.php, which at this stage doesn’t do anything of great interest, other than display the index.tpl template in ./templates/account. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT116 9063CH04CMP4 11/20/07 9:20 PM Page 116 Listing 4-33. The Account Home Page Action Controller (AccountController.php) Before we look at index.tpl, we will make a small but important change to the CustomControllerAction.php file. We are going to change it so the logged-in user’s identity data is automatically assigned to the template, thereby making it available within all site tem- plates. This is the data we generated in the createAuthIdentity() method in Listing 4-25. Additionally, we will assign a variable called $authenticated, which is true if identity data exists. We could use {if isset($identity)} in our templates instead of this variable, but we would then be making an assumption that the presence of the $identity means the user is logged in (and vice versa). To make this change, we need to implement the preDispatch() method, as shown in Listing 4-34. This method is automatically called by Zend_Controller_Front at the start of dis- patching any action. We can make this change to CustomControllerAction, since all controllers in our application extend from this class. Listing 4-34. Assigning Identity Data Automatically to Templates (CustomControllerAction.php) db = Zend_Registry::get('db'); } public function preDispatch() { $auth = Zend_Auth::getInstance(); if ($auth->hasIdentity()) { $this->view->authenticated = true; $this->view->identity = $auth->getIdentity(); } else $this->view->authenticated = false; } } ?> CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 117 9063CH04CMP4 11/20/07 9:20 PM Page 117 Now let’s look at the index.tpl file, which currently displays a simple welcome message. We can use the first_name property from the identity to personalize the message. Listing 4-35 shows this template, which is stored in ./templates/account. Listing 4-35. Displaying a Welcome Message After a User Logs In to Their Account Home Page (index.tpl) {include file='header.tpl'} Welcome {$identity->first_name}. {include file='footer.tpl'} At this point, you can try to log in by visiting http://phpweb20/account and entering your account details (remember that thanks to the permissions, trying to access this URL will dis- play the page at http://phpweb20/account/login). Updating the Web Site Navigation When we last looked at the navigation in header.tpl, all we had was a home link and a register link. We are now going to improve this navigation to include a few new items: • Log in to account link •Information about the currently logged in user (if any) •A member’s-only submenu, including a logout link To implement the second and third points, we need to check the $authenticated variable we are now assigning to the template. Additionally, once a user has logged in, the login and register links are no longer relevant, so we can hide them. Listing 4-36 shows the updated version of header.tpl, which now includes some basic template logic for the HTML header. For now we are just using vertical pipes to separate menu items, but we will use CSS to improve this in Chapter 6. Listing 4-36. Making the Site Navigation Member-Aware (header.tpl) Title
Home CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT118 9063CH04CMP4 11/20/07 9:20 PM Page 118 {if $authenticated} | Your Account | Update Your Details | Logout {else} | Register | Log In {/if} {if $authenticated}
Logged in as {$identity->first_name|escape} {$identity->last_name|escape} (logout)
{/if}
Figure 4-7 shows the account home page that users are directed to after logging in. Note the new navigation elements, as well as the information about the currently logged-in user. Figure 4-7. The account home page with updated navigation and identity display CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 119 9063CH04CMP4 11/20/07 9:20 PM Page 119 Allowing Users to Update Their Details The final thing we need to add to the user account section for now is the ability for users to update their details. In the new header.tpl shown in Figure 4-7, there is a link labeled Update Your Details, which will allow users to do this. Because this code is largely similar to the user registration system, I have not included all of the repetitive details. The key differences between user registration and updating details are as follows: •We are updating an existing user record rather than creating a new one. •We do not allow the user to update their username. •We allow the user to set a new password. •We do not need the CAPTCHA test. •Because the user is already logged in, we must update their Zend_Auth identity accordingly. ■Note While there isn’t anything inherently bad about allowing users to change their own usernames, it is my own preference to generally not allow users to do so (an exception might be if their e-mail address is used as their login username). One reason why it is bad to allow the changing of usernames is that other users get to know a user by their username; in the case of this application, we will be using the username to generate a unique user home page URL. Changing their username would result in a new URL for their home page. When allowing users to change their password, we will show them a password field and a password confirmation field, requiring them to enter the new password twice in order to change it. Additionally, we will include a note telling them to leave the password field blank if they do not want to change their password. This is because we cannot prepopulate the pass- word field with their existing password, since we only store an MD5 hash of it. To implement the update details function, we must do the following: • Create a new form processor class called FormProcessor_UserDetails, which is similar to FormProcessor_UserRegistration. This class will read the submitted form values and process them to ensure they are valid. If no errors occur when validating the data, the existing user record is updated. • Create a new action called detailsAction() in AccountController that instantiates FormProcessor_UserDetails, and passes to it the ID of the logged-in user. This function also updates the Zend_Auth identity by calling the createAuthIdentity() function in DatabaseObject_User that we created earlier. • Create a confirmation page to confirm to the user that their details have been updated. To do this, we will create a new action handler called detailscompleteAction(), which simply tells the user that their details have been saved. Figure 4-8 shows what the form looks like when initially displayed to users. Note the pre- populated fields, as well as the lack of a username field and the addition of a password field. CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT120 9063CH04CMP4 11/20/07 9:20 PM Page 120 You may want to display the username as a read-only field, but that is a personal preference. If the user tries to remove a value and then submit the form, a corresponding error message will be shown, just as in the registration form. Figure 4-8. The update details form as it is initially shown to users All the code for this section is included with the downloadable application source code. Summary In this chapter we implemented a user registration system, which allows users to create a new account by filling out a web form. This form requires users to enter a CAPTCHA phrase to prove that they are people (and not computer programs). Once the user’s registration is com- plete, their details are saved to the database using DatabaseObject_User and Profile_User, and the users are then sent an e-mail containing their account details. We then added code to the application to allow users to log in to their accounts. We saved their identity to the current session using Zend_Auth so it would be accessible on all pages they visit. Additionally, we added logging capabilities to the login system, so both successful and unsuccessful login attempts would be recorded. Finally, we created a basic account home page, to which users will be redirected after log- ging in. We also added code to let them update their account details. In the next chapter we will move slightly away from the development of the web applica- tion while we take a look at two JavaScript libraries: Prototype and Scriptaculous. We will be using these libraries to help give our application a funky interface and make it “Web 2.0.” CHAPTER 4 ■ USER REGISTRATION, LOGIN, AND LOGOUT 121 9063CH04CMP4 11/20/07 9:20 PM Page 121 9063CH04CMP4 11/20/07 9:20 PM Page 122 Introduction to Prototype and Scriptaculous In this chapter we will be looking at two JavaScript libraries that are designed to help with Web 2.0 and Ajax application development. First, we will look at Prototype, a JavaScript framework developed by Sam Stephenson. Pro- totype simplifies JavaScript development by providing the means to easily write for different platforms (browsers). For example, implementing an Ajax subrequest using XMLHttpRequest can be achieved with the same code in Internet Explorer, Firefox, and Safari. Next, we will look at Scriptaculous, a JavaScript library used to add special effects and improve a web site’s user interface. Scriptaculous is built upon Prototype, so knowing how to use Scriptaculous requires knowledge of how Prototype works. Scriptaculous was created by Thomas Fuchs. We will cover the basic functions of Prototype and look at how it can be used in your web applications. Then we will look at some of the effects that can be achieved with Scriptaculous. Finally, we will look at an example that makes use of Prototype, Scriptaculous, Ajax, and PHP. The code covered in this chapter will not form part of our final web application, but in forthcoming chapters we will use the techniques from this chapter to add various effects and to help with coding clean and maintainable JavaScript. Downloading and Installing Prototype The Prototype JavaScript framework can be downloaded from http://prototypejs.org. At time of writing, the latest release version of Prototype is 1.5.1.1, and it is a single JavaScript file that you include in your HTML files. For example, if you store your JavaScript code in the /js directory on your web site, you would use the following HTML code to include Prototype: Loading the Prototype library 123 CHAPTER 5 9063CH05CMP2 10/29/07 8:39 PM Page 123 ■Note At time of writing, Prototype 1.5.1.1 is the latest stable release; however, version 1.6 is close to being released. This new version will introduce several key features and improvements in the event handling model of Prototype (as well as many other enhancements). Prototype Documentation You can find comprehensive documentation for all the functionality provided by Prototype at http://prototypejs.org/api. I highly recommend you look through this site, as it will provide details about Prototype beyond what I can cover in this chapter. Additionally, you may find value in perusing the Prototype source code. Doing so may give you a feel not only for how certain functions work but also to see a good example of how to use various aspects of Prototype. Selecting Objects in the Document Object Model There are several functions available in Prototype for selecting elements in the Document Object Model (DOM). I recommend that you use the Prototype functions wherever possible instead of methods you may be more used to using (such as document.getElementById()), since they are simpler, they work across different browsers, and they provide you with extra functionality (as you will shortly see). The $() Function The $() function is used to select an element from the Document Object Model (DOM)—in other words, it selects an element on your HTML page. This function is extremely useful and may be one of the most commonly used functions in your JavaScript development. Essentially, $() is a replacement for using document.getElementById(), except that it will also do the following: •Return an array of elements if you pass in multiple arguments (each returned element corresponds to the argument position; that is, the 0 element corresponds to the first argument). •Extend the returned element(s) with extra Prototype functionality (which we will cover in this chapter). Because of this second point, you should always use $() (or one of the other Prototype element selectors we will look at shortly) to select elements in your JavaScript code when you are using Prototype. This will give you the full range of functionality that Prototype provides. Listing 5-1 shows several examples of selecting elements with the $() function. Note that you can pass in an element’s ID or you can pass in the element directly (which effectively will just add the extra Prototype functionality to the element). CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS124 9063CH05CMP2 10/29/07 8:39 PM Page 124 Listing 5-1. Example Usage of the $() Element Selector (listing-5-1.html) Listing 5-1: Example usage of the $() function
The getElementsByClassName() Function If you have multiple elements on a page, all with the same class, you can use the getElementsByClassName() function to select all of them. An array will be returned, with each element corresponding to one element with the given class name. This can be an expensive function to call, as internally every element is analyzed to see if it is of the specified class. Because of this, you should also specify a parent element when call- ing this function. Doing so means only elements within the parent element are checked. You would typically use this function when you want a make the same update to all ele- ments of a particular class. For example, suppose you had an HTML page with several boxes on it, each having the class name .box, contained within a div called #box-container. If you wanted to add a Hide All or Show All button on your HTML page, you could select all ele- ments using document.getElementsByClassName('box', 'box-container'), and then loop over each element and hide or show it accordingly. Listing 5-2 demonstrates this. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 125 9063CH05CMP2 10/29/07 8:39 PM Page 125 Listing 5-2. Sample Usage of getElementsByClassName (listing-5-2.html) Listing 5-2: Hiding or showing boxes using document.getElementsByClassName()

Box 1

Box 2

In the preceding code, you will see a call to a method called hide() and a call to a method called show(). These are both functions provided by Prototype, which simply hide or show the respective element. These are examples of the extra functionality provided when using the Prototype element selectors. We will cover more of these later in this chapter. After the code fetches all of the box elements, it loops over them in both the showAll() and hideAll() functions to show or hide the element. There is another way you can shorten this code and easily apply the same code to all returned elements: you can use either the each() method or the invoke() method. These are two functions Prototype adds to all arrays. Listing 5-3 shows the methods in Listing 5-2 rewrit- ten to use each(). Listing 5-3. Using each() to Iterate Over the Returned Elements (listing-5-3.html) CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 127 9063CH05CMP2 10/29/07 8:39 PM Page 127 This code passes a function as the argument to each(). This function is executed once for each item in the array each() is called on. The argument passed to this function is the element in question, thereby allowing us to call hide() or show() directly on it. ■Note Although I didn’t use it in this case, the second argument passed to the function inside each() contains the loop number. For example, function(s, idx) { … } would pass 0 in the idx parameter for the first element, 1 for the second, and so on. Alternatively, you can use invoke() instead of each(). This allows you to call a single method on each element, with an arbitrary number of arguments. This would work perfectly in this hide/show example, as we are just calling these methods for each box. However, if you needed to execute multiple lines of code, you would need to go back to using each(). Listing 5-4 shows the hideAll() and showAll() functions with a call to invoke(). Note that the method you want to invoke on each array element is passed as a string. Listing 5-4. Using invoke() to Call a Single Method on Each Array Element (listing-5-4.html) ■Tip You can also call getElementsByClassName() directly on an element (rather than passing it as the first argument). For instance, you could select all .box-container elements as in the previous example by using $('box').getElementsByClassName('box-container'). The $$() Function The $$() function (not to be confused with the $() function discussed previously) is a very powerful function that allows you to select elements using CSS rules. All returned elements are extended with extra Prototype functionality, just as $() does. Note, however, that an array is returned, even if only a single element is found. The ordering of elements in the array is the order of the elements in the document. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS128 9063CH05CMP2 10/29/07 8:39 PM Page 128 A CSS rule is a string used to specify elements in Cascading Style Sheets (CSS) documents, using a combination of element names (such as div), class names (such as .box), and element ID names (such as #box-content). For example, in Listing 5-2 we could have used var elts = $$('#box-container .box') instead of using the call to document.getElementsByClassName(). Here are some more examples: • $$('form'): Selects all forms on a page • $$('div.box'): Selects all div elements that have the class name box • $$('div#logo img'): Selects the img element within the div called #logo • $$('input[type=radio]'): Selects all inputs that are radio buttons So why not just use $$() solely, and forget about $() and getElementsByClassName()? Yes, $$() can do exactly what the other two functions can do, but it is more expensive to call. That is, it is less efficient. If you want to select an element whose ID you know, you should use $('element-id') instead of $$('#element-id'), since the former is more efficient (also, using $$() returns an array, and $() doesn’t in this case). If you want to select all elements with a certain class (such as class .box inside a div with ID #box-container), you should use $('box-container'). getElementsByClassName('box') instead of $$('#box-container .box'). One recommendation from the Prototype documentation (found on http://proto- typejs.org/api), is that if you do use $$(), try to narrow the search down by specifying a parent element’s ID at the start of the CSS rule. In other words, $$('#box-container .box') would be more efficient than $$('.box'), as the former would only search within the #box- container element for elements with class .box, while the latter would search the entire DOM. If you are familiar with CSS, using $$() will be far easier to read and write, but from a per- formance point of view you should try to avoid it if there is a more efficient solution. For simplicity, I will continue to use $$() in the examples. The getElementsBySelector() Function It is possible to use the same syntax as in $$() but to only look within a particular element rather than the whole document. This can be achieved by calling the getElementsBySelector() func- tion directly on an element. For example, you can use $('box-container').getElementsBySelector('.box') to find all elements that have class .box inside the #box-container element. Prototype’s Hash Object Prototype provides an object type called Hash, which is essentially a normal JavaScript object that has been extended. I am covering it here simply because I will be referring to the Hash object in the future. It could also be referred to as an associative array, but I will call it a hash. If you are unfamiliar with JavaScript objects, they can be created and used as follows: To extend this object with extra Prototype functionality, the $H() function is used. This essentially converts the created object into a hash. So the preceding code would be modified as follows: Doing this not only allows you to understand what a hash is, but it also provides the following extra functionality: • each(): Allows you to loop over each key/value pair, similar to how you would with arrays in Prototype. • remove(): Removes a value from the hash based on the specified key (for example, person.remove('age') will remove the age element from the hash in the previous example). • toQueryString(): Serializes the keys and values into a usable query string (so the pre- ceding person hash would become name=John+Smith&age=30). ■Note Sometimes you will need to create a hash but you will not require the extended functionality (such as when defining options to be passed to Ajax.Request). In this case, you can forego calling $H(), but I will still refer to it as a hash even though strictly speaking it is a generic JavaScript object. Other Element Extensions In the previous section I stated that when using a function such as $() or $$() in Prototype, the returned elements are extended. That is, they are given extra functionality that is not nor- mally available when programming in JavaScript. We looked at a couple of these added functions (namely show() and hide()), but there are many more functions provided. We will take a brief look at the some of the more useful of these and at how you can use them in your everyday JavaScript development. Note that which extensions are added depends on the type of element. That is, some new functions will be only available for arrays, and others only for strings. Some new functions are available to all elements. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS130 9063CH05CMP2 10/29/07 8:39 PM Page 130 Showing and Hiding Elements As we saw before, the show() method makes a hidden element visible (or does nothing if the element is already visible), while the hide() method hides a visible element hidden (or does nothing if the element is already hidden). In addition, there is a toggle() method. This will hide a visible element or show a hidden element. You can check whether an element is hidden or not by using the visible() method, which returns true if the element is visible and false if not. Additionally, you can remove an element from the DOM completely by calling its remove() method. Retrieving Dimensions of Elements Prototype provides a method called getDimensions(), which returns the width and height of an element (in the width and height properties). You can retrieve an element’s width by just using getWidth(), or its height by using getHeight(), but if you need both of these values you should use a single call to getDimensions(). This is because both getWidth() and getHeight() will internally make a call to getDimensions(), thereby resulting in an extra unnecessary func- tion call. The following example shows a simple function that accepts the ID of an element and then determines and displays its dimensions in an alert box: Managing Classes of Elements You can easily manipulate an element’s classes with Prototype, which may be of great use for achieving mouseover effects or to allow the user to mark an item as selected. The following functions are available to elements: • addClassName(): Applies a class to an element. This might be useful if you have a high- light class for a selected element. • removeClassName(): Removes a class from an element. This would typically be used at some point after calling addClassName(). • toggleClassName(): Adds or removes a class name (if the element doesn’t have the class, it is added; it is removed if the element already has it). • hasClassName(): Checks whether an element has a particular class. Let’s now look at a practical example of using these methods. Listing 5-5 is slightly more complex than previous examples; it highlights a box when your mouse pointer moves over it, and removes the highlight when the pointer is moved away. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 131 9063CH05CMP2 10/29/07 8:39 PM Page 131 Listing 5-5. Demonstrating addClassName() and removeClassName() (listing-5-5.html) Listing 5-5: Manipulating element class name with Prototype

Box 1

Box 2

CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS132 9063CH05CMP2 10/29/07 8:39 PM Page 132 In this example, there are a series of boxes (with class .box) inside of #box-container, and various styles are defined for this box. I have also defined a .highlight style, which will make the box turn red when the mouse is over it. ■Note The JavaScript code in this example would be unnecessary if the :hover selector worked across all browsers. In Firefox, you could simply use CSS like div.box:hover { background : #f00; }, but this will not work in Internet Explorer (except on links) so the JavaScript solution is required. Essentially, what I want this code to do is as follows: 1. Retrieve all .box elements. 2. Add an onmouseover event to each element, which adds the .highlight class. 3. Add an onmouseout event to each element, which removes the .highlight class. I first use $$('#box-container .box') to select all the boxes, and then use each() on the array of returned elements, as I want to execute several lines of code for each element. (See Listing 5-3 for more information about using each().) Next I set the onmouseover and onmouseout events for each element with a call to addClassName() and removeClassName() respectively. Note that in the event handler, this refers to the element on which the event occurred. ■Caution In order to keep the example somewhat simple, I used a non-preferred way of observing events in JavaScript. The problem with how I added these events is that if either of the onmouseover or onmouseout events had previously been defined on the .box elements, I would have overwritten that han- dler. Conversely, if another script executes after this code, my event handlers may be overwritten. Prototype provides an event handling class that deals with these issues and allows events to be observed correctly between all platforms. We will cover this Event class in the “Event Handling in Prototype” section later in this chapter. Manipulating Strings with Prototype All string elements are extended with several methods, including the following: • truncate(): Shortens a string to a specified length, and optionally appends a string at the end (such as …). For example, you could turn “My short string” into “My short…”. • strip(): Removes whitespace from the beginning and end of a string. • stripTags(): Removes any HTML tags from a string. • stripScripts(): Removes any scripts (such as JavaScript) from a string. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 133 9063CH05CMP2 10/29/07 8:39 PM Page 133 • escapeHTML(): Turns HTML elements into their respective entities (for example, replac- ing < with <) • unescapeHTML(): Performs the opposite of escapeHTML() (for example, turning < into <). There are several more functions available, but these are among the most useful. ■Note Even if you are using functions such as stripTags() and stripScripts() on user-submitted data, you should still be performing these same operations at the server if the data is submitted, since you cannot guarantee the data has passed through the JavaScript code when it reaches the server. Ajax Operations in Prototype One of the key reasons for choosing to use Prototype in this book was not only the extended functionality applied to all elements—which in itself is extremely useful—but also for its Ajax support. Cross-browser Ajax solutions can easily be created by using the Prototype Ajax class. Typical usage of this class involves first defining a hash of options (such as form data that should be submitted in the request), and then instantiating one of Ajax.Request, Ajax.Updater, or Ajax.PeriodicalUpdater: • Ajax.Request: Generally used for a one-time request. This is the core Ajax method avail- able, and it is the function you will call directly to initiate most Ajax operations. • Ajax.Updater: Behaves in the same way as Ajax.Request, except its specific purpose is to populate an element on your HTML page with the response data from a request. This can also be achieved by using Ajax.Request, but Ajax.Updater simplifies the process for this specific operation. • Ajax.PeriodicalUpdater: Behaves the same way as Ajax.Updater in that it populates an element with the Ajax response data; however, it will continue to execute with a speci- fied frequency. For instance, if you need to retrieve fresh data every N seconds, you can use this method. Another way to look at it is that Ajax.PeriodicalUpdater performs a request with Ajax.Updater every N seconds. Ajax Request Options When initiating an Ajax request with Prototype, the one key thing you need is the URL you are requesting. In addition to this, you can define a set of options that dictate the behavior of the request. These options are not required to perform the request (default options are defined internally); however, it is rare that you wouldn’t need to set various options or callbacks. The options you will typically need to set are as follows: • method: The HTTP method used for the request. This is typically get or post (with post being the default). Note that there are other types of HTTP requests possible, but they are typically not used and are beyond the scope of this book. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS134 9063CH05CMP2 10/29/07 8:39 PM Page 134 • parameters: The form data that is included in the request, regardless of whether it is a get or post request. Prototype can accept a wide variety of data formats here (such as a string you have already encoded, or simply a hash). It will convert the data into the required format to complete the request. The following is an example of an options hash that can be used for a Prototype Ajax request: And here is an example of getting the value of a text input field from the existing page and including it in the options hash: In this example, the getValue() function retrieves a form element’s value. This is a function added to form elements by Prototype so their values can be retrieved regardless of their type (whether textarea, checkbox, radio, or other type). Ajax Callback Functions For all Ajax requests you make with Prototype, there are a number of callback functions that can be defined. Each specified callback function will be called automatically at appropriate stages of the Ajax request lifecycle. ■Note You can perform Ajax requests without specifying any event callbacks; however, it will not be possi- ble to use the returned result if you don’t define any callbacks. Sometimes you may not care about the response data, but most of the time you will. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 135 9063CH05CMP2 10/29/07 8:39 PM Page 135 Typically, you will define the callback prior to initiating the Ajax request, and then pass in the function name with the request options (as discussed in the previous section). Each call- back receives the XMLHttpRequest object as its first parameter, thereby allowing you to easily read the response data (including HTTP status code) if it is available. The following are the main callback functions you will typically need to define when han- dling an Ajax request: • onSuccess: This callback is called upon successful completion of a request. A request is successful if no error occurs and if the HTTP status code is in the 2xx family. • onFailure: If a request completes successfully but returns an HTTP status code not in the 2xx family, this callback is invoked. • onComplete: After a request has completed and all other callbacks have been called, the onComplete callback is triggered. In reality, you will probably not need this callback in your requests unless you have some kind of cleanup code that needs to be executed whether a request succeeds or not. ■Note Many Ajax programmers (both in the past and even now) simply check for an exact status code of 200 when trying to determine success. Not all successful HTTP requests will necessarily return this status code, however, so the onSuccess callback should be used instead. Prototype will automatically deal with each of these status codes. Here’s an example of defining the onComplete and onFailure callbacks, combined with the other options you may need in an Ajax request: The callback functions I have defined are somewhat useless, but hopefully they demonstrate how the Ajax request is set up. In reality, I much prefer to define the actual function as its own separate block, and then pass in the function name as the argument in the options hash. An example of this is shown CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS136 9063CH05CMP2 10/29/07 8:39 PM Page 136 next. Note that technically speaking it is a function pointer that is used as the value in the options hash—it’s not simply a string with the function name. In addition to the onSuccess and onFailure callbacks (which encompass a large number of HTTP status codes), Prototype also allows you to easily handle each status code indepen- dently. To do this, you define an onXYZ callback, where XYZ corresponds to the HTTP status code you want to handle. For example, if you wanted a specific function to be called when a 404: File Not Found error occurred, you would pass the on404 callback to the Ajax request options. The following example demonstrates this by creating several callbacks, each to handle various error codes: The XMLHttpRequest Callback Argument In all of the preceding examples, I have included an argument called transport in the callback functions. As I mentioned previously, this argument is the XMLHttpRequest object created as a result of the call to Ajax.Request. ■Note The primary reason for naming this argument transport (and not xhr or something similar) is simply convention. You can call it what you like, but to be consistent you should just call it transport. You can use transport in your callback functions to read the response data. The following properties are available inside the transport variable: • responseText: The response from the request as a string. • responseXML: The response from the request as an XMLDocument object. This allows you to manipulate the response in the same way you would with the normal DOM. I will demonstrate this shortly, in the “An Ajax.Request Example” section. • status: The HTTP status code resulting from the request (such as 200 for a successful request, or 404 for a file-not-found error). • statusText: A textual description for the HTTP status code (such as OK for a status response of 200). So you could modify the handleSuccess() callback from the previous example to show the response data in an alert box using the following code: JavaScript Object Notation (JSON) JavaScript Object Notation, or JSON, is a data-exchange format that is very useful in Ajax- enabled web applications. In essence, JSON is JavaScript code. It is typically used to serialize JavaScript arrays or objects (what I referred to as hashes earlier) into a simple format that can be exchanged between client and server. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS138 9063CH05CMP2 10/29/07 8:39 PM Page 138 ■Note My own personal preference is to use JSON data as the response to Ajax requests, since it’s much easier to manipulate the data. However, since we’re covering Ajax, it’s good to know how the X in Ajax works. As such, I will use XML for the main example in this chapter, but in following chapters, when we add Ajax functionality to our application, we will use JSON and not XML. JSON is used as an alternative to XML for data exchange in Ajax requests because it results in a much smaller payload (since there are no opening/closing tags), and it is typically simpler to access within JavaScript code. For example, the JavaScript code you might use to represent data for a book may look like this: var book = { title : 'Practical PHP Web 2.0 Applications', author : 'Quentin Zervaas' }; Now consider the code you would use in PHP to represent this same data: 'Practical PHP Web 2.0 Applications', 'author' => 'Quentin Zervaas' ); ?> If I wanted to represent this PHP snippet in JavaScript, I would need to somehow create JavaScript code like the preceding, which means creating a string of JSON data. PHP provides a function called json_encode() to do exactly this. The Zend Framework also provides the Zend_Json class, which is what we’ll be using. Earlier versions of PHP do not have the json_encode() function, and by using Zend_Json we don’t have to worry about that. Now, if I wanted to represent the preceding PHP code as JavaScript code, I could call Zend_Json::encode() to do so: This function will generate a string that looks like this: { title : 'Practical PHP Web 2.0 Applications', author : 'Quentin Zervaas' }; While this example serves no great purpose, it demonstrates what is possible with JSON. When a request is made with XMLHttpRequest, the server can return a JSON-encoded string so that the JavaScript code can interpret the results. To interpret the returned data, you can use the JavaScript eval() function, which will evaluate as JavaScript code whatever is passed as its first argument. Thankfully, Prototype simplifies this for us by providing the evalJSON() method. For example, to decode JSON data returned from an Ajax request, you could use code similar to the following: CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 139 9063CH05CMP2 10/29/07 8:39 PM Page 139 In this example, the evalJSON() is an extended method Prototype provides to all strings. The first argument to this method tells Prototype to check for data that isn’t well formed. If the string is not well-formed JavaScript code, eval() is not called internally as a safety precaution. ■Note When Prototype 1.6.0 is released, the responseJSON property will also be available in the response from Ajax requests, saving us the trouble of manually decoding the JSON data as in the preceding example. I will continue using XML in this chapter, just to give you a full taste of how Ajax solutions can be implemented. Our first real taste of JSON will be in Chapter 6, when we add client-side form validation to the user registration form we created in Chapter 4. An Ajax.Request Example Now that we have looked at defining options and callbacks for a request, we can take a look at Ajax.Request, the primary Prototype function used for Ajax. In this example, the code will request an XML file that resides on a web server. It will then loop over the data in the XML file and output it to the browser. At this stage, we won’t be doing anything fancy with the data—we will save the fanciness for when we cover Scriptaculous. Listing 5-6 shows the XML data. This is just made-up data that has no real meaning other than demonstrating the use of Ajax.Request. This data is stored in a file called listing-5-6.xml. Listing 5-6. Sample XML Data to Be Processed in the Ajax.Request Example (listing-5-6.xml) The basic code outline we will use to perform the Ajax request is as follows. We will flesh it out a bit more shortly. ■Note Since Ajax.Request is in fact a class (as opposed to simply being a function), it must be invoked using the new keyword. If new is omitted, the call to Ajax.Request will not work. As you can see, the first argument to Ajax.Request is the URL being requested. In this example, we are simply getting an XML file, but in real-world applications this is likely to be a server-side script (such as a PHP script). The second argument is the list of request options. Here you can also see that we’ve defined callbacks for both success and failure, although they do not yet do anything. Handling XML Data from an Ajax Request As mentioned previously, we can access the responseXML property of the XMLHttpRequest object passed in to the callback. This property is an XMLDocument object, which allows us to manipulate it just as we would the DOM. Referring back to our listing-5-6.xml file in Listing 5-6, we could call getElementsByTagName('person') to find all of the individual people records in the returned XML. Note that the documentElement property is the root node of the XML document, so you can’t actually call getElementsByTagName() directly on the responseXML property. In reality, it would look more like this: This will return an array called people containing all of the person records in the XML document. Strictly speaking, this is actually an HTMLCollection (not an array), but by using the Prototype $A() function, we can turn it into an array and gain the extra array functionality Prototype provides (such as each() and invoke()). CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 141 9063CH05CMP2 10/29/07 8:39 PM Page 141 So, we can modify the handleSuccess() callback to loop over each person, outputting their name in an alert box. This functionality is still somewhat crude, but we will improve it further shortly. We can use the DOM getAttribute() method to fetch a person’s name from the returned person data, as follows: If we want to output a more meaningful message for each returned person, we need to build up a string using the data associated with each user. To do this, we will use Prototype’s Template class. This class probably isn’t something you will often use with Prototype, but it is worth knowing about (particularly since we will use it in later code listings). The Template class allows you to define a template string with placeholders for change- able data. You can then call the evaluate() method on the created template, passing in the data you want to include. The following code shows an updated version of handleSuccess(), which now uses the Template class in combination with Prototype’s each() enumerator: Handling XML That Isn’t Well Formed In all of the preceding examples of handleSuccess(), we have assumed that the XML data is well formed. That is, we assume it is valid and that no errors are contained in the document. This is CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS142 9063CH05CMP2 10/29/07 8:39 PM Page 142 not always going to be the case, especially for dynamically generated XML. Just because an Ajax request is successful doesn’t mean the returned data is correct. Additionally, if the document is well formed but is missing properties that we require (for instance, if the age property is missing from one or more records), this is not an error per se. Prototype does not provide XML-handling functionality, so detecting XML errors across different platforms is not a straightforward task. We will treat an XML parsing error in our code the same way we treat no records being returned. For the sake of completeness, here is code you can use to detect XML parsing errors: Completing the onFailure Error Handler The final part of this example is the handleError() callback. In this particular example, we are doing nothing more than showing an alert box for each person record found. To accompany this, we will simply display an alert box containing the error if one has occurred. The Complete Ajax.Request Example Listing 5-7 contains the complete code for the Ajax.Request example. Listing 5-7. The Complete Ajax.Request Example (listing-5-7.html) Listing 5-7: The complete Ajax.Request example CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 143 9063CH05CMP2 10/29/07 8:39 PM Page 143
CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS144 9063CH05CMP2 10/29/07 8:39 PM Page 144 When you load listing-5-7.html in your browser, all you will see is a form button that says Load XML. At the end of the code, the click event handler is added to this button using Event.observe(), which simply calls the loadXml() function when the event is triggered. Note that we could have created the button with a line like this: However, as noted earlier in this chapter, using the Prototype event-handling code is the pre- ferred way to observe events. ■Note If you don’t quite follow how the event-observing code works, don’t worry; we’ll cover it in the next section. Event Handling in Prototype One key benefit Prototype offers developers is enhanced DOM event handling. Writing code to handle events across different browsers can be difficult, but with Prototype these issues can be avoided. One difficulty when not using Prototype is that event handlers can easily be overwritten. For example, if you have HTML code that includes and also loads an external JavaScript file containing window.onload = doSomethingElse, which function is called? Certainly not both of them! Prototype solves this problem by allowing us to add to existing event observers. This means that if you observe the same event on the same element twice, both event handlers will be triggered when the event occurs. Observing an Event To observe an event with Prototype, use the Event.observe() method. This method takes three arguments: • The element on which the event is being observed. • The event to observe; this is a string containing the event name. The event names are the same ones you might already be used to in JavaScript, except they don’t begin with on. For instance, to observe the onmouseover event, you would specify mouseover as the second argument. • The function to execute when the event is triggered. Going back to the “body onload” example, rather than using , you would use the following to correctly observe this event: Event.observe(window, 'load', something); This code would appear either in an external JavaScript file or within Next I can write the something() function, which is called when the image is clicked. I assume the first argument will be the event (which I like to simply call e). I can then pass e to Event.element() to return the image element. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS146 9063CH05CMP2 10/29/07 8:39 PM Page 146 Canceling an Event A common technique we will use in this book when writing Ajax code is to trigger an Ajax request when a form submit button is clicked. The problem with this is that the web browser will perform a normal postback when the button is clicked, meaning a new page will be loaded in the browser. To prevent this from occurring, the Event.stop() method must be called. This is a very useful method, since it is difficult to write code to achieve this across all browsers. As an example, let’s say I have the following form code:
Rather than submitting the form data back to the server, I want to run a function called handleFormSubmission() when the Submit Form button is clicked. First, I must observe the onsubmit event, and then call Event.stop() when handling the event: The best part about using code such as this is that it allows you to prevent normal postback when the user is running a browser capable of running JavaScript, yet it still submits the form as normal when a non-JavaScript browser is used. This helps you provide a rich user experience when the browser is capable of it, but it is also an accessible non-JavaScript solution. Creating JavaScript Classes in Prototype Yet another great thing about Prototype is its ability to easily create JavaScript classes. While this has always been possible with JavaScript, Prototype makes the process much simpler and helps you generate cleaner and more manageable code. Creating a Class The typical process for creating a class with Prototype is as follows: 1. Create the new class by calling Class.create(). Internally, this causes the class’s con- structor function to be automatically run when the class is instantiated. 2. Define the class’s prototype object (not to be confused with the name of the library you are using). This defines the properties and methods of the class. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 147 9063CH05CMP2 10/29/07 8:39 PM Page 147 3. When defining the class’s prototype object, implement the class constructor. The name of the constructor is initialize(), which can take any number of arguments (just as when writing any other JavaScript function). For example, to create a simple class called Book, which takes a title as its first argument, the following code could be used: Book = Class.create(); Book.prototype = { initialize : function(title) { this.title = title; } }; You can implement your own functions as required. For example, you could make a func- tion that returns the book title as follows: Book = Class.create(); Book.prototype = { initialize : function(title) { this.title = title; }, getTitle : function() { return this.title; } }; var book = new Book('Practical PHP Web 2.0 Applications'); alert(book.getTitle()); ■Tip Since each function is an element of the class’s prototype object, they must be separated by com- mas. Forgetting the comma is a very common cause of syntax errors when developing classes in JavaScript. Binding Function Calls to Objects A very important aspect of developing classes with Prototype is the use of the bind() and bindAsEventListener() functions. Please ensure you understand how these functions work, as they are used frequently in the JavaScript code in this book. These functions bind an object’s context to a class method so that when you call this in the method, it refers to the correct object. Because this is a difficult concept to grasp, I’ll use CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS148 9063CH05CMP2 10/29/07 8:39 PM Page 148 examples to explain it further. Once I have shown you how binding works, I’ll show you the difference between bind() and bindAsEventListener(), since there is only a subtle difference between the two. To demonstrate how binding works, I’ll create a class that observes the onclick event on an image. When the image is clicked, I will display an alert to the user notifying them that the image was clicked. First, I’ll create the class. The initialize method accepts the image element as its only argument, and then observes the onclick event. Also, I’ll define the notifyUser() method, which will be called by the event handler when the image is clicked. ImageHandler = Class.create(); ImageHandler.prototype = { initialize : function(img) { $(img).observe('click', handleClick); }, notifyUser : function() { alert('The image was clicked'); } }; So far so good. The image element is set as the first argument to the constructor, and the onclick event is observed on it. But wait, I haven’t implemented the handleClick() method, which is called by the event observer. I’ll add it to the class: ImageHandler = Class.create(); ImageHandler.prototype = { initialize : function(img) { $(img).observe('click', handleClick); }, notifyUser : function() { alert('The image was clicked'); }, handleClick : function(e) { this.notifyUser(); } }; CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 149 9063CH05CMP2 10/29/07 8:39 PM Page 149 The event handler function is now there. But will it be called when the image is clicked? No—the observer will call the global handleClick() function, not the handleClick() method inside the ImageHandler class. I need to add this in front of the handleClick() call: initialize : function(img) { $(img).observe('click', this.handleClick); }, There’s one small problem with this. The correct function will now be called when the image is clicked, but it will be called from the event-handling part of the system. In the handleClick() function, I refer to this.notifyUser(). Unfortunately, calling this here will not refer to the current instance of ImageHandler. This is where bind() comes in. I must bind the event-handler function to the current object. Rather than using this.handleClick as the event handler, I actually need to use this.handleClick.bind(this), as follows: ImageHandler = Class.create(); ImageHandler.prototype = { initialize : function(img) { $(img).observe('click', this.handleClick.bind(this)); }, notifyUser : function() { alert('The image was clicked'); }, handleClick : function(e) { this.notifyUser(); } }; By calling bind() on the function, I’m effectively saying, “when I refer to this in the ImageHandler’s handleClick() function, it should refer to the object I’m passing to bind(), which is an instance of ImageHandler.” The difference between bind() and bindAsEventListener() is that when you use bindAsEventListener() the event object will be passed in as the first argument to the bound function. Typically, you will always use bindAsEventListener() when observing events, not bind(). So, in actual fact, the preceding code to observe the image click needs to be as follows: $(img).observe('click', this.handleClick.bindAsEventListener(this)); When implementing callbacks for an Ajax response, you only use bind(), as the response isn’t triggered by an event. For example, the following code initiates an Ajax request when the object is initialized. The Ajax request will call handleSuccess() if the request is successfully CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS150 9063CH05CMP2 10/29/07 8:39 PM Page 150 performed. I will tell Prototype to bind the instance of AjaxBindExample to the handleSuccess() function: AjaxBindExample = Class.create(); AjaxBindExample.prototype = { initialize : function(img) { var options = { onSuccess : this.handleSuccess.bind(this) }; new Ajax.Request('/someUrl', options); }, handleSuccess : function(transport) { this.doSomething(); }, doSomething : function() { } }; From Prototype to Scriptaculous Prototype is a very useful JavaScript framework, and we just covered a large amount of the functionality it provides. We didn’t cover everything available in Prototype, however, as it is simply not all relevant to most of the code you will write in your Web 2.0 applications. We now move on to Scriptaculous, a JavaScript library used to add special effects to web sites. Scriptaculous is built upon Prototype, as it makes extensive use of nearly all classes pro- vided by Prototype—even ones we haven’t yet looked at, such as Position (used for element positioning and other issues related to the complex task of cross-browser layout). We will briefly cover exactly what Scriptaculous can do, then go over the installation of the library on your web pages, and finally look at an extensive example, which will make use of Scriptacu- lous effects and controls, Prototype classes, Ajax, and PHP. Before we go any further, though, let’s look at what Scriptaculous can do for us. We won’t go into all features in detail, but we will cover the more important ones, and anything else that will be required in this book. Prebuilt Controls Scriptaculous provides a number of prebuilt controls that can easily be included on your page. A control is a complex element for user interaction, typically used within or in place of forms. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 151 9063CH05CMP2 10/29/07 8:39 PM Page 151 The controls available in Scriptaculous are as follows: • Autocompleter: A text field that automatically provides suggestions based on user-input (somewhat similar to Google Suggest—http://www.google.com/webhp?complete=1&hl=en). ■Note In Chapter 12 we will implement a JavaScript class that behaves similarly to Google Suggest rather than using the one provided by Scriptaculous. This allows us to look at some of the nitty-gritty code involved in developing such a class. • InPlaceEditor: A class that allows a user to edit content on a web page directly. For example, if you had a list of files, you could use InPlaceEditor to allow users to rename a file by clicking on it. The filename would be replaced by a text input field, allowing the new filename to be entered inline. • Slider: A slider that a user can click and drag to change a value. Sliders are very cus- tomizable, including their styles, available values, and orientation (horizontal or vertical). Drag and Drop With Scriptaculous, it is easy to define draggable areas (using the Draggables class) and droppable areas (using the Droppables class) on your HTML pages. This allows you to achieve effects such as the following very easily: •Sort a list of items using the Sortables class, meaning that list items can be clicked on and dragged to their new location (and the new order can be saved in real time trans- parently using Ajax). •Drag an item from one list to another. For example, if you were managing product images for an online store, you might have a gallery of all the unused images. You could drag an image from this list onto a list of product images. Once again, you could save this state change transparently using Ajax. Visual Effects There are five core effects in Scriptaculous: • Effect.Opacity: Changes the opacity (transparency) of an element. This is done gradu- ally over a specified period of time. For instance, you could fade something from 100 percent opacity to 50 percent opacity over a period of 2 seconds. • Effect.Scale: Changes the size of an element to the specified dimensions. This allows you to easily grow or shrink an element. • Effect.MoveBy: Moves an element by a specified number of pixels (in both the X and Y directions). CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS152 9063CH05CMP2 10/29/07 8:39 PM Page 152 • Effect.Highlight: Highlights an element with a given color. Both a starting color and finishing color are specified, and the element changes color from the starting color to the finish color. This effect would typically be used to draw attention to a particular area of the page, such as to notify the user that an Ajax request has completed. • Effect.Parallel: Combines one or more effects into a single effect. In addition to these core effects, there are a large number of combination effects, built using the core effects. They include the following: • Effect.Appear: Makes a hidden element appear, going from complete transparency to 100 percent opacity. • Effect.Fade: Makes an element completely transparent (the opposite of Effect.Appear). At the completion of the effect, it will also hide the element from the document (that is, it will set the element’s CSS display property to none). • Effect.Grow:Grows an element from a size of 0x0 to its normal size. At the start of the effect, the element is shrunk to 0x0 and then grown gradually to normal size. Typically the element will be hidden prior to calling this effect. • Effect.Shrink: Scales an element gradually down to a size of 0x0 (the opposite of Effect.Grow). There are many more effects available, and you can write your own. The Scriptaculous web site (http://script.aculo.us) has more examples of the effects you can use. DOM Element Builder Scriptaculous provides a class called Builder, which is used to dynamically create new ele- ments in the DOM. It is effectively a replacement for the document.createElement() available in modern browsers. ■Tip The upcoming release of Prototype (version 1.6.0) will include a built-in DOM element builder. This means you can use Prototype to create new DOM elements rather than using Scriptaculous. Throughout this book, however, we will be using the Scriptaculous Builder class when we need to dynamically create new DOM elements. You can still create DOM elements using the browser’s built-in functions, but the solution provided by Scriptaculous is much cleaner and simpler. JavaScript Unit Testing The final class provided by Scriptaculous is called Test, which provides unit testing capabili- ties for JavaScript. The idea is to write a series of test cases alongside your code as you are developing it. This allows you to assert that your code still works correctly in the future even after making changes. It is useful for discovering bugs early on that you might not have discov- ered until later. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 153 9063CH05CMP2 10/29/07 8:39 PM Page 153 To use this class, you must manually include the unittest.js file in your HTML docu- ment. We will not be using this class in this book. Downloading and Installing Scriptaculous You can download Scriptaculous from http://script.aculo.us. The version used in this book is 1.7.1b3, and it requires Prototype 1.5.1.1 (typically when a new version of Prototype is released, a corresponding version of Scriptaculous is also released). After extracting the downloaded archive, all you need are the files in the src directory; I like to put these files in a directory called scriptaculous. Note that Prototype is also included in the archive (inside the lib directory), but you may already have the file installed. If not, this is the same file that you would download from http://www.prototypejs.org. Assuming you created the scriptaculous directory within a directory called /js (just as you did for Prototype), you would load Scriptaculous in your HTML pages using code similar to the following: Loading the Scriptaculous library As you can see, Prototype is loaded prior to Scriptaculous. If you do not do this, an excep- tion will be thrown by Scriptaculous. ■Tip If you do not need to use Scriptaculous on a particular page, you should avoid loading it to improve download speeds of the page and slightly reduce system overhead when loading the page. In addition to the main scriptaculous.js file, there are six JavaScript files that are automatically loaded. This totals seven HTTP requests and about 150KB just for Scriptaculous (the unit testing library, unittest.js, isn’t automati- cally loaded). In addition to this, Prototype is another 94KB. Combining Prototype, Scriptaculous, Ajax, and PHP in a Useful Example In order to demonstrate how to actually use Scriptaculous, we are going to write a script that utilizes it and makes use of the Prototype features we have covered so far in this chapter. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS154 9063CH05CMP2 10/29/07 8:39 PM Page 154 We will create a script that allows a user to sort a list of items using drag and drop. The script will do the following: 1. Once the user loads the page, use Ajax to fetch the list of items to be sorted and display them to the user 2. Allow the user to click and drag items to new locations to change the list order 3. Save the new order of the list after the user releases an item in a new location 4. Notify the user when the new order has been saved We will look at everything that is involved, including these functions: •Fetching the list of items using Ajax and using the DOM to create an unordered list (
    ) in which to display the items. •Making the list of items into a drag-and-drop list using the Scriptaculous Sortable class. •Styling the list of items in a manner that makes it easy for the user to drag items. •Handling Ajax events, including errors that may occur. •Using PHP in the background to save the list order. The list will be saved in a MySQL database. The code will be structured as follows: • index.php: A simple HTML page containing placeholders in which to show the sortable list and to show status messages. • styles.css: An external CSS file used to style the HTML page. • items.php: A PHP utility script used to manage the list of items, including connecting to the database, retrieving the list of items, and updating the order of the items. • processor.php: A PHP script to respond to the two different Ajax requests. • scripts.js: An external JavaScript file (in addition to Prototype and Scriptaculous) to handle the client-side application logic. This will be responsible for making the two Ajax requests required (fetching the list of items, and saving its new order). ■Note These files should be kept separate from the main web application we began in earlier chapters, since these files will not form part of the final application. This code will work just fine from a subdirectory. Figure 5-1 shows how the page will look once the example is complete. This is an action shot of the “Door” item being dragged to the bottom of the list. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 155 9063CH05CMP2 10/29/07 8:39 PM Page 155 Figure 5-1. Dragging an item in the list to a new location Creating the Main HTML Page: index.php First we need to create the main index HTML page, as shown in Listing 5-8. This is the page users will load in the browser. Listing 5-8. The HTML Code Used to Display the List to the User (index.php) Manage items order
    Status: (nothing to report)
    CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS156 9063CH05CMP2 10/29/07 8:39 PM Page 156

    Manage items order

    (items not yet loaded)
    In this code we first load the Prototype and Scriptaculous libraries, followed by our own JavaScript file (scripts.js). The files must be loaded in this order, as Scriptaculous relies on Prototype, and our script relies on both. Then the external CSS file is loaded. Next, we include a container called #status-container to show a status message. When a new status message is set, it will be displayed inside of #status. The text inside of #status is the default text, meaning that after a new status message is shown, #status will revert back to this text. We then define a div called #content. This is only used because of how we will style #status-container. Inside of this is a div called #container—this is where the sortable list will appear. Note that we could define the
      tag here, and then add elements to it later, but instead of doing that I’ve included a message saying the items aren’t yet loaded. This message will be replaced by the list after it is loaded. That is all that is required in this file. If you’re wondering how the script is initiated, we will actually define the onload event inside of scripts.js; after everything is loaded, the list will be fetched using Ajax. ■Note This file could just as easily be called index.html as it doesn’t contain any PHP code; however, I like to keep all files consistently named, rather than have a mix of .php and .html files. Styling the Application: styles.css Now let’s look at the CSS file for our application, styles.css. Listing 5-9 shows the code for this file. It should be stored in the same directory as the index.php file. Listing 5-9. The CSS Code Used to Style the Example Application (styles.css) body { margin : 0; font-family : sans-serif; font-size : 12px; } ul.sortable { list-style-type : none; width : 300px; margin : 0; padding : 0; } CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 157 9063CH05CMP2 10/29/07 8:39 PM Page 157 ul.sortable li { margin : 2px; padding : 5px; background : #eee; cursor : move; } #status-container { color : #333; background : #f7f7f7; font-weight : bold; font-size : 11px; border-bottom : 1px solid #666; padding : 3px; } #status { font-weight : normal; } #content { margin : 10px; } The main things to be aware of in this file are the ul.sortable and ul.sortable li selec- tors. These give the list items the look and feel of items that can be moved. We also change the mouse pointer to move to indicate that the elements can be dragged when the cursor is above them. Creating and Populating the Database: schema.sql As mentioned previously, we will be using a MySQL database in this example to store the list items. The database is called ch05_example. Assuming you already have permissions set up correctly in your MySQL server, use the following query to create your database: mysql> create database ch05_example; You may need to grant the correct permissions so that the database can be accessed. To use the same username and password as we used in Chapter 2, you can use the following command: mysql> grant all on ch05_example.* to phpweb20@localhost identified by 'myPassword'; You can then populate this database using the SQL queries inside schema.sql, as shown in Listing 5-10. Listing 5-10. The SQL Queries Used to Populate the Database (schema.sql) create table items ( item_id serial not null, title varchar(255) not null, ranking int, primary key (item_id) ); insert into items (title) values ('Bicycle'); CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS158 9063CH05CMP2 10/29/07 8:39 PM Page 158 insert into items (title) values ('Car'); insert into items (title) values ('Chair'); insert into items (title) values ('Door'); insert into items (title) values ('House'); insert into items (title) values ('Table'); insert into items (title) values ('Window'); ■Note The SQL code in schema.sql will also work just fine in PostgreSQL (although the commands to create the database and user will be different). You can either paste these commands directly into the MySQL console, or you could run the following command (from the Linux or Windows command prompt): $ mysql -u phpweb20 -p ch05_example < schema.sql In the preceding table schema, the ranking column is used to store the order of the list items. This is the value that is manipulated by clicking and dragging items using the Scriptac- ulous Sortable class. ■Note At this stage we aren’t storing any value for the ranking column. This will only be saved when the list order is updated. In the PHP code, you will see that if two or more rows have the same ranking value, they will then be sorted alphabetically. Managing the List Items on the Server Side: items.php We must now write the server-side code required to manage the list items. Essentially, we need a function to load the list of items, and another to save the order of the list. (We will look at how these functions are utilized shortly.) In addition to these two functions, we also need to include a basic wrapper function to connect to the database. In larger applications you would typically use some kind of database abstraction (such as the Zend_Db class we integrated in Chapter 2). All of the code in this section belongs in the items.php file. Connecting to the Database Listing 5-11 shows the code used to connect to the MySQL database. Listing 5-11. The dbConnect() Function,Which Connects to a MySQL Database Called ch05_example (items.php) item_id] = $row->title; } return $items; } In this function, we sort the list by each item’s ranking value. This is the value that is updated when the list order is changed. Initially, there is no ranking value for items, so we use the title column as the secondary ordering field. Processing and Saving the List Order Finally, we must save the new list order to the database after a user drags a list item to a new location. In the processItemsOrder() function, we retrieve the new order from the post data (using PHP’s $_POST superglobal), and then update the database. If this action fails, false is returned; this will occur if the new ordering data isn’t found in $_POST. If the new list order is saved, true is returned. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS160 9063CH05CMP2 10/29/07 8:39 PM Page 160 Listing 5-13 shows the processItemsOrder() function. Listing 5-13. The processItemsOrder() Function,Which Takes the New List Order from the Post Data and Saves It to the Database (items.php) function processItemsOrder($key) { if (!isset($_POST[$key]) || !is_array($_POST[$key])) return false; $items = getItems(); $ranking = 1; foreach ($_POST[$key] as $id) { if (!array_key_exists($id, $items)) continue; $query = sprintf('update items set ranking = %d where item_id = %d', $ranking, $id); mysql_query($query); $ranking++; } return true; } ?> Processing Ajax Requests on the Server Side: processor.php In the previous section, we covered the code used to manage the list of items. We will now look at processor.php, the script responsible for handling Ajax requests and interfacing with the functions in items.php. As mentioned earlier, there are two different Ajax requests to handle. The first is the load action, which returns the list of items as XML. This action is handled by calling the getItems() function, and then looping over the returned items and generating XML based on the data. The second action is save, which is triggered after the user changes the order of the sortable list. This action results in a call to the processItemsOrder() function we just looked at. Listing 5-14 shows the contents of the processor.php file. Listing 5-14. Loading and Saving Ajax Requests (processor.php) $title) $xmlItems[] = sprintf('', $id, htmlSpecialChars($title)); $xml = sprintf('%s', join("\n", $xmlItems)); header('Content-type: text/xml'); echo $xml; exit; case 'save': echo (int) processItemsOrder('items'); exit; } ?> The first thing we do in this code is include the items.php file and call dbConnect(). If this function call fails, there’s no way the Ajax requests can succeed, so we exit right away. The JavaScript code we will look at in the next section will handle this situation. We then use a switch statement to determine which action to perform, based on the value of the action element in the $_POST array. This allows for easy expansion if another Ajax request type needs to be added. If the action isn’t recognized in the switch, nothing happens and the script execution simply ends. Handling the Load Action To handle the load action, we first retrieve the array of items. We then loop over them and generate XML for the list. We use htmlSpecialChars() to escape the data so that valid XML is produced. Technically speaking, this wouldn’t be sufficient in all cases, but for this example it will suffice. The resulting XML will look like the following: CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS162 9063CH05CMP2 10/29/07 8:39 PM Page 162 Finally, we send this XML data. To tell the requester what kind of data is being returned, the content-type header is sent with text/xml as its value. Handling the Save Action All processing for the save action is taken care of by the processItemsOrder() function, so it is relatively simple to handle this request. The items value is passed as the first argument, as this corresponds to the value in the post data holding the item order. The processItemsOrder() function returns true if the list order was successfully updated. To indicate this to the JavaScript, we return 1 for success. Any other value will be treated as failure. As such, we can simply cast the return value of processItemsOrder() using (int) to return a 1 on success. Creating the Client-Side Application Logic: scripts.js We will now look at the JavaScript code used to make and handle all Ajax requests, including loading the items list initially, making it sortable with Scriptaculous, and handling any changes in the order of the list. All the code listed in this section is from the scripts.js file in this chap- ter’s source code. Application Settings We first define a few settings that are used in multiple areas. Using a hash to store options at the start of the script makes altering code behavior very simple. Listing 5-15 shows the hash used to store settings. Listing 5-15. The JavaScript Hash That Stores Application Settings (scripts.js) var settings = { containerId : 'container', statusId : 'status', processUrl : 'processor.php', statusSuccessColor : '#99ff99', statusErrorColor : '#ff9999' }; The containerId value specifies the ID of the element that holds the list items (that is, where the
        of list items will go). The statusId value specifies the element where status messages will appear. The value for processUrl is the URL where Ajax requests are sent. statusSuccessColor is the color used to highlight the status box when an Ajax request is successful, while statusErrorColor is used when an Ajax request fails. Initializing the Application with init() To begin this simple Ajax application, we call the init() function. Listing 5-16 shows the code for init(). CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 163 9063CH05CMP2 10/29/07 8:39 PM Page 163 Listing 5-16. The init() Function,Which Begins this Example Ajax Application (scripts.js) function init() { $(settings.statusId).defaultContent = $(settings.statusId).innerHTML; loadItems(); } You might find the first line of this function to be slightly confusing. Essentially, what it does is save the initial content from the status container in a new property called defaultContent (remember that in index.php we had the string (nothing to report) in the status container). This allows us to change the contents of the status container back to this value after showing a new status message. Next, we call the loadItems() function, which fetches the list of items from the server and displays them to the user. We will look at this function shortly. In order to call this function, we use the onload event. Using Prototype’s Event.observe() method, we set the init() function to run once the page has finished loading. This is shown in Listing 5-17. Listing 5-17. Setting init() to Run once the Page Finishes Loading—Triggered by the window.onload Event (scripts.js) Event.observe(window, 'load', init); ■Note As we saw earlier in this chapter, using Event.observe() to handle the page onload event is preferred over using . Updating the Status Container with setStatus() Before we go over the main function calls in this example, we will look at the setStatus() util- ity function. This function is used to update the status message, and it uses Scriptaculous to highlight the status box (with green for success, or red for error). Listing 5-18 shows the code for setStatus(). The first argument to this function specifies the text to appear in the status box. Note that there is also an optional second argument that indicates whether or not an error occurred. If setStatus() is called with this second argument (with a value of true), the message is treated as though it occurred as a result of an error. Essentially, this means the status box will be highlighted with red. Listing 5-18. The setStatus() Function,Which Displays a Status Message to the User (scripts.js) function setStatus(msg) { var isError = typeof arguments[1] == 'boolean' && arguments[1]; var status = $(settings.statusId); var options = { CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS164 9063CH05CMP2 10/29/07 8:39 PM Page 164 startcolor : isError ? settings.statusErrorColor : settings.statusSuccessColor, afterFinish : function() { this.update(this.defaultContent); }.bind(status) }; status.update(msg); new Effect.Highlight(status, options); } The options hash holds the options for the Scriptaculous effect we will be using (Effect.Highlight). First, we specify the starting color based on whether or not an error occurred, and then we specify code to run after the effect has completed. In the init() function, we stored the initial content of the status container in the defaultContent property. Here we change the status content back to this value after the effect completes. Notice that we are making use of bind(), which was explained earlier in this chapter. Even though we haven’t created this code in a class, we can bind a function to an arbitrary element, allowing us to use this within that function to refer to that element. Next, we call the Prototype update() method to set the status message. We then create a new instance of the Effect.Highlight class to begin the highlight effect on the status box. Once again, because this is a class, it must be instantiated using the new keyword. Loading the List of Items with loadItems() The loadItems() function initiates the load Ajax request. This function is somewhat straight- forward—it is the onSuccess callback loadItemsSuccess that is more complicated. Listing 5-19 shows the code for loadItems(), including a call to the setStatus() function we just covered. Listing 5-19. The loadItems() Function,Which Initiates the Load Ajax Request (scripts.js) function loadItems() { var options = { method : 'post', parameters : 'action=load', onSuccess : loadItemsSuccess, onFailure : loadItemsFailure }; setStatus('Loading items'); new Ajax.Request(settings.processUrl, options); } In this code, we specify the action=load string as the parameters value. This action value is used in processor.php to determine which Ajax request to handle. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 165 9063CH05CMP2 10/29/07 8:39 PM Page 165 Handling the Response from the Ajax Request in loadItems() We will now look at the onSuccess and onFailure callbacks for the Ajax request in the previous section. The onFailure callback is handled by the loadItemsFailure() function shown in List- ing 5-20, while the onSuccess callback is handled by the loadItemsSuccess() function shown in Listing 5-21. Listing 5-20. The onFailure Callback Handler (scripts.js) function loadItemsFailure(transport) { setStatus('Error loading items', true); } In this function, we simply set an error status message by passing true as the second parameter to setStatus(). Listing 5-21. The onSuccess Callback Handler (scripts.js) function loadItemsSuccess(transport) { // Find all tags in the return XML, then cast it into // a Prototype Array var xml = transport.responseXML; var items = $A(xml.documentElement.getElementsByTagName('item')); // If no items were found there's nothing to do if (items.size() == 0) { setStatus('No items found', true); return; } // Create an array to hold items in. These will become the
      • tags. // By storing them in an array, we can pass this array to Builder when // creating the surrounding
          . This will automatically take care // of adding the items to the list var listItems = $A(); // Use Builder to create an
        • element for each item in the list, then // add it to the listItems array items.each(function(s) { var elt = Builder.node('li', { id : 'item_' + s.getAttribute('id') }, s.getAttribute('title')); listItems.push(elt); }); // Finally, create the surrounding
            element, giving it the className CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS166 9063CH05CMP2 10/29/07 8:39 PM Page 166 // property (for styling purposes), and the 'items' values as an Id (for // form processing - Scriptaculous uses this as the form item name). // The final parameter is the
          • element we just created var list = Builder.node('ul', { className : 'sortable', id : 'items' }, listItems); // Get the item container and clear its content var container = $(settings.containerId); container.update(); // Add the
              to the empty container container.appendChild(list); // Finally, make the list into a Sortable list. All we need to pass here // is the callback function to use after an item has been dropped in a // new position. Sortable.create(list, { onUpdate : saveItemOrder.bind(list) }); } The preceding code has been documented inline to show you how it works. The only new things in this code we haven’t yet covered are the calls to the Scriptaculous functions Builder.node() and Sortable.create(). The following code shows the HTML equivalent of the elements created using the Builder.node() function:
              • Bicycle
              • Car
              • Chair
              • Door
              • House
              • Table
              • Window
              This list is then made into a sortable list by passing it as the first parameter to Sortable.create(). Additionally, the saveItemOrder() function is specified as the function to be called after the user moves a list item to a new location. Once again, we use bind(), allow- ing us to use this inside of saveItemOrder() to refer to the #items list. Handling a Change to the List Order with saveItemOrder() A call to the saveItemOrder() function will initiate the second Ajax request, save. This function shouldn’t be called directly, but only as the callback function on the sortable list, to be trig- gered after the list order is changed. Listing 5-22 shows the code for saveItemOrder(). CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 167 9063CH05CMP2 10/29/07 8:39 PM Page 167 Listing 5-22. The saveItemOrder Callback, Triggered After the Sortable List Order is Changed (scripts.js) function saveItemOrder() { var options = { method : 'post', parameters : 'action=save&' + Sortable.serialize(this), onSuccess : saveItemOrderSuccess, onFailure : saveItemOrderFailure }; new Ajax.Request(settings.processUrl, options); } In this code, we once again create an options hash to pass to Ajax.Request(). This time, we set the action value inside of parameters to save. Additionally, we use Sortable.serialize() to create appropriate form data for the order of the list. This is the data that is processed in the PHP function processItemsOrder() from items.php. The value of parameters will look something like the following: action=save&items[]=1&items[]=2&items[]=3&items[]=4&items[]=5&items[]=6&items[]=7 Each value for items[] corresponds to a value in the items database table (with the item_ part automatically removed). Handling the Response from the Ajax Request in saveItemOrder() Finally, we must handle the onSuccess and onFailure events for the save Ajax request. Listing 5-23 shows the code for the onFailure callback saveItemOrderFailure(), while Listing 5-24 shows the code for the onSuccess callback saveItemOrderSuccess(). Listing 5-23. The saveItemOrderFailure() Callback, Used for the onFailure Event (scripts.js) function saveItemOrderFailure(transport) { setStatus('Error saving order', true); } If saving the order of the list fails, we simply call setStatus() to indicate this, marking the status message as an error by passing true as the second parameter. Handling the onSuccess event is also fairly straightforward. To determine whether the request was successful, we simply check to see if the response contains 1. If so, the request was successful. Once again we call setStatus() to notify the user. If the request wasn’t successful, we call saveItemOrderFailure() to handle the error. Listing 5-24. The saveItemOrderSuccess() Callback, Used for the onSuccess Event (scripts.js) function saveItemOrderSuccess(transport) { CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS168 9063CH05CMP2 10/29/07 8:39 PM Page 168 if (transport.responseText != '1') return saveItemOrderFailure(transport); setStatus('Order saved'); } If you now load the index.php file created in Listing 5-8 in your web browser you will be shown a list of items that you can now drag and drop. When you drop an item to a new loca- tion an Ajax request will be performed, updating the order saved in the database. Summary As you have seen in this chapter, the Prototype JavaScript library is a very powerful library that provides a lot of useful functionality, as well as making cross-browser scripting simpler. We also looked at the Scriptaculous library and created a simple Ajax application that made use of its highlight effect and sortable control. In the next chapter, we will build on the HTML code we created in Chapter 2 by using some powerful CSS techniques to style our web application. Once we have the HTML and CSS in place, we can add new functionality that makes use of the JavaScript techniques we have learned in this chapter. CHAPTER 5 ■ INTRODUCTION TO PROTOTYPE AND SCRIPTACULOUS 169 9063CH05CMP2 10/29/07 8:39 PM Page 169 9063CH05CMP2 10/29/07 8:39 PM Page 170 Styling the Web Application At this stage in the development of our Web 2.0 application, we have created some basic templates and a few different forms (for user registration and login), but we haven’t applied any customized styling to these forms. In this chapter we are going to start sprucing up our site. In addition to making the forms we have already created look much better, we are also going to put styles and layout in place to help with development in following chapters. We will be covering a number of topics in this chapter, including the following: •Adding navigation and search engine optimization elements, such as the document title, page headings, and breadcrumb trails •Creating a set of generic global styles that can easily be applied throughout all tem- plates (such as forms and headings) using Cascading Style Sheets (CSS) • Allowing for viewing on devices other than a desktop computer (such as creating a print-only style sheet for “printer-friendly” pages) •Integrating the HTML and CSS into the existing Smarty templates, and using Smarty templates to easily generate maintainable HTML •Creating an Ajax-based form validator for the user registration form created in Chapter 4 Adding Page Titles and Breadcrumbs Visually indicating to users where they are in the structure of a web site is very important for the site’s usability, and many web sites overlook this. A user should easily be able to identify where they are and how they got there without having to retrace their steps. To do this, we must assign a title to every page in our application. Once we have the titles, we can set up a breadcrumb system. A breadcrumb trail is a navigational tool that shows users the hierarchy of pages from the home page to where they currently are. Note that this differs from how the web browser’s history works—the breadcrumb system essentially shows all of the parent sections the current page is in, not the trail of specific pages the user visited to get to the current page. A breadcrumb system might look like this: Home > Products > XYZ Widget In this example, the current page would be XYZ Widget, while Home would be hyperlinked to the web site’s home page, and Products would link to the appropriate page. 171 CHAPTER 6 9063Ch06CMP2 11/13/07 7:56 PM Page 171 To name the pages, we need to define a title in each action handler of each controller (for example, to add a title to the account login page we will add it to the loginAction() method of the AccountController PHP class). Some titles will be dynamically generated based on the pur- pose of the action (such as using the headline of a news article as the page title when displaying that article), while others will be static. You could argue about whether the title of a page should be determined by the application logic (that is, in the controller file) or by the display logic (determined by the template). In some special cases titles will need to be determined in the template, but it is important to always define a page title in the controller actions to build up a correct breadcrumb trail. If the page titles were defined within templates, it would be very diffi- cult to construct the breadcrumb trail. ■Note In larger web applications, where the target audience includes people not only from your country but also other countries, you need to consider internationalization and localization (also known as i18n and L10n, with the numbers indicating the number of letters between the starting and finishing letters). Interna- tionalization and localization take into account a number of international differences, including languages and formatting of numbers, currencies, and dates. In the case of page titles, you would fetch the appropriate page title for the given language based on the user’s settings, rather than hard-coding the title in the PHP code. The Zend_Translate component of the Zend Framework can help with implementation of i18n and L10n. To implement the title and breadcrumb system, we need to make two changes to the way we create application controllers: 1. We must implement the Breadcrumbs class, which is used to hold each of the bread- crumb steps. The Breadcrumbs object will be assigned to the template, so we can easily output the trail in the header.tpl file. 2. We must build a trail in each controller action with the steps that lead up to the action. The steps (and number of steps) will be different for each action, depending on its spe- cific purpose. The Breadcrumbs Class This is a class that simply holds an array of the steps leading up to the current page. Each element of the array has a title and a link associated with it. Listing 6-1 shows the code for Breadcrumbs, which we will store in Breadcrumbs.php in the /var/www/phpweb20/include directory. Listing 6-1. Tracking the Trail to the Current Page with the Breadcrumbs Class (Breadcrumbs.php) _trail[] = array('title' => $title, 'link' => $link); } public function getTrail() { return $this->_trail; } public function getTitle() { if (count($this->_trail) == 0) return null; return $this->_trail[count($this->_trail) - 1]['title']; } } ?> This class is very short and straightforward, consisting of just three methods: one to add a step to the breadcrumbs trail (addStep()), one to retrieve the trail (getTrail()), and one to determine the page title using the final step of the trail (getTitle()). To use Breadcrumbs, we instantiate it in the init() method of the CustomControllerAction class. This makes it available to all classes that extend from this class. Additionally, we will add a link to the web site home page by calling addStep('Home', '/') after we instantiate Breadcrumbs. ■Note This object is freshly created for every action that is dispatched. This means that even if you forward from one action to another in the same request, the breadcrumbs trail is recreated (since the controller object is reinstantiated). Next, we need to add the postDispatch() function to CustomControllerAction. This func- tion will be executed once a controller action has completed. We will use this function to assign the breadcrumbs trail and the page title to the template, since postDispatch() is called prior to the automatic view renderer displaying the template. Listing 6-2 shows the updated version of CustomControllerAction.php, which now instan- tiates Breadcrumbs and assigns it to the template. Listing 6-2. Instantiating and Assigning the Breadcrumbs Class (CustomControllerAction.php) db = Zend_Registry::get('db'); $this->breadcrumbs = new Breadcrumbs(); $this->breadcrumbs->addStep('Home', '/'); } // ... other code public function postDispatch() { $this->view->breadcrumbs = $this->breadcrumbs; $this->view->title = $this->breadcrumbs->getTitle(); } } ?> ■Note When we add the title of the current page to the trail, we don’t need to add its URL, since the user is already on this page and doesn’t need to navigate to it. Generating URLs Before we go any further, we need to consider how to generate URLs for each step we add to the breadcrumbs. For example, if we wanted to link to the account login page, the URL would be /account/login. In this instance, the controller name is account and the action name is login. The simplest solution is to hard-code this URL both in the PHP code (when creating the breadcrumbs) and in the template (when creating hyperlinks). However, hard-coding URLs doesn’t give you any flexibility to change the format of the URL. For example, if you decide to move your web application to a subdirectory of your server instead of the root directory, all of your hard-coded URLs would be incorrect. ■Tip If you did decide to use a subdirectory, you would call $controller->setBaseUrl('/path/to/base') in the index.php bootstrap file. This could then be retrieved by calling $request->getBaseUrl() when inside a controller action, as you will see shortly. CHAPTER 6 ■ STYLING THE WEB APPLICATION174 9063Ch06CMP2 11/13/07 7:56 PM Page 174 Generating URLs in Controller Actions We now need to write a function that generates a URL based on the controller and action names passed to it. To help us with URL generation, we will use the Url helper that comes with Zend_Controller. The only thing to be aware of is that this helper will not prefix the generated URL with a slash, or even with the base URL (as mentioned in the preceding tip). Because of this, we must make a slight modification by extending this helper—we will create a new func- tion called getUrl(). Listing 6-3 shows the getUrl() function we will add to CustomControllerAction.php. This code uses the Url helper to generate the URL, and then prepends the base URL and a slash at the start. The other change made in this file modifies the home link that is generated so it calls the new getUrl() function, rather than hard-coding the slash. Listing 6-3. Creating a Function to Generate Application URLs (CustomControllerAction.php) breadcrumbs->addStep('Home', $this->getUrl(null, 'index')); } public function getUrl($action = null, $controller = null) { $url = rtrim($this->getRequest()->getBaseUrl(), '/') . '/'; $url .= $this->_helper->url->simple($action, $controller); return $url; } // ... other code } ?> ■Note The call to rtrim() is included because the base URL may end with a slash, in which case the URL would have // at the end. Now within each controller action we can call $this->getUrl() directly. For example, if we wanted to generate the URL for the login page, we would call $this->getUrl('login', 'account'). CHAPTER 6 ■ STYLING THE WEB APPLICATION 175 9063Ch06CMP2 11/13/07 7:56 PM Page 175 ■Note This code uses the simple() method on the Url helper, which is used to generate a URL from an action and a controller. In later chapters we will define custom routes, which means the format of URLs is more complex. This helper also provides a method called url(), which is used to generate URLs based on the defined routes. Generating URLs in Smarty Templates Before we go any further, we must also cater for URL generation within our templates. To achieve this, we will implement a Smarty plug-in called geturl. Doing so will allow us to generate URLs by using {geturl} in templates. For instance, we could generate a URL for the login page like this: {geturl action='login' controller='account'} Additionally, we will allow the user to omit the controller argument, meaning that the current controller would be used. ■Tip The preceding code is an example of a Smarty function call. The three main types of plug-ins are functions, modifiers, and blocks. Modifiers are functions that are applied to strings that are being output (making a string uppercase with {$myString|upper}, for example) while blocks are used to define output that wraps whatever is between the opening and closing tags (such as {rounded_box} Inner content. {/rounded_box}). In the case of geturl, we will use a Smarty function in order to perform a specific oper- ation based on the provided arguments; that function isn’t being applied to an existing string, so it is not a modifier. A Smarty plug-in is created by defining a PHP function called smarty_type_name(), where type is either function, modifier, or block. In our case, since the plug-in is called geturl, the function is called smarty_function_geturl(). ■Tip There are other plug-in types available, such as output filters (which modify template output after it has been generated), compiler functions (which change the behavior of the template compiler), pre and post filters (which modify template source prior to or immediately after compilation), and resources (which load templates from a source other than the defined template directory). These could be the subject of their own book, so I can’t cover them all here, but this section will at least give you a good idea of how to implement your own function plug-ins. All plug-ins should be stored in one of the registered Smarty plug-in directories. Smarty comes with its own set of plug-ins, and in Chapter 2 we created our own directory in which to store custom plug-ins (./include/Templater/plugins). The filename of plug-ins follows the CHAPTER 6 ■ STYLING THE WEB APPLICATION176 9063Ch06CMP2 11/13/07 7:56 PM Page 176 format type.name.php, so in our case the file is named function.geturl.php. Smarty will automatically load the plug-in as soon as we try to access it in a template. The code for the geturl plug-in is shown in Listing 6-4. It should be written to ./include/Templater/plugins/function.geturl.php. Listing 6-4. The Smarty geturl Plug-In That Uses the Zend_Controller URL Helper (function.geturl.php) getRequest(); $url = rtrim($request->getBaseUrl(), '/') . '/'; $url .= $helper->simple($action, $controller); return $url; } ?> All function plug-ins in Smarty retrieve an array of parameters as the first argument and the Smarty object as the second argument. The array of parameters is generated using the arguments specified when calling the function. In other words, calling the geturl function using {geturl action='login' controller='account'} will result in the $params array being the same as if you used the following PHP code: 'login', 'controller' => 'account' ); ?> The function must do its own initialization and checking of the specified parameters. This is why the code in Listing 6-4 checks for the existence of the action and controller parame- ters in the first two lines of the function. Next the Url helper and the current request are retrieved using the provided functions. You will notice that the code we use to generate the actual URL is almost identical to that in the CustomControllerAction class. Finally, the URL is returned to the template, meaning it is output directly. This allows us to use it inside forms and hyperlinks (such as
              ). CHAPTER 6 ■ STYLING THE WEB APPLICATION 177 9063Ch06CMP2 11/13/07 7:56 PM Page 177 ■Tip The function in Listing 6-4 returns the generated URL so it is output directly to the template. You may prefer to write it to a variable in your template so you can reuse the URL as required. The convention for this in Smarty is to pass an argument called assign, whose value is then used as the variable name. For instance, you could call the function using {geturl action='login' controller='account' assign='myUrl'}. By including $smarty->assign($params['assign'], $url) in the plug-in instead of returning the value, you can then access $myUrl from within your template. Typically you would check for the existence of assign and output the value normally if it is not specified. Now, if you need to link to another controller action within a template, you should be using the {geturl} plug-in. This may be a normal hyperlink, or it may be a form action. ■Note At this point I make the assumption that existing templates have been updated to use the {geturl} plug-in. Try updating the existing templates for registration, login, and updating details (located in the ./templates/account directory) that we created in Chapter 4 so the forms and any other links in the page use {geturl}.Alternatively, the downloadable source code for this and remaining chapters will use {geturl} wherever it should. Setting the Title and Trail for Each Controller Action We now have the ability to set the page title and breadcrumb trail for all pages in our web application, so we must update the AccountController class we created in Chapter 3 to use these features. First, we want all action handlers in this controller to have a base breadcrumb trail of “Home: Account”, with additional steps depending on the action. To add the “Account” bread- crumb step automatically, we will define the init() method in this class, which calls the Breadcrumbs::addStep() method. We must also call parent::init(), because the init() method in CustomControllerAction sets up other important data. In fact, this parent method instantiates Breadcrumbs, so it must be called before adding the breadcrumbs step. By automatically adding the “Account” step for all actions in this controller, we are effec- tively naming the index action for this controller Account. This means that in the indexAction() function we don’t need to set a title, as Breadcrumbs::getTitle() will work this out for us auto- matically. Listing 6-5 shows the changes we must make to the AccountController class to set up the trail for the register and registercomplete actions. No change is required for the index action. Note that we also set the base URL for the controller in the init() method and change the redirect URL upon successful registration. Listing 6-5. Defining the Page Titles and Trails for the Index and Registration Actions (AccountController.php) breadcrumbs->addStep('Account', $this->getUrl(null, 'account')); } public function indexAction() { // nothing to do here, index.tpl will be displayed } public function registerAction() { $request = $this->getRequest(); $fp = new FormProcessor_UserRegistration($this->db); if ($request->isPost()) { if ($fp->process($request)) { $session = new Zend_Session_Namespace('registration'); $session->user_id = $fp->user->getId(); $this->_redirect($this->getUrl('registercomplete')); } } $this->breadcrumbs->addStep('Create an Account'); $this->view->fp = $fp; } public function registercompleteAction() { // ... other code here $this->breadcrumbs->addStep('Create an Account', $this->getUrl('register')); $this->breadcrumbs->addStep('Account Created'); $this->view->user = $user; } // ... other code here } ?> CHAPTER 6 ■ STYLING THE WEB APPLICATION 179 9063Ch06CMP2 11/13/07 7:56 PM Page 179 ■Note You can try adding titles to each of the other actions in this controller (although the logout action will not require it), or you can simply download the source for this chapter, which will be fully updated to use the breadcrumbs system. Because we define the title of the section in the controller’s init() method, we typically don’t need to define a title in indexAction(), since the title added in init() will be adequate. Next, we specify the title as “Create an Account” in the registerAction() function. This string is added to the trail as well as being assigned to the template as $title (this is done in CustomControllerAction’s postDispatch() method, as we saw in Listing 6-2). Creating a Smarty Plug-In to Output Breadcrumbs The breadcrumb trail has been assigned to templates as is, meaning that we can call the getTrail() method to return an array of all of the trail steps. The problem with this is that it clutters the template, especially when you consider some of the options that can be used. Instead, we will create another Smarty plug-in: a function called breadcrumbs. With this function, we will be able to output the trail based on a number of different options. This func- tion is reusable, and you’ll be able to use it for other sites you create with Smarty. This should always be a goal when developing code such as this. Listing 6-6 shows the contents of function.breadcrumbs.php, which is stored in the ./include/Templater/plugins directory. This code basically loops over each step in the bread- crumb trail and generates a hyperlink and a displayable title. Since it is optional for steps to have a link, a title is only generated if no link is included. The same class and file naming con- ventions apply as in the geturl plug-in discussed previously (in the “Generating URLs in Smarty Templates” section), and as before it is best to initialize all parameters at the beginning of the function. Listing 6-6. A Custom Smarty Plug-In Used to Output the Breadcrumb Trail (function.breadcrumbs.php) array(), 'separator' => ' > ', 'truncate' => 40); // initialize the parameters foreach ($defaultParams as $k => $v) { if (!isset($params[$k])) $params[$k] = $v; } // load the truncate modifier if ($params['truncate'] > 0) CHAPTER 6 ■ STYLING THE WEB APPLICATION180 9063Ch06CMP2 11/13/07 7:56 PM Page 180 require_once $smarty->_get_plugin_filepath('modifier', 'truncate'); $links = array(); $numSteps = count($params['trail']); for ($i = 0; $i < $numSteps; $i++) { $step = $params['trail'][$i]; // truncate the title if required if ($params['truncate'] > 0) $step['title'] = smarty_modifier_truncate($step['title'], $params['truncate']); // build the link if it's set and isn't the last step if (strlen($step['link']) > 0 && $i < $numSteps - 1) { $links[] = sprintf('%s', htmlSpecialChars($step['link']), htmlSpecialChars($step['title']), htmlSpecialChars($step['title'])); } else { // either the link isn't set, or it's the last step $links[] = htmlSpecialChars($step['title']); } } // join the links using the specified separator return join($params['separator'], $links); } ?> After the array of links has been built in this function, we create a single string to be returned by joining on the separator option. The default value for the separator is >, which we preescape. It is preescaped because some characters you might prefer to use aren’t typable, so you can specify the preescaped version when calling the plug-in. An example of this is the » symbol, which we can use by calling {breadcrumbs separator=' » '}. When we generate the displayable title for each link, we make use of the Smarty truncate modifier. This allows us to restrict the total length of each breadcrumb link by specifying the maximum number of characters in a given string. If the string is longer than that number, it is chopped off at the end of the previous word and “...” is appended. For instance, if you were to truncate “The Quick Brown Fox Jumped over the Lazy Dog” to 13 characters, it would become “The Quick...”. This is an improvement over the PHP substr() function, since substr() will simply perform a hard break in the middle of a word (so the example string would become “The Quick Bro”). CHAPTER 6 ■ STYLING THE WEB APPLICATION 181 9063Ch06CMP2 11/13/07 7:56 PM Page 181 ■Tip In a Smarty template, you would use {$string|truncate}, but we can use the truncate modifier directly in our PHP code by first loading the modifier (using $smarty->_get_plugin_filepath() to retrieve the full path of the plug-in and then passing the plug-in type and name as the arguments) and then calling smarty_modifier_truncate() on the string. The final thing to note in this function is that the URLs and titles are escaped as required when adding elements to the $links array. This ensures that valid HTML is generated and also prevents cross-site scripting (XSS) and cross-site request forgery (CSRF). This is explained in more detail in Chapter 7. Displaying the Page Title The final step is to display the title and breadcrumbs in the site templates, and to update the links to use the geturl plug-in. Listing 6-7 shows the changes to be made to header.tpl, where we now display the page title within the tag as well as within an <h1> tag. Additionally, we use the new {breadcrumbs} plug-in to easily output the breadcrumb trail. Listing 6-7. Outputting the Title and Breadcrumbs in the Header Template (header.tpl) <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <title>{$title|escape}
              Home {if $authenticated} | Your Account | Update Your Details | Logout {else} | Register | Login {/if}
              {breadcrumbs trail=$breadcrumbs->getTrail()} CHAPTER 6 ■ STYLING THE WEB APPLICATION182 9063Ch06CMP2 11/13/07 7:56 PM Page 182 {if $authenticated}
              Logged in as {$identity->first_name|escape} {$identity->last_name|escape} (logout)
              {/if}

              {$title|escape}

              Figure 6-1 shows the page, now that it includes the page title and breadcrumbs. Figure 6-1. The Account Created page, showing the page title as well as the full trail of how the page was reached Integrating the Design into the Application We are now at the stage where we can create the application layout by using a more formal design in the header and footer templates and styling it using Cascading Style Sheets (CSS). In this section, we will first determine which elements we want to include on pages, and then create a static HTML file (allowing us to see a single complete page), which we will break up into various parts that can be integrated into the site templates. CHAPTER 6 ■ STYLING THE WEB APPLICATION 183 9063Ch06CMP2 11/13/07 7:56 PM Page 183 Creating the Static HTML Figure 6-2 shows the design we will use for the web application (including CSS, which we will integrate in the next section), as viewed in Firefox. The layout developed in this chapter has been tested with Firefox 2, Internet Explorer 6 and 7, and Safari. ■Note It is worth mentioning here that this book is devoted to the development side of web applications, not the design side. As such, the look and feel we use for the web application will be straightforward in com- parison to what a professional web designer would come up with. Hopefully, though, the techniques here can help you in marking up a professional design into HTML and CSS. Figure 6-2. The web page design we will use for the web application: a cross-browser, fluid, table-free layout The key elements of this layout include: • Three columns with a fluid middle column and fixed-size left and right columns •No tables to set the columns •A header area (for a logo), which can also be expanded to include other elements (such as advertising) •A tabbed navigation system that allows users to see which section of the site they are in •A breadcrumb trail and page title CHAPTER 6 ■ STYLING THE WEB APPLICATION184 9063Ch06CMP2 11/13/07 7:56 PM Page 184 It is actually somewhat difficult to get a multiple-column layout with a single fluid central column without using tables. This cross-browser solution is adapted from Matthew Levine’s Holy Grail technique from “A List Apart” (http://www.alistapart.com/articles/holygrail). The following HTML code shows the basic structure of how our main site template will be structured. We will integrate this into our templates shortly.
              As you can see in this HTML code, the center column (#content-container) appears before the other columns. This helps with search engine optimization, as the core page con- tent is earlier in the file, and is therefore treated as being of greater priority in the document. ■Note Placing the center column first is also an accessibility feature, since users who rely on screen read- ers will reach the relevant content sooner. The preceding code simply demonstrates at the most basic level how the elements of the page piece together. Let’s now take a look at the full markup before we integrate it into the templates. Listing 6-8 shows the HTML code that we will be splitting up for use in the tem- plates. We must also include calls to the Smarty plug-ins we created in order to generate links and for displaying breadcrumbs. For now though, we just include placeholders for these, which we will replace with Smarty code in Listing 6-9. CHAPTER 6 ■ STYLING THE WEB APPLICATION 185 9063Ch06CMP2 11/13/07 7:56 PM Page 185 ■Note If you’re anything like me—a programmer rather than a designer—it can be useful to see a site design statically before it is integrated into the application. Typically when I build a new web site or web application, I work from either prebuilt HTML templates such as this or from a Photoshop design which I then convert into static HTML with corresponding CSS. Listing 6-8. The Complete HTML Code Used in Figure 6-2 (listing-6.8.html) Sample HTML Layout

              Sample HTML Layout

              Center column

              CHAPTER 6 ■ STYLING THE WEB APPLICATION186 9063Ch06CMP2 11/13/07 7:56 PM Page 186
              Left column box 1
              Left column box 2
              Right column box
              In this code, we first create the #header block, which is left empty. We will display the logo in this block by using a CSS background image. Of course, you could choose to include the logo here using an tag—I have left it blank here because we will be using this block to include a “print-only” logo (which we will cover in the “Creating a Print-Only Style Sheet” section in this chapter). Next, we use an unordered list (
                ) to display the web site navigation. You could argue that this list is in fact in order, so the
                  tag may be used instead. In any case, the correct semantics involve using an HTML list. ■Tip Using an unordered (or ordered) list lends itself to scalability very well. For example, if you were using JavaScript and CSS to build a drop-down navigation system (one that expands the navigation on mouseover), using nested
                    tags would work perfectly. Additionally, if the user’s web browser doesn’t render a JavaScript menu solution, they could easily navigate the site because the links would be structured for them. After defining the main content area, we populate the left and right columns. The content that appears in these columns will be split up into separate boxes, so we give the divs within these columns a class of .box to easily define that structure. We will define this style shortly in the style sheet. Let’s now take a look at how this markup is rendered in Firefox with no styles defined. Figure 6-3 demonstrates how everything gets rendered from top to bottom exactly as it is defined in the HTML. Additionally, you can see how the navigation is displayed horizontally, which we will also fix in the CSS. CHAPTER 6 ■ STYLING THE WEB APPLICATION 187 9063Ch06CMP2 11/13/07 7:56 PM Page 187 Figure 6-3. The web page design we will use for the web application before it has had styles applied to it Moving the HTML Markup into Smarty Templates The next step in styling our web application is to integrate the HTML from Listing 6-8 into our existing templates. This primarily involves modifying the header.tpl and footer.tpl files, but there are also some minor changes that need to be made to other templates. In this section, we will go over all of the changes required to integrate this design. The steps are as follows: •Copy the top half of the HTML file into header.tpl. •Copy the bottom half of the HTML file into footer.tpl. •Keep the dynamic variables in place in the header (namely the browser title, page title, and breadcrumbs). •Highlight the active section in the navigation based on a variable passed in from the action templates, and modify the action templates to tell header.tpl which section to highlight in the navigation. ■Note The “top half” of the design referred to in the preceding list is all markup prior to the content for the body of each controller action, while the “bottom half” is all markup after the end of the controller action content. In Listing 6-8, the top half is all code from the start of the file until the breadcrumbs (including the breadcrumbs). Everything else inside the #content element will be defined in each action’s template. CHAPTER 6 ■ STYLING THE WEB APPLICATION188 9063Ch06CMP2 11/13/07 7:56 PM Page 188 One other thing to note is that we don’t yet have content to place in either of the side columns, so we will use the right column to display the details of the currently logged-in user, and we will simply leave a place marker in the left column until these columns are populated. If you haven’t done so already, copy the logo.gif and logo-print.gif files into the images directory from the book’s source code. We will create the styles.css file that is loaded in the header later in this chapter. Modifying header.tpl To make the necessary changes to header.tpl, we can just copy some of the HTML in Listing 6-8 into this file—from the beginning of the listing down to where the page heading is dis- played. We then include the calls to {breadcrumbs} and {geturl} where appropriate. Listing 6-9 shows the new version of header.tpl (in the ./templates directory). This ver- sion loads the external style sheet and uses variables for the breadcrumbs and title unlike the static values in Listing 6-8. This code should replace the code previously in the header.tpl file. Listing 6-9. Integrating the Design into the Header Template (header.tpl) {$title|escape}

                    {$title|escape}

                    There are a few things to notice in this template: • The site navigation has been modified so the geturl plug-in is used to generate the links, while the “Update Details” link has been removed (we will include this in the right column in footer.tpl). • The value of the $section variable is checked to determine which navigation item should be highlighted. To highlight the item, the CSS class .active is applied. We must define the $section variable when we load the header.tpl template. • The breadcrumbs separator is specified as » (which has the entity name ») for a slightly fancier look. Spaces must also be included on either side of this character. • The “Logged in as…” information is removed. This will also move to the right column (in footer.tpl). •We no longer bother to check the $section variable for the logout link because after log- ging out a user is directed right back to the login page. Modifying footer.tpl In order to finish integrating this template, we must add the corresponding section of markup from Listing 6-8 to the site footer. Listing 6-10 shows the code that will replace the code in the footer.tpl file (in the ./templates directory). Note that this code includes details about the currently logged-in user in a box in the right column, and it includes a link to “Update details”. Listing 6-10. Integrating the Design into the Site Footer (footer.tpl)
                    Left column placeholder
                    CHAPTER 6 ■ STYLING THE WEB APPLICATION190 9063Ch06CMP2 11/13/07 7:56 PM Page 190
                    {if $authenticated} Logged in as {$identity->first_name|escape} {$identity->last_name|escape} (logout). Update details. {else} You are not logged in. Log in or register now. {/if}
                    ■Tip If you haven’t yet tried, you should be able to validate the generated markup with no warnings or errors using the W3C validator at http://validator.w3.org. In fact, you could have done so prior to this chapter, as we are developing standards-compliant code. It is important when developing your CSS and templates to check the validity of both your HTML/XHTML and your CSS (using http://jigsaw.w3.org/css-validator), as it is easy to accidentally put something in your code that breaks the validation. Chris Pederick’s Web Devel- oper toolbar for Firefox (http://chrispederick.com/work/web-developer) has quick-access links to validate HTML and CSS code. Highlighting the Active Navigation Section The new header.tpl in Listing 6-9 includes code to check the value of the $section variable to determine which section to highlight. We must now update each of the controller action tem- plates so each one defines the $section variable. This is done when including the header template. For example, to highlight the “Home” link, the following code would be used to include header.tpl: {include file='header.tpl' section='home'} Note that we don’t use $ in front of section when using a variable name as the attribute value in Smarty, but we do use it when referring to the variable in header.tpl. Listing 6-11 shows the updated version of index.tpl, which now highlights the correspond- ing entry in the main navigation. Note that there may be situations where no item is selected. CHAPTER 6 ■ STYLING THE WEB APPLICATION 191 9063Ch06CMP2 11/13/07 7:56 PM Page 191 Listing 6-11. Highlighting the “Home” Link in the Header Template (index.tpl) {include file='header.tpl' section='home'} Web site home {include file='footer.tpl'} ■Note Try updating each of the other controller action templates so the correct section is highlighted. You can check what the value needs to be by checking the header.tpl file. Specifically, you will need to update each of the files in the ./templates/account directory to use {include file='header.tpl' section='account'} rather than {include file='header.tpl'}. This is fairly simple to test, since you only need to visit each page and check that the navigation is highlighted properly. Alternatively, you can download the source code for this chapter. Constructing the CSS Now that we’ve integrated the HTML markup into our Smarty templates, we can incorporate the CSS so the page displays nicely as the three-column layout we discussed. All styles will be stored in a file called styles.css, which will reside in the css directory of the web site (/var/www/phpweb20/htdocs/css). ■Note There’s no particular reason for choosing this directory, other than that it keeps the files organized. You may find that an internal section of your web site may require its own CSS file—for example, it might require a large number of custom styles that you don’t want to include in the main site’s CSS file (why slow down the loading of the home page with extra styles that aren’t required?). Creating a separate directory for your CSS files will help you keep the files organized, just as you might organize images. Specifying Media Types and Loading the CSS File Later in this chapter we will look at creating a print-only style sheet, so we must keep in mind that we need to provide styles for different media types. There are two different ways of telling the browser which media type is being used: the @media rule and the media attribute (used when loading the CSS file with a tag). For our application, we will use the @media CSS rule, but we will look at them both here first. ■Note I’m not necessarily advocating using @media over loading a separate style sheet with ; however, using @media is my personal preference in most cases, since it means fewer files are loaded when a user visits the site, reducing page-load time and server overhead. CHAPTER 6 ■ STYLING THE WEB APPLICATION192 9063Ch06CMP2 11/13/07 7:56 PM Page 192 To load separate style sheets for the screen and for printing, you could use the following HTML code: Alternatively, if you wanted to use the @media rule, you could load a single style sheet and separate the media types within that file. First, you would load the file specifying media="all" so this style sheet would be used regardless of what type of device is viewing the page: Next, you would use the @media rule to separate the media types. Within styles.css, you would use the following: .some-css-item { color : #000; } @media screen { .some-css-item { color : #f00; } } @media print { .some-css-item { color : #00f; } } In this example, the global styles for .some-css-item would use the color black, while red (#f00) would be used for screen, and blue (#00f) would be used when printing. Other media types you might use include aural (for screen-reading software) and hand- held (for handheld devices, such as a phone with a small screen and limited capabilities). ■Tip According to the Apple Developer Connection web site at http://developer.apple.com/iphone /designingcontent.html, you can specify a style sheet specifically for the Apple iPhone by using the only keyword in combination with the screen media type. Other devices will ignore the only keyword and therefore not use the style sheet. For example, to load the iphone.css file only for people viewing on an iPhone you can use . Creating the Application CSS The next step is to create the first CSS code in our web application. In this section I will briefly describe the custom CSS that is used. The Holy Grail technique mentioned earlier is explained by Matthew Levine at http://www.alistapart.com/articles/holygrail. The entire CSS file is listed at the end of this section so you can see how it all fits together. Creating the Three-Column Layout Since the Holy Grail article describes how the fluid three-column layout works, I will not describe those techniques here. The important thing to note is that we are setting both of the CHAPTER 6 ■ STYLING THE WEB APPLICATION 193 9063Ch06CMP2 11/13/07 7:56 PM Page 193 side columns to be 300 pixels wide. If you want to use a different size, you will need to modify the values in the code accordingly. body { margin : 0; padding : 0 300px; min-width : 600px; } #header, #footer, #nav { margin : 0 -300px 0 -300px; } .column { float : left; position : relative; } #content-container { width : 100%; padding : 0; } #left-container { width : 300px; margin-left : -100%; right : 300px; } #right-container { width : 300px; margin-right : -300px; } #footer { clear : both; } * html #left-container { left : 300px; } If you were to view the HTML code from Listing 6-8 using only the preceding CSS, the display in Firefox would be similar to the screen in Figure 6-4. The bottom half of this figure shows the Firebug console as it integrates into Firefox. ■Tip Firebug is arguably the most powerful web development plug-in available for Firefox. While the Web Developer toolbar has been around for longer and is also very useful, the CSS and DOM inspection capabili- ties, as well as the ability to debug subrequests made with XMLHttpRequest, make it a must-have plug-in. You can download Firebug from http://www.getfirebug.com. Figure 6-4. Using Firebug to see the layout properties of the three-column layout CHAPTER 6 ■ STYLING THE WEB APPLICATION194 9063Ch06CMP2 11/13/07 7:56 PM Page 194 CHAPTER 6 ■ STYLING THE WEB APPLICATION 195 Styling the Page Header In Figure 6-2, there was a logo displayed in a header block that hasn’t appeared in subsequent figures. To include this logo, we must set the background to use an image in the CSS. This allows us to include other code in #header as we need. For instance, when we implement printer-friendly styles later in this chapter, we will include a printer-friendly logo in this area, since CSS backgrounds typically aren’t included when people print web pages. Here is the code used to style the #header div: #header { background : url(../images/logo.gif) no-repeat 5px center #f22; height : 45px; border-bottom : 1px solid #922; } We first set the background properties. The path used in url() is relative to the CSS file, not to the HTML document that loads the CSS file. By using no-repeat, we tell the browser to include the background image only once. The image is also positioned 5 pixels from the left of the div and centered vertically. Finally, the background color is set to a shade of red (to match the background color of logo.gif). Next, we set the height of the div to 45 pixels, which is slightly taller than the image. Since #header is empty, we must give it a height so the browser will make it big enough for the back- ground image to appear. Finally, we add a dark red border to the bottom of the div. We will also be using this color when we join the navigation to the header. Styling the Tabbed Navigation Bar The navigation bar consists of horizontal tabs created as an unordered list. In order to make the unordered list horizontal, we set the display property of list items (
                  • ) to be inline. Addi- tionally, we need to consider browser defaults for unordered lists: Internet Explorer uses a margin, and Firefox uses padding on the left of each element. We remove this by setting both the padding and margin to 0. Additionally, each list item will display a bullet point, which we can remove by using list-style : none. ■Tip A useful way to deal with browsers that have different default styles is to use a “reset” style sheet. This is an extra style sheet that you load in your pages to give all elements the same style across all browsers (where relevant). The Yahoo Developer Network provides a reset style sheet that you can use (http://developer.yahoo.com/yui/reset), although Eric Meyer has developed his own, which he based on Yahoo’s. You can find his latest reset style sheet at http://meyerweb.com/eric/thoughts/ 2007/05/01/reset-reloaded, or his original article at http://meyerweb.com/eric/thoughts/2007/ 04/12/reset-styles. One thing to be aware of is that using an extra style sheet may result in extra page- load time. You may prefer to just include your own reset styles as you need them to keep your CSS file smaller. 9063Ch06CMP2 11/13/07 7:56 PM Page 195 The following code styles the navigation bar. This code defines not only the layout of the navigation (making the list horizontal), but also the style of links in the navigation. The .active class highlights the navigation item that represents the section of the user’s current page. We use this style when we check for the $section variable in header.tpl. #nav { margin-top : -1px; margin-bottom : 20px; font-size : 0.9em; text-transform : uppercase; } #nav ul { margin : 0; padding : 4px 0; text-align : center; } #nav li { list-style : none; padding : 0; margin : 0; display : inline; } #nav a { background : #922; color : #aaa; text-decoration : none; padding : 4px 8px; text-align : center; border : 1px solid #922; border-top : none; margin : 0 3px; } #nav a:hover { color : #fff; text-decoration : underline; } #nav li.active a { color : #fff; background : #f22; font-weight : bold; } CHAPTER 6 ■ STYLING THE WEB APPLICATION196 9063Ch06CMP2 11/13/07 7:56 PM Page 196 Setting the Global Styles In addition to setting styles for specific containers or areas on a page, we must also define a set of global styles. They are called global styles because each selector applies to every occurrence in a page of its respective element(s). The following code sets the heading style, the text font and size, and the style for links. Take the img style as an example. Every time an image is used in the page, it will have no bor- der—even if it is hyperlinked. Each global style can be overridden on a case-by-case basis. body { color : #333; background : #fafafa; font-family : Verdana, Arial, Helvetica, sans-serif; font-size : 0.75em; } h1 { font-size : 1.7em; margin-top : 0; } h2 { font-size : 1.5em; } h3 { font-size : 1.3em; } h4 { font-size : 1.1em; } h5 { font-size : 1.0em; } h1, h2, h3, h4, h5 { font-family : Georgia, serif; color : #f22; } img { border : 0; } form { margin : 0; } a { color : #f22; background : none; text-decoration : underline; } a:hover { color : #fff; background : #f22; text-decoration : none; } In this code, we set the base font size to 0.75em. While the specific value used here isn’t important, the fact that we use ems is. A single unit of em (1 em) is the width of the “m” character in the current font family and size. In other words, you could interpret a font-size directive inside the body as saying “set the font size to 75 percent of the browser’s default size.” Using ems allows the browser to scale fonts as required (most noticeably when a user selects “increase font size” or “decrease font size” in their browser). ■Note We could also use ems instead of pixels for other measurements in the style sheet, such as for the column widths or border sizes. However, I have chosen not to in order to have more precise control over the on-screen layout. Styling the Page Content The remaining page areas to be styled are the content areas of the three columns. This includes creating the .box class, since all side-column content will appear inside various divs using this style. CHAPTER 6 ■ STYLING THE WEB APPLICATION 197 9063Ch06CMP2 11/13/07 7:56 PM Page 197 The following styles format the various content areas of the page, including the page footer and the breadcrumb trail: #content-container { background : #fff; } #content { border : 1px solid #eee; padding : 10px; line-height : 1.8em; } #breadcrumbs { font-size : 0.8em; color : #ccc; } #breadcrumbs a { color : #aaa; } #breadcrumbs a:hover { background : #aaa; color : #fff; } #left-container .box, #right-container .box { margin : 0 10px 10px 10px; padding : 10px; border : 1px solid #eee; background : #fff; font-size : 0.9em; line-height : 1.6em; } #footer { color : #999; font-size : 0.8em; padding : 10px; text-align : center; } This concludes the selectors for setting up global styles and styling the screen media type according to the design in Figure 6-2. We will add further elements as we require them throughout the book (including later in this chapter for styling forms), but the base styles defined here will suffice in most situations. Creating a Print-Only Style Sheet Many web sites offer a “print this page” link on their pages. Traditionally, this will link to another page on the site that repeats the content while stripping out all of the elements that have no relevance when printed (such as site navigation or a search form). By using print-only style sheets, we can mimic this behavior without the need for a secondary page of the same content. All we need to do is define styles for the print media type, as we saw earlier. CHAPTER 6 ■ STYLING THE WEB APPLICATION198 9063Ch06CMP2 11/13/07 7:56 PM Page 198 Before we do this, we should at least compare the two methods: using a secondary page as opposed to using a print style sheet. The advantages of using a print style sheet are as follows: • The user doesn’t need to navigate to another page in order to print content. •You, as the developer, don’t need to code in extra functionality to serve a stripped-down page (you will have to create a style sheet for this page anyway). • The server does not have to serve an extra page, reducing server load and bandwidth use. •Your web site statistics will be more accurate (although this isn’t much of a problem, since you could always filter these extra entries out). On the other hand, the advantages of using a secondary print page instead of a print style sheets are as follows: •It will make more sense to users, as they will be able to see that the content is indeed stripped down. •Users are more used to this method. •Users might want to print the page exactly as it appears on screen, but a print style sheet won’t allow them to do this (unless they use an advanced tool, which will allow them to block certain style sheets). •Users probably won’t rely on there being a print style sheet, because most developers don’t provide one. Note that the advantages of using secondary print pages stem from the fact that people are more used to using them. Ultimately, you must decide how you want to do this; since this is a book on Web 2.0 development, we will follow the CSS standard and implement code as it was intended. After all, adhering to standards was one of the aspects of Web 2.0 I defined in Chapter 1. Modifying the Screen Style Sheet There are essentially two key things we want to do in creating a print style sheet for our web application. The first is to hide elements that don’t need to be printed, which in this case means the navigation and left and right columns. The second is to add a header that will be printed on all pages. Typically, web browsers will strip out background colors and images when printing pages (users can generally change this setting, but most won’t). To deal with this, we will place a printer-friendly image in our HTML. This forces the browser to print the logo; however, we must then alter the screen style sheet so this image isn’t normally displayed on the screen. Listing 6-12 shows how we can add the printer-friendly logo to the header.tpl template. Listing 6-12. Including a Printer-Friendly Logo in the Header Template (header.tpl) We then just need to add a rule to the screen media-type section of the style sheet that hides this logo when the user views the page in their browser. We also need to add rules to the print media-type section of the style sheet to hide the elements that we don’t want to print (the side columns and the navigation). The following code shows how this is achieved (ignoring the remainder of the style sheet for now). @media screen { #header img { display : none; } } @media print { #nav, #left-container, #right-container { display : none; } } Figure 6-5 shows how the page will look if you use the print preview tool in Firefox, com- pared to how the page normally looks in the browser. As an exercise, you may want to add extra styles to the print style sheet so the printable page has a nicer layout. Figure 6-5. Comparing the screen and print styles of the same page CHAPTER 6 ■ STYLING THE WEB APPLICATION200 9063Ch06CMP2 11/13/07 7:56 PM Page 200 As a final note on this topic, you can easily add sections you want to include when print- ing, yet don’t want to include when viewed on screen. This is just the opposite of how we hid the navigation and side columns; simply include them in the HTML markup, and then hide them in the screen section of the style sheet. This is effectively the same thing we did with the print-only logo. The Full Application Style Sheet Now that we have looked at all of the sections that make up the style sheet (including global styles, screen-only styles, and print-only styles), we can see how it all pieces together. Listing 6-13 shows the full CSS file with all the styles we have looked at in this chapter. This code should be written to the styles.css file in ./htdocs/css. Listing 6-13. The CSS Used to Implement the Three-Column Layout (styles.css) @media screen { /** * Global elements */ body { color : #333; background : #fafafa; font-family : Verdana, Arial, Helvetica, sans-serif; font-size : 0.75em; } h1 { font-size : 1.7em; margin-top : 0; } h2 { font-size : 1.5em; } h3 { font-size : 1.3em; } h4 { font-size : 1.1em; } h5 { font-size : 1.0em; } h1, h2, h3, h4, h5 { font-family : Georgia, serif; color : #f22; } img { border : 0; } form { margin : 0; } a { color : #f22; background : none; text-decoration : underline; } a:hover { color : #fff; background : #f22; text-decoration : none; } /** * Setup the 3 column layout */ CHAPTER 6 ■ STYLING THE WEB APPLICATION 201 9063Ch06CMP2 11/13/07 7:56 PM Page 201 body { margin : 0; padding : 0 300px; min-width : 300px; } #header, #footer, #nav { margin : 0 -300px 0 -300px; } .column { float : left; position : relative; } #content-container { width : 100%; padding : 0; } #left-container { width : 300px; margin-left : -100%; right : 300px; } #right-container { width : 300px; margin-right : -300px; } #footer { clear : both; } * html #left-container { left : 300px; } /** * Style the main page areas */ #header { background : url(../images/logo.gif) no-repeat 5px center #f22; height : 45px; border-bottom : 1px solid #922; } #header img { display : none; } #content-container { background : #fff; } #content { border : 1px solid #eee; padding : 10px; line-height : 1.8em; } #breadcrumbs { font-size : 0.8em; color : #ccc; margin-bottom : 10px; } #breadcrumbs a { color : #aaa; } #breadcrumbs a:hover { background : #aaa; color : #fff; } #left-container .box, #right-container .box { margin : 0 10px 10px 10px; padding : 10px; border : 1px solid #eee; background : #fff; CHAPTER 6 ■ STYLING THE WEB APPLICATION202 9063Ch06CMP2 11/13/07 7:56 PM Page 202 font-size : 0.9em; line-height : 1.6em; } #footer { color : #999; font-size : 0.8em; padding : 10px; text-align : center; } /** * Tabbed navigation */ #nav { margin-top : -1px; margin-bottom : 20px; font-size : 0.9em; text-transform : uppercase; } #nav ul { margin : 0; padding : 4px 0; text-align : center; } #nav li { list-style : none; padding : 0; margin : 0; display : inline; } #nav a { background : #922; color : #aaa; text-decoration : none; padding : 4px 8px; text-align : center; border : 1px solid #922; border-top : none; margin : 0 3px; } CHAPTER 6 ■ STYLING THE WEB APPLICATION 203 9063Ch06CMP2 11/13/07 7:56 PM Page 203 #nav a:hover { color : #fff; text-decoration : underline; } #nav li.active a { color : #fff; background : #f22; font-weight : bold; } } @media print { /** * Elements to hide */ #nav, #left-container, #right-container { display : none; } } Styling the Application Web Forms In Chapter 4 we created three forms for the user system: a registration form, a login form, and a fetch-password form. Since forms play such an important part in interactive web sites, we must make our forms easy for users to understand and use. Let’s look at how to style these forms. Each form should meet the following requirements: •Elements must be clearly labeled. •Errors that occur should be highlighted. •A submit button must be included. In Chapter 4 we used a Smarty template called error.tpl to output errors. This template outputs a div regardless of whether an error has occurred, since this allows us to use it as a placeholder for JavaScript-generated errors. As such, we must hide this div if no error has occurred. First, we style the .error div. This div will have a red background with white text so it stands out. Additionally, we will add a rule so that if the error div occurs inside the .row class (the container we use to hold each form element), we will shrink the font slightly. div.error { background : #a00; padding : 5px; margin : 5px 0; color : #fff; } CHAPTER 6 ■ STYLING THE WEB APPLICATION204 9063Ch06CMP2 11/13/07 7:56 PM Page 204 form .row div.error { font-size : 0.8em; line-height : 1em; } Next, we will style the .row class, which holds each element. We will add a margin to the top and bottom of each .row, and then float the label left (allowing us to set its display type to block instead of the default of inline) and give it a width of 150px. If you set the width when its display type is inline, this will be ignored. form .row { margin : 10px 0; clear : both; } form .row label { width : 150px; float : left; display : block; font-weight : bold; } Next, we set the default widths of text inputs, using the following CSS: form .row input[type=text] { width : 230px; } form .row input[type=password] { width : 230px; } Be aware that Internet Explorer 6 does not understand CSS selectors based on element attribute values (although Internet Explorer 7 does). An alternative would be to simply use .row input, but this would affect check boxes and radio buttons (and any other type of ). The other alternative is to explicitly set a class name on the input, and then style that class accordingly. Finally, we will set the CAPTCHA image to align with the other input elements by setting its left margin, and then we’ll create a simple style to hold submit buttons. form .captcha { margin-left : 150px; } form .submit { padding : 5px; margin-top : 10px; background : #eee; } Listing 6-14 shows how this new CSS code fits into the styles.css file. I have omitted the parts not relevant to display forms. Listing 6-14. The Application Style Sheet Including Styling of Forms and Errors (styles.css) @media screen { /* ... other code */ /** * Forms */ CHAPTER 6 ■ STYLING THE WEB APPLICATION 205 9063Ch06CMP2 11/13/07 7:56 PM Page 205 div.error { background : #a00; padding : 5px; margin : 5px 0; color : #fff; } form .row div.error { font-size : 0.8em; line-height : 1em; } form .row { margin : 10px 0; clear : both; } form .row label { width : 150px; float : left; display : block; font-weight : bold; } form .row input[type=text] { width : 230px; } form .row input[type=password] { width : 230px; } form .captcha { margin-left : 150px; } form .submit { padding : 5px; margin-top : 10px; background : #eee; } /* ... other code */ } Note that these are all somewhat generic styles, and while they will work fine for most situations, they may not suit every type of form you create—you may need to create new form styles in some situations. However, these styles do work well for the registration form, the login form, and the fetch-password form, as you can see in Figure 6-6. CHAPTER 6 ■ STYLING THE WEB APPLICATION206 9063Ch06CMP2 11/13/07 7:56 PM Page 206 Figure 6-6. The registration form, now styled and showing errors usefully Loading Prototype and Scriptaculous In Chapter 5 we took a look at the Prototype and Scriptaculous JavaScript libraries, which we will make heavy use of in later chapters. Since the examples used in that chapter were inde- pendent of the application we are developing, we did not actually load these libraries for our application. We will now update the header.tpl template to automatically load these libraries in the section of the template. For more discussion on loading each of these libraries, refer to Chapter 5. Listing 6-15 shows the lines we will add to header.tpl to load Prototype and Scriptaculous. The lines not listed here that we added earlier in this chapter remain the same. Listing 6-15. Loading Prototype and Scriptaculous Automatically (header.tpl) {$title|escape} CHAPTER 6 ■ STYLING THE WEB APPLICATION 207 9063Ch06CMP2 11/13/07 7:56 PM Page 207 Implementing Client-Side Form Validation Now that we have looked at how Prototype and Scriptaculous work and have added styles to the site, we can revisit the user registration form. In this section, we will add client-side form validation to the user registration form using JavaScript and Ajax. Adding client-side valida- tion improves usability since the user will receive feedback about any invalid form values more quickly. Specifically, we will check that each of the form fields contain valid values when the user clicks the submit button to register. If everything appears correct, we will allow the form to be sent to the server. Note that we will still have our server-side validation in place (as imple- mented in Chapter 4), so even if the user doesn’t have JavaScript enabled, they cannot circumvent any of the data checks. Rather than duplicating the server-side validation we already have in place, we will make some small changes to the existing code so it can be used for Ajax validation in addition to the normal registration. The changes we will implement include the following: •Modifying the FormProcessor_UserRegistration class so we have the option of validat- ing form data without actually creating the user if no errors occur • Changing the way the registerAction() method of AccountController works so that if the action is requested via Ajax, a JSON response is sent containing any errors that occurred •Creating a JavaScript class to trigger the form validation, as well as submitting the form once all values have been verified In actual fact, the form validation we are implementing here still uses the server in that it submits the data to the server for validation. We could add simple validation (such as checking for empty fields) without communicating with the server, but more complicated checks such as determining whether or not a username is already in use require server interaction. Although the client-side validation still uses the server for validation, it is quicker than doing a normal post-back since the page doesn’t need to be reloaded. ■Note In this particular example, all validation is done using the FormProcessor_UserRegistration class. The client-side code we will implement is really just a proxy to this class. This means we can easily expand the form-processing capabilities in the future by modifying FormProcessor_UserRegistration— the JavaScript we develop in this section will scale automatically. CHAPTER 6 ■ STYLING THE WEB APPLICATION208 9063Ch06CMP2 11/13/07 7:56 PM Page 208 Adding JSON Support to CustomControllerAction In Chapter 5 we briefly looked at JSON (JavaScript Object Notation), which can be used to easily send data between client and server in Ajax requests. Implementing this form validator gives us our first chance of using JSON in this application. In order to return JSON data from controller actions, we will add a new method to the CustomControllerAction class. Since we need to send a certain content type HTTP header for JSON data, it is much simpler to add this method once rather than sending the header manu- ally each time we need to send JSON data. Listing 6-16 shows the sendJson() method we will add to the CustomControllerAction.php file in ./include. Listing 6-16. A Utility Method to Send JSON Data from Controller Actions (CustomControllerAction.php) _helper->viewRenderer->setNoRender(); $this->getResponse()->setHeader('content-type', 'application/json'); echo Zend_Json::encode($data); } } ?> The first thing that we do here is disable autorendering of the view, since we’re not outputting with a template. For more discussion of how the automatic view rendering in Zend_Controller works, refer to Chapter 2. Next, we must send the appropriate content-type header. By default, PHP will send a con- tent type of text/html, which will work in this case, but it is not technically correct. According to RFC 4627 (which can be found at http://www.ietf.org/rfc/rfc4627.txt), the official MIME type for JSON data is application/json. Finally, we can call Zend_Json::encode() to encode the $data array. Modifying the Form Processor The next step in implementing client-side form validation is to add an extra option to the FormProcessor_UserRegistration class so form data can be checked without actually creating a new user account. We do this so the JavaScript code can determine whether the form data is correct before submitting the actual form. To achieve this, we will add a new method to this class called validateOnly(). If this method is called with an argument value of true, the form will be processed, but even if there are no errors, the new user database row will not be created. CHAPTER 6 ■ STYLING THE WEB APPLICATION 209 9063Ch06CMP2 11/13/07 7:56 PM Page 209 Listing 6-17 shows the changes we need to make to the UserRegistration.php file in the ./include/FormProcessor directory. Listing 6-17. Adding the Ability to Only Validate the Registration Form (UserRegistration.php) _validateOnly = (bool) $flag; } public function process(Zend_Controller_Request_Abstract $request) { // ... other code // if no errors have occurred, save the user if (!$this->_validateOnly && !$this->hasError()) { $this->user->save(); unset($session->phrase); } // return true if no errors have occurred return !$this->hasError(); } } ?> Modifying the Registration Controller Action In order to make use of the validation-only mode of the form processor, as well as to return a JSON response of any errors, we must now make some changes to the registerAction() method of the AccountController class. If the request was submitted using Ajax, we want the method just to validate the form and return any errors by calling the sendJson() method we just created. Conversely, if the request wasn’t submitted using Ajax, we want this method to behave as normal—that is, to process the user registration and then redirect the confirmation page once complete. CHAPTER 6 ■ STYLING THE WEB APPLICATION210 9063Ch06CMP2 11/13/07 7:56 PM Page 210 Detecting Ajax Requests Using Zend_Controller we can easily determine whether a request came from an Ajax subre- quest by calling the isXmlHttpRequest() method on the request object that is available inside controller actions. Internally, this method looks for the presence of the X-Requested-With HTTP header. If the value of this header is XMLHttpRequest, this method returns true. This header is not automatically set when using XMLHttpRequest to initiate HTTP subrequests, but Prototype will set this header automatically. This means the Prototype Ajax.Request class is compatible with the isXmlHttpRequest() method from the Zend_ Controller_Request_Http class. Returning Form Errors Using JSON Now that you know how to detect Ajax requests, we can make the necessary changes to the registerAction() method in the AccountController class. If the request was initiated using XMLHttpRequest, we will call the validateOnly() method we just implemented and send back any errors using JSON. Note that we can call the getErrors() method on the form processor to retrieve an array of all errors (this will be an empty array if there are no errors). Listing 6-18 shows the changes to the AccountController.php file in ./include/Controllers. Listing 6-18. Adding Form Validation for Ajax Requests (AccountController.php) getRequest(); $fp = new FormProcessor_UserRegistration($this->db); $validate = $request->isXmlHttpRequest(); if ($request->isPost()) { if ($validate) { $fp->validateOnly(true); $fp->process($request); } else if ($fp->process($request)) { $session = new Zend_Session_Namespace('registration'); $session->user_id = $fp->user->getId(); $this->_redirect($this->getUrl('registercomplete')); } } if ($validate) { $json = array( CHAPTER 6 ■ STYLING THE WEB APPLICATION 211 9063Ch06CMP2 11/13/07 7:56 PM Page 211 'errors' => $fp->getErrors() ); $this->sendJson($json); } else { $this->breadcrumbs->addStep('Create an Account'); $this->view->fp = $fp; } } // ... other code } ?> To gain an understanding of what the return JSON data may look like, let’s look at a quick example. According to the FormProcessor_UserRegistration class, if the user enters a user- name that is already in use, the following line is executed: $this->addError('username', 'The selected username already exists'); If this were the only error to occur, the following JSON data would be generated: {"errors":{"username":"The selected username already exists"}} This means that if you assigned this JSON data to a JavaScript variable called json, you could access the error using json.errors.username, like this: var json = { "errors" : { "username" : "The selected username already exists" } } alert(json.errors.username); Creating the JavaScript Form Validator Now that we have added the necessary PHP code to implement client-side validation, we can implement the client-side portion of code. To do this, we will create a JavaScript class called UserRegistrationForm to trigger validation of the form and to display errors. Then we will attach this class to the existing HTML form. This class essentially performs the following steps: 1. Observes the existing HTML form so that when it is submitted, the JavaScript valida- tion is triggered. 2. Clears any existing errors that are being displayed (just in case the user already sub- mitted the form). 3. Submits the form data to the server for validation using Ajax. CHAPTER 6 ■ STYLING THE WEB APPLICATION212 9063Ch06CMP2 11/13/07 7:56 PM Page 212 4. Accepts the response, which contains any errors that occurred. •If there are no errors, tells the browser to submit the form normally. •If there are errors, loops over them and displays each one on the form. Because all of the error containers are already in place on the form, it is a simple matter to write the error message to the error container and then call the show() method on it (this is a method Prototype adds to all HTML elements, as we saw in Chapter 5). For more discussion of how to create JavaScript classes using Prototype, refer to Chapter 5. Initializing the UserRegistrationForm JavaScript Class To begin this class, we will first declare the class and then implement its constructor (the initialize() method). In this constructor, we will store the form as a property of the class, and then observe the onsubmit event on it. We’ll complete the constructor by calling the resetErrors() method (which we will look at next) to ensure no errors are being shown. Listing 6-19 shows the declaration and constructor of the UserRegistrationForm class. This code should be written to a file called UserRegistrationForm.class.js in the ./htdocs/js directory. Listing 6-19. Initializing the Registration Form Validation Class (UserRegistrationForm.class.js) UserRegistrationForm = Class.create(); UserRegistrationForm.prototype = { form : null, initialize : function(form) { this.form = $(form); this.form.observe('submit', this.onSubmit.bindAsEventListener(this)); this.resetErrors(); }, Hiding Form Errors Next, we will implement a utility method to help us clear any error messages. Whenever the form is submitted, we want to call this method to clear errors from any previous attempt—if a user attempts to submit the form multiple times, a different set of errors may occur. Since all errors on the form are contained within elements that have the .error class, we can simply find all of those elements and hide them. Listing 6-20 shows the code we need to add to UserRegistrationForm.class.js to clear all errors. This code first uses the Prototype getElementsBySelector() method to find the elements and then calls the invoke() enumerator method to hide each of them. CHAPTER 6 ■ STYLING THE WEB APPLICATION 213 9063Ch06CMP2 11/13/07 7:56 PM Page 213 Listing 6-20. Clearing All Form Errors with resetErrors() (UserRegistrationForm.class.js) resetErrors : function() { this.form.getElementsBySelector('.error').invoke('hide'); }, Displaying Form Errors To complement the hiding of form errors, we also need the ability to show errors. We will implement the showError() method, which takes the name of the error’s form field as the first argument and the error message as the second argument. The biggest challenge in this method is to locate the error container that corresponds to the given form field. To find this element, we use the Prototype DOM traversal functions (up() and down()) to locate the element. We make the assumption that the error container is within the same parent element as the form input. Therefore, we can find the parent element of the form element and look within that parent for an element with the class name .error. Listing 6-21 shows the code for the showError() method, which also goes in UserRegistrationForm.class.js. Listing 6-21. Writing the Error Message to a Form Element’s Error Container (UserRegistrationForm.class.js) showError : function(key, val) { var formElement = this.form[key]; var container = formElement.up().down('.error'); if (container) { container.update(val); container.show(); } }, Handling the Form Submission In Listing 6-19 we observed the onsubmit event on the user registration form. This means that when the form is submitted, the onSubmit() method in the UserRegistrationForm class is called. The goal of onSubmit() is to initiate an Ajax request that submits the form data to the registerAction() method of the AccountController class. Since this request will be initiated using Ajax, the changes we made in Listing 6-18 will come into play (that is, processing the form but not creating the user if there are no errors). The onSubmit() method begins by calling Event.stop(). This means that the browser won’t submit the form as usual once this method has been called. This allows us to control the submission of the form (we will submit it once we ensure no errors have occurred in the form). Additionally, we make a call to resetErrors() so that any errors from a previous sub- mission attempt are removed. CHAPTER 6 ■ STYLING THE WEB APPLICATION214 9063Ch06CMP2 11/13/07 7:56 PM Page 214 Listing 6-22 shows the code for the onSubmit() method in the UserRegistrationForm.class.js file. Listing 6-22. Submitting the Form Data for Validation via Ajax (UserRegistrationForm.class.js) onSubmit : function(e) { Event.stop(e); var options = { parameters : this.form.serialize(), method : this.form.method, onSuccess : this.onFormSuccess.bind(this) }; this.resetErrors(); new Ajax.Request(this.form.action, options); }, We make use of the original form method and action based on the values in the HTML code. This means that if we ever change the URL for the registration form, we don’t need to make any changes to this JavaScript code. Additionally, we can easily scale the form, since we call the serialize() method on it to retrieve all form values. This method is provided by Prototype. Handling the Form Validation Response In Listing 6-22 we specified that a method called onFormSuccess() would be used to handle the response from the form validation. In this JSON data, we are expecting an array called errors that holds all of the errors that occurred in the form validation. We can decode this data using the evalJSON() method. If this array contains one or more values, then an error has occurred. In that case, we must loop over each of these errors and call showError() for each error. Note that we also must look for the first element within the form with the class .error, since we have a global error message container at the top of the form (as discussed in Chapter 4). This line of code in our JavaScript makes this global error message appear. If the errors array is empty, we can assume the form values were all valid and tell the browser to submit the form by calling the submit() method on the form element. Listing 6-23 shows the code for the onFormSuccess() method, and the closing of the UserRegistrationForm class. Listing 6-23. Handling the Form Validation Response (UserRegistrationForm.class.js) onFormSuccess : function(transport) { var json = transport.responseText.evalJSON(true); var errors = $H(json.errors); if (errors.size() > 0) { CHAPTER 6 ■ STYLING THE WEB APPLICATION 215 9063Ch06CMP2 11/13/07 7:56 PM Page 215 this.form.down('.error').show(); errors.each(function(pair) { this.showError(pair.key, pair.value); }.bind(this)); } else { this.form.submit(); } } }; ■Note When calling each() on the errors array, we call bind() on the function so this refers to the UserRegistrationForm object. For further discussion on binding JavaScript class methods using Proto- type, refer to Chapter 5. Loading the UserRegistrationForm Class Finally, we must make use of the JavaScript class we just implemented. To do so, we will load the JavaScript file in the registration form template and then instantiate the class. Since this class relies on Prototype, make sure you have added the code to load prototype.js as instructed earlier in this chapter. Listing 6-24 shows the changes to register.tpl in ./templates/account. In addition to loading the JavaScript, we also give an ID to the form so we can refer to it when instantiating the UserRegistrationForm class. Listing 6-24. Loading and Instantiating the Form Validation Class (register.tpl) {include file='header.tpl' section='register'}
                  • {include file='footer.tpl'} CHAPTER 6 ■ STYLING THE WEB APPLICATION216 9063Ch06CMP2 11/13/07 7:56 PM Page 216 This completes the client-side form validation. If you now try to submit a form with invalid values, you will be shown the error messages as before; however, the page isn’t reloaded and the response is displayed much more quickly. Summary In this chapter we created a basic web design for our Web 2.0 application and integrated it into the existing Smarty templates. This included creating a fluid table-free layout that works well in all major browsers. We then revisited the forms we created in Chapter 2 and set up styles for them so they would be formatted nicely and display errors in a way that is easy to understand. Following this, we changed the site header template so Prototype and Scriptaculous would be automatically loaded. We immediately made use of Prototype by adding client-side form validation to the user registration form. We implemented this using Ajax and JSON. While the content in this chapter didn’t include much Web 2.0 content, it was still very important, as we started to bring together the look and feel of the site, while keeping the HTML markup to a minimum. This sets a solid base for integrating JavaScript code that will run efficiently, as well as being accessible and easy to maintain. This will also help in the load- ing speed of the site, which in turn improves the experience of users while keeping the load of your server (and the bandwidth it uses) to a minimum. In Chapter 7 we will start to build the blogging system of our web application. This will set the basis for the remainder of the book, as all features following on from here tie into this sys- tem. It also means we can really start to look at the features that define a Web 2.0 application. CHAPTER 6 ■ STYLING THE WEB APPLICATION 217 9063Ch06CMP2 11/13/07 7:56 PM Page 217 9063Ch06CMP2 11/13/07 7:56 PM Page 218 Building the Blogging System Now that users can register and log in to the web application, it is time to allow them to create their own blogs. In this chapter, we will begin to build the blogging functionality for our Web 2.0 application. We will implement the tools that will permit each user to create and manage their own blog posts. In this chapter, we will be adding the following functionality to our web application: • Enable users to create new blog posts. A blog post will consist of a title, the date sub- mitted, and the content (text or HTML) relating to the post. We will implement the form (and corresponding processing code) that allows users to enter this content, and that correctly filters submitted HTML code so JavaScript-based attacks cannot occur. This form will also be used for editing existing posts. • Permit users to preview new posts. This simple workflow system will allow users to double-check a post before sending it live. When a user creates a new post, they will have an option to either preview the post or send it live immediately. When previewing a post, they will have the option to either send it live or to make further changes. • Notify users of results. We will implement a system that notifies the user what has hap- pened when they perform an action. For instance, when they choose to publish one of their blog posts, the notification system will flash a message on the screen confirming this action once it has happened. There are additional features we will be implementing later in this book (such as tags, images, and web feeds); in this chapter we will simply lay the groundwork for the blog. There will be some repetition of Chapter 3 in this chapter when we set up database tables and classes for modifying the database, but I will keep it as brief as possible and point out the important differences. Because there is a lot of code to absorb in developing the blog management tools, Chap- ter 8 also deals with implementing the blog manager. In this chapter we will primarily deal with creating and editing blog posts; in the next chapter we will implement a what-you-see-is- what-you-get (WYSIWYG) editor to help format blog posts. Creating the Database Tables Before we start on writing the code, we must first create the database tables. We are going to create one table to hold the main blog post information and a secondary table to hold extra properties for each post (this is much like how we stored user information). This allows us to 219 CHAPTER 7 9063Ch07CMP2 11/13/07 8:06 PM Page 219 expand the data stored for blog posts in the future without requiring significant changes to the code or the database table. This is important, because in later chapters we will be expanding upon the blog functionality, and there will be extra data to be stored for each post. Let’s now take a look at the SQL required to create these tables in MySQL. The table defi- nitions can be found in the schema-mysql.sql file (in the /var/www/phpweb20 directory). The equivalent definitions for PostgreSQL can be found in the schema-pgsql.sql file. Listing 7-1 shows the SQL used to create the blog_posts and blog_posts_profile tables. Listing 7-1. SQL to Create the blog_posts Table in MySQL (schema-mysql.sql) create table blog_posts ( post_id serial not null, user_id bigint unsigned not null, url varchar(255) not null, ts_created datetime not null, status varchar(10) not null, primary key (post_id), foreign key (user_id) references users (user_id) ) type = InnoDB; create index blog_posts_url on blog_posts (url); create table blog_posts_profile ( post_id bigint unsigned not null, profile_key varchar(255) not null, profile_value text not null, primary key (post_id, profile_key), foreign key (post_id) references blog_posts (post_id) ) type = InnoDB; In blog_posts we link (using a foreign key constraint) to the users table, as each post will belong to a single user. We also store a timestamp of the creation date. This is the field we will primarily be sorting on when displaying blog posts, since a blog is essentially a journal that is organized by the date of each post. We will use the url field to store a permanent link for the post, generated dynamically based on the title of the post. Additionally, since we will be using this field to load blog posts (as you will see in Chapter 9), we create an index on this field in the database to speed up SQL select queries that use this field. The other field of interest here is the status field, which we will use to indicate whether or not a post is live. This will help us implement the preview functionality. The blog_posts_profile table is almost a duplicate of the users_profile table, but it links to the blog_posts table instead of the users table. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM220 9063Ch07CMP2 11/13/07 8:06 PM Page 220 ■Note As discussed in Chapter 3, when using PostgreSQL we use timestamptz instead of datetime for creating timestamp fields. Additionally, we use int for a foreign key to a serial (instead of bigint unsigned). Specifying the InnoDB table type is MySQL-specific functionality so constraints will be enforced. Setting Up DatabaseObject and Profile Classes In this section, we will add new models to our application that allow us to control data in the database tables we just created. We do this the same way we managed user data in Chapter 3. That is, we create a DatabaseObject subclass to manage the data in the blog_posts table, and we create a Profile subclass to manage the blog_posts_profile table. It may appear that we’re duplicating some code, but the DatabaseObject class makes it very easy to manage a large number of database tables, as you will see. Additionally, we will add many functions to the DatabaseObject_BlogPost class that aren’t relevant to the Data- baseObject_User class. Creating the DatabaseObject_BlogPost Class Let’s first take a look at the DatabaseObject_BlogPost class. Listing 7-2 shows the contents of the BlogPost.php file, which should be stored in the ./include/DatabaseObject directory. Listing 7-2. Managing Blog Post Data (BlogPost.php in ./include/DatabaseObject) add('user_id'); $this->add('url'); $this->add('ts_created', time(), self::TYPE_TIMESTAMP); $this->add('status', self::STATUS_DRAFT); $this->profile = new Profile_BlogPost($db); } protected function postLoad() { $this->profile->setPostId($this->getId()); CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 221 9063Ch07CMP2 11/13/07 8:06 PM Page 221 $this->profile->load(); } protected function postInsert() { $this->profile->setPostId($this->getId()); $this->profile->save(false); return true; } protected function postUpdate() { $this->profile->save(false); return true; } protected function preDelete() { $this->profile->delete(); return true; } } ?> ■Caution This class relies on the Profile_BlogPost class, which we will be writing shortly, so this class will not work until we add that one. This code is somewhat similar to the DatabaseObject_User class in that we initialize the $_profile variable, which we eventually populate with an instance of Profile_BlogPost. Addi- tionally, we use callbacks in the same manner as DatabaseObject_User. Many of the utility functions in DatabaseObject_User were specific to managing user data, so they’re obviously excluded from this class. The key difference between DatabaseObject_BlogPost and DatabaseObject_User is that here we define two constants (using the const keyword) to define the different statuses a blog post can have. Blog posts in our application will either be set to draft or live (D or L). We use constants to define the different statuses a blog post can have because these val- ues never change. Technically you could use a static variable instead; however, static variables are typically used for values that are set once only, at runtime. Additionally, by using constants we don’t need to concern ourselves with the actual value that is stored in the database. Rather than hard-coding a magic value of D every time you want to refer to the draft status, you can instead refer to DatabaseObject_BlogPost::STATUS_DRAFT in your code. Sure, it’s longer in the source code, but it’s much clearer when reading the code, and the internal cost of storage is the same. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM222 9063Ch07CMP2 11/13/07 8:06 PM Page 222 Creating the Profile_BlogPost Class The Profile_BlogPost class that we use to control the profile data for each post is almost iden- tical to the Profile_User class. The only difference between the two is that we name the utility function setPostId() instead of setUserId(). The code for this class is shown in Listing 7-3 and is to be stored in BlogPost.php in the ./include/Profile directory. Listing 7-3. Managing Blog Post Profile Data (BlogPost.php in ./include/Profile) 0) $this->setPostId($post_id); } public function setPostId($post_id) { $filters = array('post_id' => (int) $post_id); $this->_filters = $filters; } } ?> Creating a Controller for Managing Blog Posts In its current state, our application has three MVC controllers: the index, account, and utility controllers. In this section, we will create a new controller class called BlogmanagerController specifically for managing blog posts. This controller will handle the creation and editing of blog posts, the previewing of posts (as well as sending them live), as well as the deletion of posts. This controller will not perform any tasks relating to displaying a user’s blog publicly (either on the application home page or on the user’s personal page); we will implement this functionality in Chapter 9. Extending the Application Permissions Before we start creating the controller, we must extend the permissions in the CustomControllerAclManager class so only registered (and logged-in) users can access it. The way we do this is to first deny all access to the blogmanager controller, and then allow access for the member user role (which automatically also opens it up for the administrator user type, because administrator inherits from member). We must also add blogmanager as a resource before access to it can be controlled. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 223 9063Ch07CMP2 11/13/07 8:06 PM Page 223 In the constructor of the CustomerControllerAclManager.php file (located in ./include/Controllers), we will add the following three lines in this order: $this->acl->add(new Zend_Acl_Resource('blogmanager')); $this->acl->deny(null, 'blogmanager'); $this->acl->allow('member', 'blogmanager'); Listing 7-4 shows how you should add them to this file. Listing 7-4. Adding Permissions for the Blog Manager Controller (CustomControllerAclManager.php) auth = $auth; $this->acl = new Zend_Acl(); // add the different user roles $this->acl->addRole(new Zend_Acl_Role($this->_defaultRole)); $this->acl->addRole(new Zend_Acl_Role('member')); $this->acl->addRole(new Zend_Acl_Role('administrator'), 'member'); // add the resources we want to have control over $this->acl->add(new Zend_Acl_Resource('account')); $this->acl->add(new Zend_Acl_Resource('blogmanager')); $this->acl->add(new Zend_Acl_Resource('admin')); // allow access to everything for all users by default // except for the account management and administration areas $this->acl->allow(); $this->acl->deny(null, 'account'); $this->acl->deny(null, 'blogmanager'); $this->acl->deny(null, 'admin'); // add an exception so guests can log in or register // in order to gain privilege $this->acl->allow('guest', 'account', array('login', 'fetchpassword', 'register', 'registercomplete')); // allow members access to the account management area $this->acl->allow('member', 'account'); CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM224 9063Ch07CMP2 11/13/07 8:06 PM Page 224 $this->acl->allow('member', 'blogmanager'); // allow administrators access to the admin area $this->acl->allow('administrator', 'admin'); } // ... other code } ?> Refer back to Chapter 3 if you need a reminder of how Zend_Acl works and how we use it in this application. The BlogmanagerController Actions Let’s now take a look at a skeleton of the BlogmanagerController class, which at this stage lists each of the different action handlers we will be implementing in this chapter (except for indexAction(), which will be implemented in Chapter 8). Listing 7-5 shows the contents of BlogmanagerController.php, which we will store in the ./include/Controllers directory. Listing 7-5. The Skeleton for the BlogmanagerController Class (BlogmanagerController.php) breadcrumbs->addStep('Account', $this->getUrl(null, 'account')); $this->breadcrumbs->addStep('Blog Manager', $this->getUrl(null, 'blogmanager')); $this->identity = Zend_Auth::getInstance()->getIdentity(); } public function indexAction() { } public function editAction() { } public function previewAction() { CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 225 9063Ch07CMP2 11/13/07 8:06 PM Page 225 } public function setstatusAction() { } } ?> As part of the initial setup for this controller, I’ve added in the calls to build the appropri- ate breadcrumb steps. Additionally, since all of the actions we will add to this controller will require the user ID of the logged-in user, I’ve also provided easy access to the user identity data by assigning it to an object property. There are four controller action methods we must implement to complete this phase of the blog management system: • indexAction(): This method will be responsible for listing all posts in the blog. At the top of this page, a summary of each of the current month’s posts will be shown. Previ- ous months will be listed in the left column, providing access to posts belonging to other months. This will be implemented in Chapter 8. • editAction(): This action method is responsible for creating new blog posts and editing existing posts. If an error occurs, this action will be displayed again in order to show these errors. • previewAction(): When a user creates a new post, they will have the option of preview- ing it before it is sent live. This action will display their blog post to them, giving them the option of making further changes or publishing the post. This action will also be used to display a complete summary of a single post to the user. • setstatusAction(): This method will be used to update the status of a post when a user decides to publish it live. This will be done by setting the post’s status from DatabaseObject_BlogPost::STATUS_DRAFT to DatabaseObject_BlogPost::STATUS_LIVE. Once it has been sent live, previewAction() will show a summary of the post and con- firm that it has been sent live. The setstatusAction() method will also allow the user to send a live post back to draft or to delete blog posts. A confirmation message will be shown after a post is deleted, except the user will be redirected to indexAction() (since the post will no longer exist, and they cannot be redirected back to the preview page). Linking to Blog Manager Before we start to implement the actions in BlogmanagerController, let’s quickly create a link on the account home page to the blog manager. Listing 7-6 shows the new lines we will add to the index.tpl file from the ./templates/account directory. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM226 9063Ch07CMP2 11/13/07 8:06 PM Page 226 Listing 7-6. Linking to the Blog Manager from the Account Home Page (index.tpl) {include file='header.tpl' section='account'} Welcome {$identity->first_name}. {include file='footer.tpl'} The other link we will add is in the main navigation across the top of the page. This item will only be shown to logged-in users. Listing 7-7 shows the new lines in the header.tpl navi- gation (in ./templates), which creates a new list item labeled “Your Blog”. Listing 7-7. Linking to the Blog Manager in the Site Navigation (header.tpl)
        • {if $fp->post->isLive()} {assign var='label' value='Save Changes'} {elseif $fp->post->isSaved()} {assign var='label' value='Save Changes and Send Live'} {else} {assign var='label' value='Create and Send Live'} {/if} {if !$fp->post->isLive()} {/if}
          {include file='footer.tpl'} In this template, we use the {assign} Smarty function to set the label for the submit but- tons. This function allows you to create template variables on the fly. Using it has the same effect as assigning variables from your PHP code. The name argument is the name the new variable will have in the template, while the value argument is the value to be assigned to this variable. ■Note Be careful not to overuse {assign}; you may find yourself including application logic in your tem- plates if you use it excessively. In this instance, we are only using it to help with the display logic—we are using it to create temporary placeholders for button labels so we don’t have to duplicate the HTML code used to create submit buttons. Instantiating FormProcessor_BlogPost in editAction() The next step in being able to create or edit blog posts is to implement editAction() in the BlogmanagerController class. We will use the same controller action for displaying the edit form and for calling the form processor when the user submits the form. This allows us to eas- ily display any errors that occurred when processing the form, since the code will fall through to display the template again if an error occurs. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 231 9063Ch07CMP2 11/13/07 8:06 PM Page 231 Since we are using this action to edit posts as well as create new posts, we need to check for the id parameter in the URL, as this is what will be passed in to the form processor as the third argument if an existing post is to be edited. We then fetch the user ID from the user’s identity and instantiate the FormProcessor_ BlogPost class, which we will implement shortly. The form processor will try to load an exist- ing blog post for that user based on the ID passed in the URL. If it is unable to find a matching record for the ID, it behaves as though a new post is being created. The next step is to check whether the action has been invoked by submitting the blog post submission form. If so, we need to call the process() method of the form processor. If the form is successfully processed, the user will be redirected to the previewAction() method. If an error occurs, the code falls through to creating the breadcrumbs and displaying the form (just as it would when initially viewing the edit blog post page). Note that the breadcrumbs include a check to see whether an existing post is being edited (which is done by checking if the $fp->post object has been saved). If it is, we include a link back to the post preview page in the breadcrumb trail. Listing 7-12 shows the full contents of editAction() from the BlogmanagerController.php file, which concludes by assigning the $fp object to the view so it can be used in the template we created previously. Listing 7-12. The editAction() Method,Which Displays and Processes the Form (BlogmanagerController.php) getRequest(); $post_id = (int) $this->getRequest()->getQuery('id'); $fp = new FormProcessor_BlogPost($this->db, $this->identity->user_id, $post_id); if ($request->isPost()) { if ($fp->process($request)) { $url = $this->getUrl('preview') . '?id=' . $fp->post->getId(); $this->_redirect($url); } } if ($fp->post->isSaved()) { $this->breadcrumbs->addStep( 'Preview Post: ' . $fp->post->profile->title, $this->getUrl('preview') . '?id=' . $fp->post->getId() ); CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM232 9063Ch07CMP2 11/13/07 8:06 PM Page 232 $this->breadcrumbs->addStep('Edit Blog Post'); } else $this->breadcrumbs->addStep('Create a New Blog Post'); $this->view->fp = $fp; } // ... other code } ?> ■Note Regardless of whether the user chooses to preview the post or to send the post live straight away, they are still redirected to the post preview page after a post has been saved. The difference between send- ing a post live and previewing it is the status value that is stored with the post, which determines whether or not other people will be able to read the post. Implementing the FormProcessor_BlogPost Class Finally, we need to implement the FormProcessor_BlogPost class, which is used to process the blog post edit form. Just as we did for user registration, we are going to extend the FormProcessor class to simplify the tasks of sanitizing form values and storing errors. Because we’re using the same class for both creating new posts and editing existing posts, we need to handle this in the constructor. Listing 7-13 shows the constructor for the FormProcessor_BlogPost class, which accepts the database connection and the ID of the user creating the post as the first two arguments. The third argument is optional, and if specified is the ID of the post to be edited. Omitting this argument (or passing a value of 0, since our primary key sequence only generates values greater than 0) indicates a new post will be created. This code should be written to a file called BlogPost.php in the ./include/FormProcessor directory. Listing 7-13. The Constructor for FormProcessor_BlogPost (BlogPost.php) db = $db; CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 233 9063Ch07CMP2 11/13/07 8:06 PM Page 233 $this->user = new DatabaseObject_User($db); $this->user->load($user_id); $this->post = new DatabaseObject_BlogPost($db); $this->post->loadForUser($this->user->getId(), $post_id); if ($this->post->isSaved()) { $this->title = $this->post->profile->title; $this->content = $this->post->profile->content; $this->ts_created = $this->post->ts_created; } else $this->post->user_id = $this->user->getId(); } public function process(Zend_Controller_Request_Abstract $request) { // ... other code } } ?> The purpose of the constructor of this class is to try to load an existing blog post based on the third argument. If the blog post can be loaded, the class is being used to edit an existing post; otherwise it is being used to process the form for a new blog post. An important feature of this code is that we use a new method called loadForUser(), which is a custom loader method for DatabaseObject_BlogPost. This ensures that the loaded post belongs to the corresponding user. If we didn’t check this, it would be possible for a user to edit the posts of any other user simply by manipulating the URL. Listing 7-14 shows the code for loadForUser(), which we will add to DatabaseObject_BlogPost. In order to write a custom loader for DatabaseObject, we simply need to create an SQL select query with the desired conditions (where statements) that retrieves all of the columns in the table, and pass that query to the internal _load() method. We will use the helper function getSelectFields() to retrieve an array of the columns to fetch in the custom loader SQL (the values in this array are determined by the columns speci- fied in the class constructor). There is also a small optimization at the start of the function that bypasses performing the SQL if invalid values are specified for $user_id and $post_id. This function should be added to the BlogPost.php file in the ./include/DatabaseObject directory. Listing 7-14. A Custom Loader for DatabaseObject_BlogPost (BlogPost.php) getSelectFields()), $this->_table, $user_id, $post_id ); return $this->_load($query); } // ... other code } ?> Looking back to the constructor for the form processor in Listing 7-13, if an existing blog post was successfully loaded, we initialize the form processor with the values of the loaded blog post. This is so that those existing values will be shown in the form. If an existing post wasn’t loaded, we set the user_id property to be that of the loaded user. This means that when the post is saved in the process() method (as we will shortly see), the user_id property has already been set. Next, we must process the submitted form by implementing the process() method in FormProcessor_BlogPost. The steps involved in processing this form are as follows: 1. Check the title and ensure that a value has been entered. 2. Validate the date and time submitted for the post. 3. Filter unwanted HTML out of the blog post body. 4. Check whether or not the post should be sent live immediately. 5. Save the post to the database. First, to check the title we need to initialize and clean the value using the sanitize() method we first used in Chapter 3. To restrict the length of the title to a maximum of 255 char- acters (the maximum length of the field in our database schema), we pass the value through substr(). If you try to insert a value into the database longer than the field’s definition, the database will simply truncate the variable anyway. We then check the title’s length, recording an error if the length is zero. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 235 9063Ch07CMP2 11/13/07 8:06 PM Page 235 Note that this isn’t very strict checking at all. You may want to extend this check to ensure that at least some alphanumeric characters have been entered. Listing 7-15 shows the code that initializes and checks the title value. Listing 7-15. Validating the Blog Post Title (BlogPost.php) title = $this->sanitize($request->getPost('username')); $this->title = substr($this->title, 0, 255); if (strlen($this->title) == 0) $this->addError('title', 'Please enter a title for this post'); // ... other code } } ?> Next, we need to process the submitted date and time to ensure that the specified date is real. We don’t really mind what the date and time are, as long as it is a real date (so November 31, for instance, would fail). To simplify the interface, we showed users a 12-hour clock (rather than a 24-hour clock), so we need to check the meridian (“am/pm”) value and adjust the submitted hour accord- ingly. We will also use the max() and min() functions to ensure the hour is a value from 1 to 12 and the minute is a value from 0 to 59. Finally, once the date and time have been validated, we will use the mktime() function to create a timestamp that we can pass to DatabaseObject_BlogPost. ■Note Beginning in PHP 5.2.0 there is a built-in DateTime class available, which can be used to create and manipulate timestamps. It remains to be seen how popular this class will be. I have chosen to use exist- ing date manipulation functions that most users will already be familiar with. The code used to initialize and validate the date and time is shown in Listing 7-16. Once we create the timestamp, we must store it in the form processor object so the value can be used when outputting the form again if an error occurs. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM236 9063Ch07CMP2 11/13/07 8:06 PM Page 236 Listing 7-16. Initializing and Processing the Date and Time (BlogPost.php) (int) $request->getPost('ts_createdYear'), 'm' => (int) $request->getPost('ts_createdMonth'), 'd' => (int) $request->getPost('ts_createdDay') ); $time = array( 'h' => (int) $request->getPost('ts_createdHour'), 'm' => (int) $request->getPost('ts_createdMinute') ); $time['h'] = max(1, min(12, $time['h'])); $time['m'] = max(0, min(59, $time['m'])); $meridian = strtolower($request->getPost('ts_createdMeridian')); if ($meridian != 'pm') $meridian = 'am'; // convert the hour into 24 hour time if ($time['h'] < 12 && $meridian == 'pm') $time['h'] += 12; else if ($time['h'] == 12 && $meridian == 'am') $time['h'] = 0; if (!checkDate($date['m'], $date['d'], $date['y'])) $this->addError('ts_created', 'Please select a valid date'); $this->ts_created = mktime($time['h'], $time['m'], 0, $date['m'], $date['d'], $date['y']); // ... other code } } ?> CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 237 9063Ch07CMP2 11/13/07 8:06 PM Page 237 Next, we must initialize the blog post body. Since we are allowing a limited set of HTML to be used by users, we must filter the data accordingly. We will write a method called cleanHtml() to do this. Listing 7-17 shows how we will retrieve the content value from the form, as well as the method we use to filter it (cleanHtml()). This method has been left blank for now, but in the next section we will look more closely at filtering the HTML, which is a very important aspect of securing web-based applications. Listing 7-17. Initializing and Processing the Blog Post Content (BlogPost.php) content = $this->cleanHtml($request->getPost('content')); // ... other code } // temporary placeholder protected function cleanHtml($html) { return $html; } } ?> ■Tip You may want to specify a maximum length for blog posts (such as a maximum of 5000 characters), although users will likely find this restrictive and annoying. If you were to do this, you could create a new configuration setting in the settings.ini file that defines the maximum length. Note that you would also need to take the HTML tags into consideration. For instance, even though we are allowing some HTML tags, you might want to strip all tags before determining the length of a post. At this point in the code, the submitted form data will have been read from the form and validated. However, before we save the post, we must determine whether the user wants to preview the post or send it live straight away. We do this by checking for the presence of the preview variable in the submitted form. Since we are using two submit buttons on the form, we must name the buttons differently so we can determine which one was clicked. We named CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM238 9063Ch07CMP2 11/13/07 8:06 PM Page 238 the preview button preview (see Listing 7-12), so if the preview value is set in the form, we know the user clicked that button. (This test can be seen in Listing 7-19.) In order to make the post live, we must set the status value of the blog post to STATUS_LIVE (since a post is marked as preview initially by default). We will create a new method called sendLive() in the DatabaseObject_BlogPost class to help us with this—it is shown in Listing 7-18. Listing 7-18. Easily Setting a Blog Post to Live Status (BlogPost.php) status != self::STATUS_LIVE) { $this->status = self::STATUS_LIVE; $this->profile->ts_published = time(); } } public function isLive() { return $this->isSaved() && $this->status == self::STATUS_LIVE; } } ?> In the preceding code, we also set a profile variable (that is, a value that is written to the blog_posts_profile table) called ts_published, which stores a timestamp of when the post was set live. Note that the post still needs to be saved after calling this function. The ts_published variable is only set if the status value is actually being changed. In order to check whether or not a post is live, we also add a helper method called isLive() to this class, which returns true if the status value is self::STATUS_LIVE. In Listing 7-19 we continue implementing the form processor. We first check whether or not any errors have occurred by using the hasError() method. If no errors have occurred, we set the values of the DatabaseObject_BlogPost object and then mark the post as published if required. Finally, we save the database record and return from process(). CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 239 9063Ch07CMP2 11/13/07 8:06 PM Page 239 Listing 7-19. Saving the Database Record and Returning from the Processor (BlogPost.php) hasError()) { $this->post->profile->title = $this->title; $this->post->ts_created = $this->ts_created; $this->post->profile->content = $this->content; $preview = !is_null($request->getPost('preview')); if (!$preview) $this->post->sendLive(); $this->post->save(); } // return true if no errors have occurred return !$this->hasError(); } // ... other code } ?> We are nearly at the stage where we can create new blog posts. However, before the form we have created will work, we must perform one final step: create a unique URL for each post. We will now complete this step. Generating a Permanent Link to a Blog Post One thing we have overlooked so far is the setting of the url field we created in the blog_posts table. Every post in a user’s blog must have a unique value for this field, as the value is used to create a URL that links directly to the respective blog post. We will generate this value automatically, based on the title of the blog post (as specified by the user when they create the post). We can automate the generation of this value by using the preInsert() method in the DatabaseObject_BlogPost class. This method is called immedi- ately prior to executing the SQL insert statement when creating a new record. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM240 9063Ch07CMP2 11/13/07 8:06 PM Page 240 ■Note Generating the URL automatically when creating the blog post doesn’t give users the opportunity to change the URL. If they were able to change this value, it would somewhat defeat the purpose of a perma- nent link. However, if the user chooses to change the title of their post, the URL will no longer be based on the title. You may want to add an option to the form to let users change the URL value—to simplify matters, I have not included this option. There are four steps to generating a unique URL: 1. Turn the title value into a string that is URL friendly. To do this, we will ensure that only letters, numbers, and hyphens are included. Additionally, we will make the entire string lowercase for uniformity. We will make the string a maximum of 30 characters, which should be enough to ensure uniqueness. For example, a title of “Went to the movies” could be turned into went-to-the-movies. Note that these rules aren’t hard and fast—you can adapt them as you please. 2. Check whether or not the generated URL already exists for this user. If it doesn’t, proceed to step 4. 3. If the URL already exists, create a unique one by appending a number to the end of the string. So if went-to-the-movies already existed, we would make the URL went-to-the- movies-2. If this alternate URL already existed, we would use went-to-the-movies-3. This process can be repeated until a unique URL is found. 4. Set the URL field in the blog post to the generated value. Listing 7-20 shows the generateUniqueUrl() method, which we will now add to the BlogPost.php file in ./include/DatabaseObject. This method accepts a string as its value and returns a unique value to be used as the URL. The listing also shows the preInsert() method, which calls generateUniqueUrl(). Remember that preInsert() is automatically called when the save() method is called for new records. Listing 7-20. Automatically Setting the Permanent Link for the Post (BlogPost.php) url = $this->generateUniqueUrl($this->profile->title); return true; } CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 241 9063Ch07CMP2 11/13/07 8:06 PM Page 241 // ... other code already in this class protected function generateUniqueUrl($title) { $url = strtolower($title); $filters = array( // replace & with 'and' for readability '/&+/' => 'and', // replace non-alphanumeric characters with a hyphen '/[^a-z0-9]+/i' => '-', // replace multiple hyphens with a single hyphen '/-+/' => '-' ); // apply each replacement foreach ($filters as $regex => $replacement) $url = preg_replace($regex, $replacement, $url); // remove hyphens from the start and end of string $url = trim($url, '-'); // restrict the length of the URL $url = trim(substr($url, 0, 30)); // set a default value just in case if (strlen($url) == 0) $url = 'post'; // find similar URLs $query = sprintf("select url from %s where user_id = %d and url like ?", $this->_table, $this->user_id); $query = $this->_db->quoteInto($query, $url . '%'); $result = $this->_db->fetchCol($query); // if no matching URLs then return the current URL if (count($result) == 0 || !in_array($url, $result)) return $url; CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM242 9063Ch07CMP2 11/13/07 8:06 PM Page 242 // generate a unique URL $i = 2; do { $_url = $url . '-' . $i++; } while (in_array($_url, $result)); return $_url; } } ?> ■Note The position of these functions in the file is not important, but I tend to keep the callbacks near the top of the classes and put other functions later on in the code. At the beginning of generateUniqueUrl(), we apply a series of regular expressions to filter out unwanted values and to clean up the string. This includes ensuring the string only has letters, numbers, and hyphens in it, as well as ensuring that multiple hyphens don’t appear consecutively in the string. We also trim any hyphens from the start and end of the string. As a final touch to make the string nicer, we replace the & character with the word and. ■Tip As an exercise, you may want to change this portion of the function to use a custom filter that extends from Zend_Filter.To do this, you would create a class called Zend_Filter_CreateUrl (or something similar) that implements the filter() method. Next, we check the database for any other URLs belonging to the current user that begin with the URL we have just generated. This is done by fetching other URLs that were previously generated from the same value, and then looping until we find a new value that isn’t in the database. At this stage, the code is sufficiently developed that you will be able to use the form at http://phpweb20/blogmanager/edit to create a new blog post. However, we will continue to develop the blog management area in this chapter. Filtering Submitted HTML In this application, we allow anybody that signs up (using the registration form created earlier) to submit their own content. Because of this, we need to protect against malicious users whose goal is to attack the web site or its users. This is crucial to ensuring the security of web applications such as this one, where any user can submit data. In situations where only trusted users will be submitting data, filtering data is not as critical, but when anybody can sign up, it is extremely important. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 243 9063Ch07CMP2 11/13/07 8:06 PM Page 243 The primary thing we want to protect against is a malicious user submitting JavaScript in one of their posts, which is then executed by another user who views their blog. There are sev- eral common ways a malicious user might try to inject JavaScript code into their postings: • Inserting
          {if $post->isLive()}
          This post is live on your blog. To unpublish it click the Unpublish post button below.
          ■Note This template won’t work until we complete it below—we’re currently in the middle of an {if} statement. In the preceding code, the status box that is created is for live listings only. This is deter- mined by calling the isLive() method on the post. Listing 7-24 shows the remainder of this template, which shows similar code for unpublished listings. Listing 7-24. The Second Half of the Preview Template (preview.tpl) {else}
          This post is not yet live on your blog. To publish it on your blog, click the button below.
          CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM250 9063Ch07CMP2 11/13/07 8:06 PM Page 250
          {/if}
          {$post->ts_created|date_format:'%x %X'}
          {$post->profile->content}
          {include file='footer.tpl'} As you can see, after the if/else statement is closed, we output the date and time of the post, as well as the content of the post. To output the date and time, we use the date_format modifier, which uses the same arguments as the PHP strftime() function. We use the %x switch to output the current date and %X for the current time, both using the preferred repre- sentation for the current locale. Next we need to add some new styles to format the status box and the date and time. We will show the status box in green for published posts and in orange for unpublished posts. Listing 7-25 shows the new styles we will add to the ./htdocs/css/styles.css file. Listing 7-25. New Styles Used to Format the Blog Post Preview (styles.css) @media screen { /* ... other code */ /** * Status boxes */ div.status { padding : 5px; margin : 5px 0; } .status.live { color : #fff; background : #070; } CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 251 9063Ch07CMP2 11/13/07 8:06 PM Page 251 .status.draft { color : #fff; background : #fa0; } /** * Previewing of blog posts */ .preview-status form { margin-top : 5px; } .preview-status { margin-bottom : 10px; } .preview-date { font-size : 0.9em; color : #999; } } /* ... other code */ ■Tip To apply styles to elements with multiple class names (as we did with
          ), you simply include both class names without spacing in the CSS file. So, in this case, we can apply styles to .status.live. Note that the support of this functionality in Internet Explorer 6 is somewhat unpre- dictable, and the order of the classes can sometimes affect how the markup is rendered (so in IE6 .live.status may behave differently than .status.live), depending on the makeup of other styles in the style sheet. Requesting Confirmation for User Actions Finally, as a way to improve the interface, we will display a confirmation box when a user tries to publish (or unpublish) a blog post, as well as when they try to delete a post. To help with this, we will now create a new JavaScript file in which we observe the click events on each of those but- tons. For further details on how Prototype’s Event.observe() works, refer to Chapter 5. Listing 7-26 shows the code we will add to the ./htdocs/js/blogPreview.js file (this file was loaded by the code in Listing 7-23). In this code, we check that each element exists before trying to observe the click event, since the publish button won’t be shown for posts that are already published, and the unpublish button won’t be shown for draft posts. Listing 7-26. Attaching Click Events to the Post Preview Buttons (blogPreview.js) Event.observe(window, 'load', function() { var publishButton = $('status-publish'); CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM252 9063Ch07CMP2 11/13/07 8:06 PM Page 252 var unpublishButton = $('status-unpublish'); var deleteButton = $('status-delete'); if (publishButton) { publishButton.observe('click', function(e) { if (!confirm('Click OK to publish this post')) Event.stop(e); }); } if (unpublishButton) { unpublishButton.observe('click', function(e) { if (!confirm('Click OK to unpublish this post')) Event.stop(e); }); } if (deleteButton) { deleteButton.observe('click', function(e) { if (!confirm('Click OK to permanently delete this post')) Event.stop(e); }); } }); ■Note This code goes inside the window onload event to ensure that the button elements exist in the DOM when this code is executed. In the preceding code we want to stop the form from being submitted if the user clicks cancel in any of the confirmation boxes. To achieve this, we call the Event.stop() method. ■Note You may be more familiar with returning false from links or forms to prevent the browser from proceeding. In Prototype’s event handling, this does not apply—to prevent an event from propagating (that is, from following the link or submitting the form) after the event has been handled, you must call Event.stop(). This is covered in more detail in Chapter 5. Figure 7-2 shows the preview page for a post that has not yet been published. Note that the buttons will not work until we implement the setstatus action. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 253 9063Ch07CMP2 11/13/07 8:06 PM Page 253 Figure 7-2. Previewing a blog post that is not yet live Updating the Status of a Blog Post In Listings 7-23 and 7-24 we created a form to update the status of a blog post. We must now implement the controller action to handle the processing of this form. In addition to changing the status or deleting the post, we also added an option to edit posts. If the user clicks the edit button, we need to redirect them to the edit action we created earlier. Completing setstatusAction() Since the earlier version of setstatusAction() we created was empty, we will complete the method with the code shown in Listing 7-27. Listing 7-27. Handling the Different Types of Status Updates (BlogmanagerController.php) getRequest(); $post_id = (int) $request->getPost('id'); $post = new DatabaseObject_BlogPost($this->db); if (!$post->loadForUser($this->identity->user_id, $post_id)) CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM254 9063Ch07CMP2 11/13/07 8:06 PM Page 254 $this->_redirect($this->getUrl()); // URL to redirect back to $url = $this->getUrl('preview') . '?id=' . $post->getId(); if ($request->getPost('edit')) { $this->_redirect($this->getUrl('edit') . '?id=' . $post->getId()); } else if ($request->getPost('publish')) { $post->sendLive(); $post->save(); } else if ($request->getPost('unpublish')) { $post->sendBackToDraft(); $post->save(); } else if ($request->getPost('delete')) { $post->delete(); // Preview page no longer exists for this page so go back to index $url = $this->getUrl(); } $this->_redirect($url); } // ... other code } ?> Once again, as in previewAction(), we initialize the post ID and try to load the record based on that value and the user ID of the logged-in user. Since we are accessing the request variables several times in the method, it makes the code somewhat more readable to assign the request to $request. Next, we define the return URL. This is where the user will be redirected to after the cur- rent action has completed (apart from the edit and delete actions). This URL is simply the preview page for the given blog post. To determine which action to take, we simply need to check for the presence of the appropriate variable in the request post data. For example, if the user clicks the publish button, publish will be set in the post data, but the other buttons won’t be. In order to unpublish a live blog post, we will use a helper function called sendBackToDraft(), which does nothing more than set the status value of the post to DatabaseObject_BlogPost:: STATUS_DRAFT. The function—which we will add to ./include/DatabaseObject/BlogPost.php—is provided more for completeness than anything, since we already have the sendLive() function. The sendBackToDraft() function is shown in Listing 7-28. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 255 9063Ch07CMP2 11/13/07 8:06 PM Page 255 Listing 7-28. The sendBackToDraft() Function (BlogPost.php) status = self::STATUS_DRAFT; } // ... other code } ?> Referring back to Listing 7-27, you can see that it’s simply a matter of calling the delete() method on the DatabaseObject_BlogPost object to delete the post. Since the preview page for this post will no longer be valid (since the post doesn’t exist), we will change the URL to redi- rect the user back to the blog manager index page. ■Note If you look closely at setstatusAction(), you will notice that if you pass in a valid post ID but not a valid action, all that occurs is that the user is redirected to the post preview page. You can take advantage of this if you want to provide a submit button to reach the preview page. Notifying the User As the code stands now, the user isn’t informed when a change is made. For example, when a post is deleted, the user is simply redirected back to the blog manager index page and is not told that the post was actually deleted. While this is not a huge problem when sending a post live (or changing it back to draft) due to the colored box we use to display the status, it is still good practice to inform them of the change that was made. It is also important after updating an existing post to notify the user that the changes have been saved. To achieve this, we are going to display a message to the user after they arrive on the “next” page (that is, the page we redirect them to after competing the chosen action). One thing to be aware of is that the new page needs to know about the status message somehow. To do this, we will store the message in the user’s session, and then remove it from the session once they have viewed it (to prevent it from continually being shown). Fortunately, Zend_Controller provides us with functionality to achieve this. The FlashMessenger action helper class (in no way related to Adobe’s Flash technology) allows us to easily do exactly this. It is instantiated automatically by Zend Controller when we try to access it. The other change we will make to our code is to assign all messages found in the flash messenger object to the template. In the template, we will then check whether there are any messages, and if so we can output them accordingly. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM256 9063Ch07CMP2 11/13/07 8:06 PM Page 256 ■Note This flash message container will integrate nicely with any real-time operations we perform using Ajax. We can reuse this same container to display any messages that are generated dynamically with JavaScript. As we implement Ajax features in this book, we will use this container. Adding FlashMessenger to CustomControllerAction We are going to create the flash messenger in the init() function of CustomControllerAction, which means it will be created for every single request that takes place on our site. This makes it very useful, as we can then use it not only to tell logged-in users about updates to their blog posts, but also to give any notification to any user (whether authenticated or not). To instantiate the flash messenger, we simply access it from the _helper object, which is an internal property of Zend_Controller_Action (the class which CustomControllerAction extends from). If $this->_helper doesn’t find the flash messenger, it will automatically create it for us. Listing 7-29 shows the code we will add to the CustomControllerAction.php file (in the ./include directory), which includes not only additions to init(), but also assigns any mes- sages that may be stored in the messenger to the template. Listing 7-29. Creating the Flash Messenger and Assigning Its Messages to the Template (CustomControllerAction.php) messenger = $this->_helper->_flashMessenger; } // ... other code public function postDispatch() { // ... other code $this->view->messages = $this->messenger->getMessages(); } } ?> CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 257 9063Ch07CMP2 11/13/07 8:06 PM Page 257 Writing Messages to FlashMessenger The next step is to write messages to the flash messenger as required. In the case of updating the status of blog posts, we will write a message when a post is sent live, when a post is unpub- lished, and when a post is deleted. We will make further use of the messenger in other parts of this web application. To add a message, we simply call $this->messenger->addMessage('The message'). Effectively, all this does is write a message to the current session, which will automatically be deleted on the subsequent page request (meaning it is retrieved for display in the next request and then immediately deleted). Listing 7-30 shows a new version of the setstatusAction() function for BlogmanagerController, which now adds messages to the $this->messenger object as required. Listing 7-30. Adding Messages to the Messenger As Required (BlogmanagerController.php) getRequest(); $post_id = (int) $request->getPost('id'); $post = new DatabaseObject_BlogPost($this->db); if (!$post->loadForUser($this->identity->user_id, $post_id)) $this->_redirect($this->getUrl()); // URL to redirect back to $url = $this->getUrl('preview') . '?id=' . $post->getId(); if ($request->getPost('edit')) { $this->_redirect($this->getUrl('edit') . '?id=' . $post->getId()); } else if ($request->getPost('publish')) { $post->sendLive(); $post->save(); $this->messenger->addMessage('Post sent live'); } else if ($request->getPost('unpublish')) { $post->sendBackToDraft(); $post->save(); $this->messenger->addMessage('Post unpublished'); } else if ($request->getPost('delete')) { CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM258 9063Ch07CMP2 11/13/07 8:06 PM Page 258 $post->delete(); // Preview page no longer exists for this page so go back to index $url = $this->getUrl(); $this->messenger->addMessage('Post deleted'); } $this->_redirect($url); } // ... other code } ?> Outputting FlashMessenger Messages on the Web Site Finally, we must output any existing messages to the template. In order for messages to be dis- played regardless of where the user is in the site (in other words, so we can use it in other areas aside from managing blog posts), we add the display code to the footer.tpl template, because we will be displaying messages in the right column. Just like the error containers we created for form errors, we will reuse this message con- tainer for similar messages we generate from Ajax requests. To achieve this, we check how many messages there are available to be written. If there are none, we apply the display: none style so the message container does not appear. Later, when we add Ajax functionality, we can simply unhide this element as required. If there is more than one message, we will use an unordered list (
            ) to output the messages. Listing 7-31 shows the changes we will make to footer.tpl (in the ./templates directory), which checks the $messages array for any messages to output. Note that if there’s only one message, the $messages array contains only one element, so we use $messages.0 in Smarty to access this array element. Listing 7-31. Outputting Status Messages to the Template (footer.tpl)
          {if $messages|@count > 0}
          {if $messages|@count == 1} Status Message: {$messages.0|escape} {else} CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 259 9063Ch07CMP2 11/13/07 8:06 PM Page 259 Status Messages:
            {foreach from=$messages item=row}
          • {$row|escape}
          • {/foreach}
          {/if}
          {else} {/if}
          ■Note I have chosen to display status messages in the right column of the web site. You may prefer to use a different location, such as between the breadcrumbs and page title in the main area of the page. You may also want to add a close button to the #messages div to allow the user to hide the status message window immediately. To do so, you would use Event.observe() on that close button, which would call $('messages').hide(). When we reuse this status box later in this chapter for Ajax notifications, we will set the box to auto-hide after a short delay. This is all that is required to get the flash messenger working; however, it doesn’t stand out for users very well. In order to make it stand out more, we will use the Scriptaculous Highlight effect. To apply this effect (with the default colors and time delay), the only code we have to use is as follows: In an effort to keep the page markup as clean as possible (and also to ensure that this code doesn’t run until the Scriptaculous files have all loaded), we will make this effect run once the page has loaded. To do so, we will create a new file called scripts.js, which we will store in ./htdocs/js. This file will contain any custom JavaScript we will use globally in our application (that is, on all pages). For now, though, all we are going to do is create a function that runs once the page has loaded. This is the equivalent of writing HTML like , but we are going to do it the “Web 2.0 way” using Prototype (that is, observing the window.onload event properly and not cluttering up the page HTML). CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM260 9063Ch07CMP2 11/13/07 8:06 PM Page 260 Listing 7-32 shows the contents of the scripts.js file, which begins by creating a hash called settings that we can use to hold any required settings (making the JavaScript code more maintainable). For starters, we define the ID of the element that holds messages. Next, we define a function that will run on page load, which currently finds the #messages element and applies the Effect.Highlight class to it. Finally, we observe the onload event. The Event.observe() call follows the function definition of init(), because the function would be undefined at run time if it were the other way around. Note that we first check that #messages is visible using the Prototype function isVisible(), as we still include the element on the page (as a hidden element) even if there are no messages. Internally, Effect.Highlight actually checks this for you, but it’s still good to be explicit in your own code as to how you want it to function. Listing 7-32. Highlighting the Messages Div after the Page Has Loaded (scripts.js) var settings = { messages : 'messages' }; function init(e) { // check if the messages element exists and is visible, // and if so, apply the highlight effect to it var messages = $(settings.messages); if (messages && messages.visible()) { new Effect.Highlight(messages); } } Event.observe(window, 'load', init); ■Note Because Effect.Highlight is a class and not a function, you must remember to use the new keyword when applying the effect. Otherwise a JavaScript error will occur. This applies to other effects in Scriptaculous too. Finally, we must make the scripts.js file load from header.tpl. This file must be included after the inclusion of both Prototype and Scriptaculous. Listing 7-33 shows the updated version of header.tpl that loads scripts.js. Listing 7-33. Loading scripts.js in the Web Site Header (header.tpl) CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 261 9063Ch07CMP2 11/13/07 8:06 PM Page 261 Figure 7-3 shows how status messages are displayed on a typical page in our web applica- tion. This message is a result of clicking the Publish Post button on the preview page for a post. Figure 7-3. Displaying the status message after a blog post has been sent live Summary In this chapter we began the implementation of the blogging functionality of our Web 2.0 application. Specifically, we added the ability to create, edit, and delete posts. We also imple- mented a simple publishing system that allows users to preview a blog post before they publish it. The blog posts aren’t actually published anywhere yet—we will do this in Chapter 9. The key concepts we covered in this chapter include the following: •Extending the permissions system as required. •Cross-site scripting (XSS) and cross-site request forgery (CSRF) attacks and how they can occur. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM262 9063Ch07CMP2 11/13/07 8:06 PM Page 262 •Preventing such attacks by correctly filtering user-submitted data. We achieved this by defining a white list of allowed HTML tags and attributes, and stripping out everything else. •Implementing a simple notification system using the Zend_Controller flash messenger and Scriptaculous, so users know what (if any) action has been performed. In Chapter 8 we will continue to build on the blogging system by adding greater functionality to the blog manager. This will include an Ajax-powered blog post listing to help users manage their blogs, as well a WYSIWYG editor so users can format their posts more easily. CHAPTER 7 ■ BUILDING THE BLOGGING SYSTEM 263 9063Ch07CMP2 11/13/07 8:06 PM Page 263 9063Ch07CMP2 11/13/07 8:06 PM Page 264 Extending the Blog Manager In Chapter 7 we began implementing the blogging functionality in our web application, which included giving users the ability to add, edit, and delete posts, as well as allowing them to preview posts prior to sending them live. In this chapter, we will continue to implement these blog management tools, building on what we started in the previous chapter. The features we will implement include the following: • Retrieving multiple posts. So far in the blog manager we load only one blog post data- base record at a time. We will look at how to effectively retrieve large amounts of data from the database in a single operation. • Displaying existing blog posts. Using the functions we create to retrieve multiple blog posts, we will create an index page used to list a user’s posts so they can preview or edit them as required. We will make this post index Ajax-powered to help users quickly access their previous posts. • Integrating a WYSIWYG editor. We will implement FCKeditor, an open source What You See Is What You Get (WYSIWYG) editor. This will allow users to easily format their blog posts with HTML using the provided toolbar. Once you have completed this chapter, the blog management tools will be in a sufficient state to allow users to quickly and easily post new entries to their blogs. This will allow users to move on (in Chapter 9) to publishing their blog so other users can view it. Listing Blog Posts on the Blog Manager Index Currently users are able to create new blog posts using the tools created in Chapter 7, but there is no way to return to existing posts to edit them. For users to easily manage their blog posts, we will now add a list of all their posts on the blog manager index page (http://phpweb20/blogmanager). We’ll display their posts so all of their posts from the current month are displayed at the top of the page (with a short teaser summary of each post), with a monthly summary to the side of this list, in the left column. The user will be able to click a month to reload the page with the selected month’s posts showing. Once we have completed this functionality, we will improve this code to use Ajax to fetch a list of the selected month’s post, meaning the page will not have to be reloaded. Initially we are creating the “non-Ajax” version of the blog manager index, which is provided for accessi- bility and for browsers that don’t support JavaScript. 265 CHAPTER 8 9063Ch08CMP2 11/11/07 12:35 PM Page 265 The process for achieving this is as follows: 1. Retrieving the posts for the specified month (this will default to the current month) 2. Retrieving a list of the other months that contain posts, as well as the number of posts belonging to that month 3. Outputting the selected month’s posts with a brief summary of the post 4. Outputting the summary of months, linking back to the indexAction() method to list those posts Fetching Blog Posts from the Database Before we can do anything else, we must allow multiple records to be accessed at one time in the DatabaseObject_BlogPost class. So far, we have only ever loaded one record at a time; how- ever, now we want to load multiple records. To do this, we’ll write four separate static methods that we can use in this chapter, as well as in other parts of the application (when displaying blog posts in other areas of the site): • GetPosts(): This method will retrieve an array of posts based on the options passed in. This includes the ability to set the offset and limit of the returned results for multipaged data. It will return an array of DatabaseObject_BlogPost objects. • GetPostsCount(): This method will return the total number of posts that match the passed-in criteria. Since GetPosts() will be able to return multipaged data (in that we can specify the offset and limit), we will need to know the total number of posts so the number of pages can be determined. • GetMonthlySummary(): Similar to GetPostsCount(), this method is used to return the number of posts found for each month in the specified date range. If no date range is specified, then all months with posts will be included. • _GetBaseQuery(): This private method will be used by each of the previous functions to build a query for the specified options. This is purely used to prevent code duplication. For instance, if you wanted to add a new option to how posts are retrieved in GetPosts(), you would want this same functionality in GetPostsCount() so an accurate count is returned. Creating the _GetBaseQuery() Method Since GetPosts(), GetPostsCount(), and GetMonthlySummary() will all rely on _GetBaseQuery(), I’ll cover the _GetBaseQuery() method first. We’ll use the Zend_Db_Select class that comes with the Zend_Db component of the Zend Framework. This class is used to build SQL select queries. It provides methods to easily add the various parts that make up such a query. For exam- ple, the where() method is called to add a where clause. For more information on this class, you can read the Zend Framework manual entry at http://framework.zend.com/manual/en/ zend.db.select.html. To instantiate Zend_Db_Select, you can use new Zend_Db_Select($db) (where $db is the database connection), or you can call $db->select() to retrieve a new instance. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER266 9063Ch08CMP2 11/11/07 12:35 PM Page 266 Listing 8-1 shows the complete _GetBaseQuery() function as it fits into the DatabaseObject_ BlogPost class. Listing 8-1. The _GetBaseQuery() Function, Used to Build a SQL Select Statement (BlogPost.php) array(), 'from' => '', 'to' => '' ); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } // create a query that selects from the blog_posts table $select = $db->select(); $select->from(array('p' => 'blog_posts'), array()); // filter the records based on the start and finish dates if (strlen($options['from']) > 0) { $ts = strtotime($options['from']); $select->where('p.ts_created >= ?', date('Y-m-d H:i:s', $ts)); } if (strlen($options['to']) > 0) { $ts = strtotime($options['to']); $select->where('p.ts_created <= ?', date('Y-m-d H:i:s', $ts)); } // filter results on specified user ids (if any) if (count($options['user_id']) > 0) $select->where('p.user_id in (?)', $options['user_id']); return $select; } } ?> CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 267 9063Ch08CMP2 11/11/07 12:35 PM Page 267 The first thing this method does is define an array of default options. At this stage, the only options are the user_id parameter (to specify which user to filter returned data on) and the from and to parameters, which define the date range of returned posts. Next we loop over these defaults to initialize all of these values in the $options array, so we know they will exist when we try to access them in the remainder of the method. The next step is to instantiate Zend_Db_Select by calling $db->select(). Typically the first thing you should do with your new instance of Zend_Db_Select is to define the tables to select from and to define which fields to select from those tables. Each table to select from can be specified using the from() method (you must call it once for each table). The first argument to from() is the name of the table. You can use either a string or an array for this argument. Using an array allows you to give an alias to the table name for later use in the query. This array consists of one element: the value is the name of the table, while its key is the table alias. We will be using the blog_posts table, which we will give an alias of p. Hence, the first argument to from() is array('p' => 'blog_posts'). The second argument to from() is the fields you want to select. You can select one field by specifying a single string, or you can select multiple fields by using an array (where each ele- ment corresponds to a column from the table). In our case, we use an empty array since _GetBaseQuery() is used only to build the base options for the query. In the other methods that call _GetBaseQuery(), we will specify which fields to select. Next we check for the presence of the from and to parameters, which are used to filter the posts by the date and time stored in the ts_created column of the blog_posts table. If the options are empty, we ignore them, but if they have been specified, we add where clauses to restrict the dates based on these timestamps. The next thing we to do is add where clauses to filter the results on the user_id column. The Zend_Db class takes care of quoting the values. This is extremely important since this helps to prevent malicious users who try to attack your applications using SQL injection. When calling the where() method, the first argument is the where clause, which can include a question mark to indicate a placeholder for a value that should be substituted into the clause. The second (optional) argument is the value that should be substituted in for the question mark. When Zend_Db quotes values, it checks their types so values are included correctly. For example, if an array is specified (as in our case), each value is quoted accordingly (and then joined by a comma). This allows us to pass in multiple values in the $options['user_id'] value, meaning we can filter on multiple users at once if we want to do so. ■Note Using an array as the value is designed for using in rather than an equals sign. So if $options['user_id'] were defined as array(1, 2, 3, 4), the generated SQL would read user_id in (1, 2, 3, 4). Finally, we return the Zend_Db_Select object. This allows GetPosts() and GetPostsCount() to make further additions to the query if required (which they will, as you will now see). Note that the query generated thus far is unusable since we haven’t specified any fields to select (as noted earlier in the discussion of the from() method). CHAPTER 8 ■ EXTENDING THE BLOG MANAGER268 9063Ch08CMP2 11/11/07 12:35 PM Page 268 Creating the GetPostsCount() Function Next we define the GetPostsCount() method, which returns the total number of results that would be returned for the passed-in criteria (that is, the number of rows that GetPosts() would return if no limit were specified). Just like in _GetBaseQuery(), we accept an array called $options that holds the required options for the database query. There are no options specific to GetPostsCount(), so it simply passes on the array to _GetBaseQuery(). Listing 8-2 shows the GetPostsCount() method, which belongs in the BlogPost.php file in the ./include/DatabaseObject directory. Since we already specified the table to select from in _GetBaseQuery(), we pass null as the argument to from() and include only which column to fetch—in this case count(*), since we are counting the number of rows. It then uses the fetchOne() function to return the first element of the first returned row, which in this case will be the total number of rows found. Listing 8-2. The GetPostsCount() Method,Which Determines the Total Number of Rows That Would Be Returned (BlogPost.php) from(null, 'count(*)'); return $db->fetchOne($select); } private static function _GetBaseQuery($db, $options) { // ... other code } } ?> Creating the GetPosts() Function Now that we have a good idea of how _GetBaseQuery() and GetPostsCount() work, we can write the GetPosts() function. The idea in this function is to build the query with _GetBaseQuery() and then add the required fields to select. The other important task we do here and not in _GetBaseQuery() is to set the offset, limit, and ordering options. Since these options don’t apply to GetPostsCount(), they must be done here instead of in _GetBaseQuery(). I have split this function up into three parts so we can easily dissect it. Listing 8-3 shows the first part of the function. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 269 9063Ch08CMP2 11/11/07 12:35 PM Page 269 Listing 8-3. The First Third of the GetPosts() Function (BlogPost.php) 0, 'limit' => 0, 'order' => 'p.ts_created' ); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } $select = self::_GetBaseQuery($db, $options); // set the fields to select $select->from(null, 'p.*'); // set the offset, limit, and ordering of results if ($options['limit'] > 0) $select->limit($options['limit'], $options['offset']); $select->order($options['order']); ■Note Using Zend_Db_Select helps make queries that work on different database servers. For example, MySQL uses LIMIT x, y or LIMIT y OFFSET x to limit the returned results, while PostgreSQL uses OFFSET x LIMIT y (where x is the offset and y is the limit). The next step is to perform the database query and build an array of DatabaseObject_ BlogPost objects that we can return. We use the $db->fetchAll() method to retrieve all the database data and write it to an array. Since a single instance of the DatabaseObject subclass (such as DatabaseObject_BlogPost) corresponds to a single database record, we need multiple instances of this class: one for each row returned from the SQL we have just created. To help us create this array, we use the static BuildMultiple() helper method of DatabaseObject. We pass the name of the class (DatabaseObject_BlogPost, which we can use __CLASS__ to dynamically generate) to this method as well as the data we’re using to build the array of objects. Listing 8-4 shows this process. The key of each element in the array corre- sponds to its post_id value. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER270 9063Ch08CMP2 11/11/07 12:35 PM Page 270 Listing 8-4. Creating an Array of DatabaseObject_BlogPost Objects (BlogPost.php) // fetch post data from database $data = $db->fetchAll($select); // turn data into array of DatabaseObject_BlogPost objects $posts = self::BuildMultiple($db, __CLASS__, $data); $post_ids = array_keys($posts); if (count($post_ids) == 0) return array(); The process of creating the database data for GetPosts() is nearly done; however, when we have previously used an instance of DatabaseObject_BlogPost, the object has also had a Profile_BlogPost object attached to it as the $profile property. Since all of the important data of the post (such as the title and content) is stored in the profile, we must load the profile for each of the blog posts. When a record is normally loaded with DatabaseObject, the postLoad() method is auto- matically called. Since this would result in an SQL query for every row (in order to load the profile), we need a more efficient solution. Instead, we are going to use a method that is included with my Profile class that is used to create multiple Profile instances. Doing this means only a single SQL statement is executed internally, rather than one for each blog post for which we’re loading the profile. We use the BuildMultiple() method of the Profile class to retrieve an array of Profile_ BlogPost objects. The first argument is the database connection, the second is the Profile subclass to use, while the third argument consists of the IDs of the posts to load. This is effec- tively the same as calling $profile = new Profile_BlogPost($db, $post_id) once for every blog post. Finally, we must match up each Profile_BlogPost object with the corresponding DatabaseObject_BlogPost object. In both the $profiles and $posts arrays, the key corre- sponds to the post_id of the element. To assign each profile to its corresponding blog post, we loop over each post and look for a matching profile record. If one isn’t found, we simply make sure we call setPostId() so we can write to the profile if required (the profile property is set to be an instance of Profile_ BlogPost in the DatabaseObject_BlogPost constructor). Listing 8-5 shows the conclusion of the GetPosts() method. Once again, this code belongs in the BlogPost.php file in ./include/DatabaseObject. Listing 8-5. Loading the Profile Data for Each Blog Post (BlogPost.php) // load the profile data for loaded posts $profiles = Profile::BuildMultiple( $db, 'Profile_BlogPost', array('post_id' => $post_ids) ); foreach ($posts as $post_id => $post) { CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 271 9063Ch08CMP2 11/11/07 12:35 PM Page 271 if (array_key_exists($post_id, $profiles) && $profiles[$post_id] instanceof Profile_BlogPost) { $posts[$post_id]->profile = $profiles[$post_id]; } else { $posts[$post_id]->profile->setPostId($post_id); } } return $posts; } // ... other code } ?> Retrieving a Monthly Summary of Posts As an extra utility function, we will now implement a function called GetMonthlySummary(), which returns a summary of the number of posts in each month. Once again we use the $options array that we pass on to _GetBaseQuery(), allowing us to easily extend the capabili- ties of the function in the future. This function is slightly different from GetPosts() and GetPostsCount(), since we will be grouping the results by the year and month of each post. Listing 8-6 shows the code for GetMonthlySummary(), which builds the query once again using Zend_Db_Select and then calls fetchPairs() to create an array that uses the first column (the year and month) as the key and the second column (the number of posts) as the value. Listing 8-6. Building the SQL Query and Fetching the Post Data (BlogPost.php) 0, 'limit' => 0, 'order' => $dateString . ' desc' CHAPTER 8 ■ EXTENDING THE BLOG MANAGER272 9063Ch08CMP2 11/11/07 12:35 PM Page 272 ); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } $select = self::_GetBaseQuery($db, $options); $select->from(null, array($dateString . ' as month', 'count(*) as num_posts')); $select->group($dateString); $select->order($options['order']); return $db->fetchPairs($select); } // ... other code } ?> After calling _GetBaseQuery(), we add the fields we require to the statement. To execute the query, we call fetchPairs(). This method returns an array of the rows returned from the SQL query. It uses the first selected column as the array key and the second selected column as the array element. In this code, we use the timestamp as the array key and the number of posts for that month as the array value. The format string we pass to MySQL’s date_format() function will generate a timestamp in the format of YYYY-MM, so in the case of November 2007, the returned month column would have the value 2007-11. ■Note The date_format() function is specific to MySQL and will not work in other database servers. Other servers such as PostgreSQL use the to_char() function instead, which is why we check the type of database adapter being used in Listing 8-6. Next we must group the data by the year/month value, before setting the ordering options. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 273 9063Ch08CMP2 11/11/07 12:35 PM Page 273 ■Note In MySQL, if you give a column alias to a function call (as we did by using date_format() as month), you can then refer directly to the month pseudocolumn in other parts of the statement (in this case in group by and order by). In other database servers, this syntax is not allowed—the function call must be used explicitly in each required place. This is the reason we assigned the function call to the variable in the PHP code ($dateString). Assigning Recent Posts and the Monthly Summary to the Template Since we have just written code to retrieve posts from the database, we can now fetch all the data we need to display on the blog manager index. We are going to retrieve two different items: • The posts for the selected month using the GetPosts() function (using the current month as the default). • The total number of posts by the logged-in user using the GetPostsCount() function. To display data that we retrieve from GetMonthlySummary(), we are going to create a Smarty plug-in, which we will look at shortly. Listing 8-7 shows the code we add to the indexAction() method in the BlogmanagerController.php file in order to fetch a summary of the blog posts for the current user. Listing 8-7. Calling the New Post Retrieval Functions from indexAction() (BlogManagerController.php) getRequest()->getQuery('month'); if (preg_match('/^(\d{4})-(\d{2})$/', $month, $matches)) { $y = $matches[1]; $m = max(1, min(12, $matches[2])); } else { $y = date('Y'); // current year $m = date('n'); // current month } $from = mktime(0, 0, 0, $m, 1, $y); CHAPTER 8 ■ EXTENDING THE BLOG MANAGER274 9063Ch08CMP2 11/11/07 12:35 PM Page 274 $to = mktime(0, 0, 0, $m + 1, 1, $y) - 1; $options = array( 'user_id' => $this->identity->user_id, 'from' => date('Y-m-d H:i:s', $from), 'to' => date('Y-m-d H:i:s', $to), 'order' => 'p.ts_created desc' ); $recentPosts = DatabaseObject_BlogPost::GetPosts($this->db, $options); // get the total number of posts for this user $totalPosts = DatabaseObject_BlogPost::GetPostsCount( $this->db, array('user_id' => $this->identity->user_id) ); $this->view->month = $from; $this->view->recentPosts = $recentPosts; $this->view->totalPosts = $totalPosts; } // ... other code } ?> The first thing we do in this action is initialize the selected month and year. By default, the current month and year is selected, but if a valid string (in the form of YYYY-MM) is specified, then the month and year in that string are used instead. Once we have the month and year, we can define the from and to parameters for GetPosts(). We use mktime() to generate the start of the month, and then we find the start of the next month and subtract 1 to find the last second in the selected month. Another way would be to use mktime(23, 59, 59, date('n', $from), date('t', $from), date('Y', $from), since the t parameter returns the number of days in a given month. I think the first way is simpler. In each of the $options arrays defined in this code, the key parameter is the user_id parameter. If this isn’t specified, then posts will be returned for all users, not just the current user. The final step in this method is to assign the returned data to the template. ■Note You could assign these values directly to the template (that is, $this->view->recentPosts = DatabaseObject_BlogPost::GetPosts(…)), but I prefer to group all the template assignments together at the end of the method so I can quickly see exactly which data will be available in the template just by looking at the end of the method. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 275 9063Ch08CMP2 11/11/07 12:35 PM Page 275 Displaying Recent Posts in the Template We now have all of the recent posts (if any) assigned to template, as well as a timestamp of the month they are from. We can now write a template to output these posts. Rather than outputting them directly to the ./templates/blogmanager/index.tpl template (that is, the template for the indexAction() method of BlogmanagerController), we’ll create a helper template to output the necessary HTML. We will then include this template from index.tpl. The reason we do this is so we can easily add some Ajax functionality to this page, which we will be doing later in the “Ajaxing the Blog Monthly Summary” section. By creating a separate template, we can generate HTML in the background HTTP request (which uses XMLHttpRequest) and directly display the output. Let’s forget about the Ajax part for now, though; we will add that functionality later in this chapter. As I mentioned, I like to store helper templates in a directory called lib, which then sepa- rates them from the main controller action templates. Listing 8-8 shows the contents of the month-preview.tpl template, which we store in the ./templates/blogmanager/lib directory. Since this template is specific to the blog manager, I have created a separate lib directory in ./templates/blogmanager rather than using the “global” lib directory. Listing 8-8. A Basic Template to Output All the Posts for a Single Month (month-preview.tpl)

          {$month|date_format:'%B %Y'}

          {if $posts|@count == 0}

          No posts found for this month.

          {else}
          {foreach from=$posts item=post}
          {$post->ts_created|date_format:'%a, %e %b'}: {$post->profile->title|escape} {if !$post->isLive()} not published {/if}
          {$post->getTeaser(100)|escape}
          {/foreach}
          {/if} CHAPTER 8 ■ EXTENDING THE BLOG MANAGER276 9063Ch08CMP2 11/11/07 12:35 PM Page 276 This template is fairly straightforward in that it assumes a timestamp called $month is assigned, as well as an array of DatabaseObject_BlogPost objects called $posts. The template loops over each post and outputs it inside a definition list (
          ). The
          HTML tag serves our needs well, because we want to output the date and title of the blog (using the definition title tag
          ), followed by a brief summary of the content (using the definition description tag
          ). To include a short summary (also known as a teaser) of the blog post, we call the getTeaser() method from the DatabaseObject_BlogPost class. Listing 8-9 shows the code for this method, which we add to the BlogPost.php file in ./include/DatabaseObject. To ensure the preview of the content fits on a single line, we apply the PHP strip_tags() function as a modifier. Additionally, we use the Smarty truncate modifier to restrict the total length to 100 characters. Listing 8-9. Generating a One-Line Summary of a Blog Post (BlogPost.php) profile->content), $length); } // ... other code } ?> To use the month-preview.tpl template created in Listing 8-8, we must now include it (using Smarty’s {include} function) in the index.tpl template from the ./templates/blogmanager directory. Listing 8-10 shows the changes to this template (which we started in Chapter 7). Listing 8-10. Displaying a Summary of the User’s Blog and Outputting the Assigned Posts (index.tpl) {include file='header.tpl' section='blogmanager'} {if $totalPosts == 1}

          There is currently 1 post in your blog.

          CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 277 9063Ch08CMP2 11/11/07 12:35 PM Page 277 {else}

          There are currently {$totalPosts} posts in your blog.

          {/if}
          {include file='blogmanager/lib/month-preview.tpl' month=$month posts=$recentPosts}
          {include file='footer.tpl'} At the start of this template we include some basic introductory text that uses the $totalPosts variable. Note that we change the language depending on the number of posts. This is simple to do, yet if you look closely at many computer or web applications, developers often seem to miss this (have you ever noticed text along the lines of “1 blog posts found”?). The only thing to do now is to add a few extra styles to tidy up this output. We will make the date and title appear in bold, as well as making the status text for unpublished posts a bit smaller and not bold. Listing 8-11 shows these styles, which should be added to the styles.css file (in ./htdocs/css). Listing 8-11. Styling the Blog Post Summary (styles.css) #month-preview .status { font-weight : normal; font-size : 0.9em; } #month-preview dt { font-weight : bold; } If you now visit http://phpweb20/blogmanager after logging in to the web application, you should see a display similar to Figure 8-1. The posts for the current month are now being dis- played, although there’s no way to navigate to past months. We will add this to the template in the next section. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER278 9063Ch08CMP2 11/11/07 12:35 PM Page 278 Figure 8-1. Displaying a summary of posts from the current month Displaying the Monthly Summary Now that we are displaying a summary of posts from the current month, we need a way to dis- play posts from the other months. In Listing 8-6 we created the GetMonthlySummary() method, which gives us an array of months and the number of posts belonging to that month. We will now create a Smarty plug-in to retrieve this data and assign it to the template. We could have generated this data in the indexAction() method and then assigned it directly; however, the problem with this occurs when we want to show the same data on another page. We would have to retrieve and assign the data on every page on which we wanted to display it. This means if we decided to change the layout of the pages, we would need to make changes to the PHP code, not just the templates. Using a Smarty plug-in allows us to get the data when- ever we like. To bring the data from GetMonthlySummary(), we are going to use Smarty code as follows: {get_monthly_blog_summary user_id=$identity->user_id assign=summary} Effectively what this code means is that we are going to create a custom Smarty function called get_monthly_blog_summary. This function will take two arguments: the ID of the user the summary is being fetched for and the name of the template variable to assign the summary to (meaning we will be able to access the $summary variable in the template after this function has been called). CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 279 9063Ch08CMP2 11/11/07 12:35 PM Page 279 ■Note The reason we pass in the user ID instead of automatically retrieving it within the plug-in is that by doing it this way we can use this plug-in when displaying users’ public home pages. Since the ID in that case is dependent on the page being looked at and not which user is viewing the page, we specify the ID using the function argument. Listing 8-12 shows the code for this plug-in. We save this code to a file called function.get_ monthly_blog_summary.php, which we store in the ./include/Templater/plugins directory. Listing 8-12. A Custom Smarty Plug-in to Retrieve the Blog Summary (function.get_monthly_blog_summary.php) 0) $smarty->assign($params['assign'], $summary); } ?> The first thing this plug-in does is to check for the user_id parameter. If it is set, it adds it to the $options array. We must fetch the $db object from the application registry because it is required to make the call to GetMonthlySummary(). Finally, we determine the variable name to use for assigning the data back to the template. As you saw earlier, we’ll use a variable called $summary. After calling get_monthly_blog_summary, we can simply loop over the $summary array in the template as we would with any other array. ■Note You could argue that this technique is using application logic within a template, which as discussed in Chapter 2 is a bad thing. To some degree this is application logic, although technically speaking we are doing it only for the purpose of the view—we are not causing any application side effects. Additionally, sometimes you need to make minor sacrifices in the way code is structured in order to provide flexibility. Calling the Smarty Plug-in in the Side Columns We are now going to use the plug-in we just created to output the monthly summary in the left column of the site template. By using the plug-in, we have made it very easy to include this CHAPTER 8 ■ EXTENDING THE BLOG MANAGER280 9063Ch08CMP2 11/11/07 12:35 PM Page 280 data on other pages also. The one problem we now run into is that to add content to either of the side columns, we must alter the footer.tpl template. Since we don’t want to include this data site-wide, we must make some enhancements to our template structure to allow us to include these additions to the left column only when required. To do this, we’ll pass two optional parameters when we include the footer.tpl template. The first parameter will specify a template to use to generate content for the left column, while the second parameter will specify a template for generating content in the right column. First, let’s create the template that calls the get_monthly_blog_summary plug-in and out- puts its data. This is the template we will pass to footer.tpl to output. Listing 8-13 shows the left-column.tpl template, which we store in the ./templates/blogmanager/lib directory. Note that we use the class name .box, because this is the class we defined earlier for styling content areas in the side columns. Listing 8-13. Outputting the Data from the get_monthly_blog_summary Plug-in (left-column.tpl) {get_monthly_blog_summary user_id=$identity->user_id assign=summary} {if $summary|@count > 0}

          Your Blog Archive

          {/if} Second, we must modify the index.tpl template (from ./templates/blogmanager) to tell footer.tpl to use this template. Listing 8-14 shows the change we make to the bottom {include} call. Listing 8-14. Specifying the Template to Use in the Left Column of the Site (index.tpl) {include file='header.tpl' section='blogmanager'} {if $totalPosts == 1}

          There is currently 1 post in your blog.

          {else}

          CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 281 9063Ch08CMP2 11/11/07 12:35 PM Page 281 There are currently {$totalPosts} posts in your blog.

          {/if}
          {include file='blogmanager/lib/month-preview.tpl' month=$month posts=$recentPosts}
          {include file='footer.tpl' leftcolumn='blogmanager/lib/left-column.tpl'} You should also make the same change to the edit.tpl and preview.tpl templates from the blog manager controller. The final change is to make footer.tpl recognize the $leftcolumn and $rightcolumn parameters and include the templates accordingly. Listing 8-15 shows the new version of footer.tpl, which now includes the left and right templates if required. Note that for the left column we can use the else block to display some default content. I haven’t worried about this for the right column, since there is always authentication data shown (whether logged in or not). Listing 8-15. Including the Template to Generate Left and Right Column Content (footer.tpl)
          {if isset($leftcolumn) && $leftcolumn|strlen > 0} {include file=$leftcolumn} {else}
          Left column placeholder
          {/if}
          CHAPTER 8 ■ EXTENDING THE BLOG MANAGER282 9063Ch08CMP2 11/11/07 12:35 PM Page 282 {if isset($rightcolumn) && $rightcolumn|strlen > 0} {include file=$rightcolumn} {/if}
          Including Additional Data in the Side Column Sometimes In certain instances you will want different combinations of data included in the side columns. For example, you might want to show the blog summary and the authentication data in the same column—but only on a particular page. To achieve this, you would make a new template that outputs this data accordingly and then pass this new template in as the value to $leftcolumn or $rightcolumn. The recommended way to do this is to not include multiple content boxes in a single tem- plate but to keep them all in separate templates and then to create an additional wrapper template to bring them together. For example, you might store the monthly blog summary in blog-summary-box.tpl, and you might keep authentication data in authentication-box.tpl. You would then create another template called some-template.tpl that might look as follows: {include file='blog-summary-box.tpl'} {include file='authentication-box.tpl'} You would then use some-template.tpl as the value for $leftcolumn. To keep the code rel- atively simple, I have chosen not to break up the templates to this degree. Ajaxing the Blog Monthly Summary In the previous section, we wrote code to output blog posts in the blog manager for the selected month, with a list of all months that have posts in the side column. The way it works now is that if a month is clicked by the user, the page reloads, displaying the posts from that month. We’ll now enhance this system. Instead of reloading the page for the newly selected month, we’ll make the blog manager index page fetch the posts in the background using Ajax and then display them on the page. This code will still be accessible for non-JavaScript users, because the solution we have already implemented does not rely on JavaScript. This new functionality will be built on top of the existing functionality, meaning those who use it will have an improved experience but those who don’t will not suffer. The only other consideration we must make is that we’re also listing the monthly sum- mary on the edit and preview pages. If one of the months is clicked from these pages, we will not use Ajax to fetch the new page content but instead navigate normally to the page as we would without this Ajax functionality. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 283 9063Ch08CMP2 11/11/07 12:35 PM Page 283 Creating the Ajax Request Output Before we add any JavaScript code, we will create the necessary changes to generate the Ajax request data. We can reuse the indexAction() method from BlogmanagerController.php with- out any changes to code. All we need to do is to change its corresponding template so the page header and footer aren’t included when the controller action is requested via Ajax. To help with this, we’ll make a minor addition to the CustomControllerAction class. In Chapter 6 we discussed how the isXmlHttpRequest() method worked with the Zend_ Controller_Request_Http class. This method is a simple way to determine whether the current request was initiated using XMLHttpRequest. We’ll assign the value of this function call to all templates. Listing 8-16 shows the changes we make to the CustomControllerAction.php file in the ./include directory. Listing 8-16. Adding Ajax Request Detection to Templates (CustomControllerAction.php) view->isXmlHttpRequest = $this->getRequest()->isXmlHttpRequest(); } // ... other code } ?> Next we modify the template for the BlogmanagerController’s indexAction() method. All we do in this template now is check the value of the $isXmlHttpRequest variable that is auto- matically assigned. If this value is false, then the template will generate output as previously, whereas if it’s true, then we won’t include the page header and footer. Listing 8-17 shows the changes we make to the index.tpl file in ./templates/blogmanager. Listing 8-17. Altering the Output for Ajax Requests (index.tpl) {if $isXmlHttpRequest} {include file='blogmanager/lib/month-preview.tpl' month=$month posts=$recentPosts} {else} {include file='header.tpl' section='blogmanager'} {if $totalPosts == 1}

          CHAPTER 8 ■ EXTENDING THE BLOG MANAGER284 9063Ch08CMP2 11/11/07 12:35 PM Page 284 There is currently 1 post in your blog.

          {else}

          There are currently {$totalPosts} posts in your blog.

          {/if}
          {include file='blogmanager/lib/month-preview.tpl' month=$month posts=$recentPosts}
          {include file='footer.tpl' leftcolumn='blogmanager/lib/left-column.tpl'} {/if} The BlogMonthlySummary JavaScript Class To initiate the background HTTP request to fetch the monthly summary data (using XMLHttpRequest), we need to attach some JavaScript code to each of the links in the month listing. To do this, we’ll create a JavaScript class called BlogMonthlySummary. This class will be loaded and instantiated automatically when we include the left- column.tpl template we created earlier this chapter, as you will see shortly. Using some of the Prototype techniques you learned in Chapter 5, we can create a class to encapsulate all the functionality we need. The general algorithm for this class is as follows: 1. Check for the existence of the link container (where the month links are listed) and the content container (where the blog posts are listed). If either one doesn’t exist, stop exe- cution (meaning clicking the month links will just load the respective page as normal). 2. Observe the click event for each of the links found in the link container. 3. When a link is clicked, initiate an Ajax request using the Ajax.Updater class. This class is built on top of the Ajax.Request class and is used specifically to update an element with the results from XMLHttpRequest. 4. Cancel the click event so the browser doesn’t follow the link href. We use the Event.stop() method in the event handler to achieve this. Listing 8-18 shows the contents of the BlogMonthlySummary.class.js file, which we store in the ./htdocs/js directory. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 285 9063Ch08CMP2 11/11/07 12:35 PM Page 285 Listing 8-18. The BlogMonthlySummary JavaScript Class (BlogMonthlySummary.class.js) BlogMonthlySummary = Class.create(); BlogMonthlySummary.prototype = { container : null, linkContainer : null, initialize : function(container, linkContainer) { this.container = $(container); this.linkContainer = $(linkContainer); if (!this.container || !this.linkContainer) return; this.linkContainer.getElementsBySelector('a').each(function(link) { link.observe('click', this.onLinkClick.bindAsEventListener(this)); }.bind(this)); }, onLinkClick : function(e) { var link = Event.element(e); var options = { }; new Ajax.Updater(this.container, link.href, options); Event.stop(e); } }; After creating the class using Prototype’s Class.create() function, we define the con- structor for the class (the initialize() method), which accepts the content container as the first argument and the link container as the second argument. If both of these containers are found to exist, the code continues to add the click event handler to each of the links. This results in the onLinkClick() method being called if any of the links are clicked. ■Note Chapter 6 discusses the Prototype event handling mechanism. You’ll also see how the bind() and bindAsEventListener() functions work in that chapter. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER286 9063Ch08CMP2 11/11/07 12:35 PM Page 286 We begin the onLinkClick() method by determining exactly which link was clicked. This is achieved by calling the Event.element() function with the event object passed to onLinkClick(). We will use the href attribute of the link as the URL to pass to Ajax.Updater. Currently there are no extra options we need to pass to this Ajax request; however, we still define the options hash since we will be using it later in this chapter. The onLinkClick() method concludes by calling Event.stop(). This is to ensure the browser doesn’t follow the link, thereby defeating the point of using Ajax. Installing the BlogMonthlySummary Class Now we must update the left-column.tpl template to load and instantiate the BlogMonthlySummary JavaScript class. Listing 8-19 shows the updated version of left-column.tpl, which now loads and instan- tiates this JavaScript class. Once you reload your page, clicking these links while on the blog manager index will refresh the middle container without reloading the whole page! Listing 8-19. Instantiating the BlogMonthlySummary Class (left-container.tpl) {get_monthly_blog_summary user_id=$identity->user_id assign=summary} {if $summary|@count > 0}

          Your Blog Archive

          {/if} Notifying the User About the Content Update Although the code we have just implemented works well and updates the page as it should, the only problem with it is that it doesn’t give any feedback to the user. To fix this, we will use the messages container we created in Chapter 7 to notify the user that new content is being loaded. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 287 9063Ch08CMP2 11/11/07 12:35 PM Page 287 In this section, we will create two new functions: message_write(), which we use to write a new message to the message container (and then make the container appear if hidden), and message_clear(), which hides the message container. We will then update the BlogMonthlySummary JavaScript class to use these functions so the user knows when page content has been updated. Managing Message Containers The first thing we need to do is to create a new setting for the settings hash in the scripts.js file. When we implement the message_clear() function next, we’ll add a delay so the message is cleared only after the specified interval. This ensures the user has time to read the message before it disappears. Listing 8-20 shows the messages_hide_delay setting we add to scripts.js in ./htdocs/js. This value is the number of seconds before the message container is hidden. Listing 8-20. Adding the Delay Setting to the Application JavaScript Settings (scripts.js) var settings = { messages : 'messages', messages_hide_delay : 0.5 }; Next we define the message_write() and message_clear() functions, which can go after the Event.observe() call in the scripts.js file. Listing 8-21 shows these functions. Listing 8-21. Setting and Clearing Site Status Messages (scripts.js) function message_write(message) { var messages = $(settings.messages); if (!messages) return; if (message.length == 0) { messages.hide(); return; } messages.update(message); messages.show(); new Effect.Highlight(messages); } function message_clear() { setTimeout("message_write('')", settings.messages_hide_delay * 1000); } CHAPTER 8 ■ EXTENDING THE BLOG MANAGER288 9063Ch08CMP2 11/11/07 12:35 PM Page 288 The message_write() function works by first checking the length of the message to show. If it is an empty string, the messages container is hidden. If the string isn’t empty, then the content of the container is updated to show the message. Finally, the container is shown, and the Scriptaculous highlight effect is once again applied. The message_clear() function simply calls the message_write() function with an empty string after the specified delay time. Note that to be consistent with Scriptaculous, I specified the delay time in seconds, while setTimeout() accepts milliseconds (1/1000th of a second). This is why we multiply the value by 1,000. Updating the Messages Container with BlogMonthlySummary Finally, we must modify the BlogMonthlySummary JavaScript class to use the message_write() and message_clear() functions. We’ll call message_write() in the link click event handler (onLinkClick()), and we will then call message_clear() once the Ajax request has completed. We do this by calling message_clear() in the onSuccess callback option for Ajax.Updater. Listing 8-22 shows the new version of the onLinkClick() event handler in BlogMonthlySummary.class.js (in the./htdocs/js directory). Listing 8-22. Updating the Message Container When Loading Blog Posts (BlogMonthlySummary.class.js) BlogMonthlySummary = Class.create(); BlogMonthlySummary.prototype = { // ... other code onLinkClick : function(e) { var link = Event.element(e); var options = { onComplete : message_clear }; message_write('Loading blog posts...'); new Ajax.Updater(this.container, link.href, options); Event.stop(e); } }; CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 289 9063Ch08CMP2 11/11/07 12:35 PM Page 289 In Figure 8-2 you can see how the blog manager index page now looks after an archive link in the left column has been clicked. Note the status message at the top of the right of the picture, while at the bottom Firebug shows that a background request is running. Figure 8-2. The blog manager index when an archive link is clicked We have now completed the Ajax functionality on the blog manager monthly summary page. The way we have implemented it works very well, because of the following reasons: • It is easy to maintain. We are using the same Smarty template for both the non-Ajax and Ajax versions, meaning to change the layout we need to modify only this one file. • The code is clean. There is almost no clutter in our HTML code for the extensive JavaScript code that is used. The only code is a single call to instantiate the BlogMonthlySummary class. • The page is accessible. If the user doesn’t have a JavaScript-enabled browser (or dis- ables JavaScript), they are not restricted from using this section in any way. It is simply enhanced for users who do use JavaScript. • The page is scalable. An alternative method to loading the posts by Ajax would be to preload them and place them in hidden containers on the page. This works fine for a small number of posts, but once you hit a larger number, the page takes much longer to load and uses more memory on your computer. • It tells the users what is happening. By adding the message container, the user knows that something is happening when they click an archive link, even though the browser doesn’t start to load another page. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER290 9063Ch08CMP2 11/11/07 12:35 PM Page 290 • The code is cross-browser compatible. Because we used the Prototype library, we were easily able to make code that works across all major browsers. Using Prototype cuts down on development time, because only a single solution needs to be implemented— not one for each browser. Integrating a WYSIWYG Editor The final step in implementing the blog management tools we created in Chapter 7 and this chapter is to add “what you see is what you get” functionality. This allows users to easily for- mat their blog posts without requiring any real knowledge of HTML. The WYSIWYG editor we will be using is called FCKeditor, named so after its creator, Frederico Caldeira Knabben. It is a very powerful and lightweight editor, and it doesn’t require installation of any programs on the client’s computer (aside from their web browser, that is). More important, it is highly customizable. These are some of the customization features it contains: •It is easy to change the toolbar buttons available to users. •Custom plug-ins can be written, allowing the developer to create their own toolbar buttons. •It contains a built-in file browser that allows users to upload files to the server in real- time. Additionally, it allows custom-made connectors, which are scripts written in a server-side language (such as PHP) that handle uploads through the file browser. The connector can save the file wherever or however it needs to, and it can send back the list of files to the FCKeditor file browser as required. • The editor can be reskinned. In other words, the color scheme and look and feel of the buttons can be changed. •It provides the ability to define custom templates that can be easily inserted into the editor (not to be confused with the Smarty templates in our application). Figure 8-3 shows the default layout of FCKeditor, with all the toolbar buttons. Other features that make FCKeditor a popular choice for content management systems include the following: •It generates valid XHTML code (subject to how the user chooses to manipulate the HTML). •Users can paste in content from Microsoft Word, which will automatically be cleaned up by the editor. •It is cross-browser compatible. Currently it is not compatible with Safari because of some restrictions in that browser, but it works on other major browsers. Mac OS users can use Firefox as an alternative. Users of Safari are shown a plain textarea instead of the editor. In the following sections, we will download, install, and integrate FCKeditor into our web application. We will make some basic customizations to the editor, including restricting the toolbar buttons so only the HTML tags listed earlier this chapter will be generated. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 291 9063Ch08CMP2 11/11/07 12:35 PM Page 291 Figure 8-3. An example of editing content in FCKeditor Additionally, we will develop a Smarty plug-in that allows us to easily load the WYSIWYG in our templates when required. Downloading and Installing FCKeditor At time of writing, the current version of FCKeditor is version 2.4.3. This can be downloaded from http://www.fckeditor.net/download. We will be storing the code in the ./htdocs/js directory, just as we did with Prototype and Scriptaculous. Once you have the FCKeditor_2.4.3.tar.gz file, extract it to that directory. I have assumed you downloaded the file to /var/www/phpweb20/htdocs/js. # cd /var/www/phpweb20/htdocs/js # tar -zxf FCKeditor_2.4.3.tar.gz # rm FCKeditor_2.4.3.tar.gz # cd fckeditor/ # ls _documentation.html fckeditor.afp fckeditor.php fckstyles.xml _samples/ fckeditor.asp fckeditor.pl fcktemplates.xml _upgrade.html fckeditor.cfc fckeditor.py htaccess.txt _whatsnew.html fckeditor.cfm fckeditor_php4.php license.txt editor/ fckeditor.js fckeditor_php5.php fckconfig.js fckeditor.lasso fckpackager.xml CHAPTER 8 ■ EXTENDING THE BLOG MANAGER292 9063Ch08CMP2 11/11/07 12:35 PM Page 292 The first thing I usually like to do is go through and clean out the unnecessary files in the distribution. I will leave all these items for now, but you may consider deleting the following: • Loader classes for other languages (the fckeditor.* files in the main directory, aside from the fckeditor_php5.php file, which we will use shortly). •The file browser and upload connectors that aren’t being used. These can be found within the ./htdocs/js/fckeditor/editor/filemanager directory. Configuring FCKeditor Next we must configure the way FCKeditor works. We do this by modifying fckconfig.js in the main directory. Most of the settings we won’t need to touch, but we will need to customize the toolbars and then disable the connectors that are enabled by default. First we’ll define a new toolbar that contains only buttons for the list of tags we defined in Chapter 7. These tags are , , , , , ,
            ,
          • ,
              ,

              , and
              . On line 94 in fckconfig.js a toolbar called Default is defined, which contains a wide range of buttons, which is directly followed by a simpler toolbar called Basic. We will leave these two toolbars in this file and define a new toolbar called phpweb20 that is a combination of these toolbars. The primary reason for leaving them in is to use them as a reference for the other buttons that can be added. Listing 8-23 shows the JavaScript array we use to create a new toolbar. This can be placed in fckconfig.js directly after the other toolbars. Note that the '-' element renders a separator in the toolbar. Listing 8-23. The Custom FCKeditor Toolbar (fckconfig.js) FCKConfig.ToolbarSets["phpweb20"] = [ ['Bold','Italic','-','OrderedList','UnorderedList','-', 'Link','Unlink','-','Image'] ]; ■Note Technically speaking, Listing 8-23 actually defines a toolbar set, not a toolbar. In other words, one or more toolbars makes up a toolbar set. This code creates an array of arrays, where the internal arrays are the actual toolbars. The only other change we need to make in this configuration file is to disable the file manager and upload connectors, since we aren’t allowing users to upload files. Disabling them removes the respective options from the user interface. Listing 8-24 shows the new lines for fckconfig.js, all of which set the listed values to false. You can find at the bottom of the fckconfig.js file where each of these variables is defined as true and update them accordingly. Listing 8-24. Disabling the File Browser and Upload Connectors (fckconfig.js) FCKConfig.LinkBrowser = false; CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 293 9063Ch08CMP2 11/11/07 12:35 PM Page 293 FCKConfig.ImageBrowser = false; FCKConfig.FlashBrowser = false; FCKConfig.LinkUpload = false; FCKConfig.ImageUpload = false; FCKConfig.FlashUpload = false; Loading FCKeditor in the Blog Editing Page Finally, we need to load the editor in the blog post’s editing form. First we will write a Smarty plug-in that outputs HTML code to load. There is a PHP class bundled with FCKeditor to facil- itate the generation of the HTML. The FCKeditor class is located in the fckeditor_php5.php file in the main FCKeditor direc- tory (./htdocs/js/fckeditor). To keep our own code organized, we will copy this class to the application include directory. Additionally, we will rename the file to FCKeditor.php to be consis- tent with our application file naming. This also means it can be autoloaded with Zend_Loader. # cd /var/www/phpweb20/htdocs/js/fckeditor # cp fckeditor_php5.php /var/www/phpweb20/include/FCKeditor.php Now we create a new Smarty plug-in called wysiwyg, which we can call in our template using {wysiwyg}. Listing 8-25 shows the contents of function.wysiwyg.php, which we store in ./include/Templater/plugins. Listing 8-25. A Smarty Plug-in to Create the FCKeditor in a Template (function.wysiwyg.php) BasePath = '/js/fckeditor/'; $fckeditor->ToolbarSet = 'phpweb20'; $fckeditor->Value = $value; return $fckeditor->CreateHtml(); } ?> When we call this Smarty function in the template, we provide two arguments: the name parameter and the value parameter. The name parameter defines the name of the form ele- CHAPTER 8 ■ EXTENDING THE BLOG MANAGER294 9063Ch08CMP2 11/11/07 12:35 PM Page 294 ment the user’s HTML is submitted in. The value parameter sets the default value to be shown in the WYSIWYG editor. After initializing these parameters, we instantiate the FCKeditor class. Next we must tell the $fckeditor object where the editor code is stored relative to the web root (we stored it in http://phpweb20/js/fckeditor). Next we must tell it to use the new toolbar we just created (phpweb20) rather than the default toolbar (Default). We then pass in the default value to the class. Finally, we call the CreateHtml() method to generate the FCKeditor HTML code, and we return it to the template. ■Note You can also set the width and height of the editor. By default, a width of 100 percent and a height of 200 pixels are used. To change the height to 300 pixels, you would use $fckeditor->Height = 300;. The only thing left to do now is to call {wysiwyg} in the edit.tpl template in the ./templates/blogmanager directory. Listing 8-26 shows the changes we make to this template. I’ve moved the WYSIWYG editor out of the fieldset to make the form look a little nicer. Addi- tionally, I’ve wrapped it in a div with a class name of .wysiwyg, allowing us to add a new CSS class that adds some extra spacing around the editor. This new code replaces the textarea that was in the template previously. Listing 8-26. Loading the WYSIWYG in the Template

              Blog Post Details
              {wysiwyg name='content' value=$fp->content} {include file='lib/error.tpl' error=$fp->getError('content')}
              Finally, we add an extra style to styles.css (in ./htdocs/css) to add some extra spacing around the editor, as shown in Listing 8-27. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER 295 9063Ch08CMP2 11/11/07 12:35 PM Page 295 Listing 8-27. Adding Spacing Around the WYSIWYG Editor (styles.css) .wysiwyg { margin : 10px 0; } By creating a Smarty plug-in to help with loading the WYSIWYG editor, it is extremely sim- ple to load the editor, and we manage to keep the template code very clean. Additionally, you can easily define new parameters for the plug-in that you can then use with the FCKeditor class as required. Summary In this chapter, we extended the blog post management tools that we began in Chapter 7. We first looked at how to select large amounts of data from the database in an efficient manner before using this data to help users manage their blogs. Next we extended the capabilities of the blog post listing so it is Ajax-powered, thereby making it easier to use (since each page will load more quickly). One of the biggest advantages of our implementation is that it will automatically fall back to a non-Ajax solution if the user wasn’t using JavaScript. The final step in this chapter was to implement FCKeditor, an open source WYSIWYG edi- tor that allows users to easily format their blog posts using HTML. In the next chapter, we will focus on creating a public home page for each user that lists all of their live blog posts. When we do this, we will also update the application home page so it displays blog posts from all users that choose to have their posts included. CHAPTER 8 ■ EXTENDING THE BLOG MANAGER296 9063Ch08CMP2 11/11/07 12:35 PM Page 296 Personalized User Areas In Chapters 7 and 8 we created the necessary forms and tools for users to manage their blogs, allowing them to create, edit, and delete posts. In this chapter we will be extending the web application further by creating a public home page for each user, which will be used to display their blog posts. In addition to creating a home page for each user, we will populate the main home page of the web application. The home page will consist of blog posts from all users who choose to have their posts included. They will be able to make this choice by using the options we will add to the “Your Account Details” page in this chapter. One key technique we will be looking at in this chapter is defining a custom URL scheme, instead of using the /controller/action method used previously. The address of a user’s home page will be defined by their username, and we will manipulate the request handling of Zend_Controller_Front so that http://phpweb20/user/username will be used as the unique address to a user’s page. Combining this with the URL field we defined for blog posts, we will also create a unique permanent URL for every blog post that exists in the database. Controlling User Settings The first thing we’re going to do in this chapter is implement a settings-management system for users. This will allow them to control the way their blog behaves. These are the settings we want users to be able to control: • Whether or not posts are shown on the application home page. In the last section of this chapter we will change the application so it displays blog posts from all registered users on the home page if they choose to. By default, we will not include a user’s posts on the home page, but if they want to allow it, they will be able to change this setting. • The number of posts displayed on their own home page. When we set up the user home page, we will list the most recent posts on the this page. This setting will let the user control how many posts are shown on their home page. To see further posts, visi- tors will be able to click on a month to view all posts from that month. When we created the database tables for managing user data in Chapter 3, we created two tables: users and users_profile. The users_profile table was designed to allow us to easily expand the amount of data stored for each user account. We will use this table to store the settings we add in this section. Because of how this system is designed, you will be able to expand on it in the future if you want to give users more control over how their accounts or public home pages work. 297 CHAPTER 9 9063Ch09CMP2 11/13/07 8:08 PM Page 297 ■Note Since we have also created a profile table for blog posts (blog_posts_profile), we could even add per-post settings. You could use this in a number of different scenarios. For example, if you had allowed visitors to post comments on your blog posts, you could use per-post settings to disable commenting on a single post. An appropriate place to add these settings to the interface would be in the “Edit Blog Post” form that we added in Chapter 7. Presenting Customizable Settings to Users To give users control over these settings, we will add them to the “Your Account Details” page. This involves adding the necessary HTML elements to the template for this page, as well as updating the class that processes this form (FormProcessor_UserDetails). ■Note The code used to update user details was introduced at the end of Chapter 4. We didn’t actually implement this code in the book, so you will need to first download the source code to implement the functionality in this section. This includes the UserDetails.php file in ./include/FormProcessor, the detailsAction() and detailscompleteAction() methods in ./include/Controllers/ AccountController.php, and the details.tpl and detailscomplete.tpl templates in ./templates/ account. To implement settings management, the first thing we will do is add the settings described previously to the “Your Account Details” template. Listing 9-1 shows the HTML code we will add to the ./templates/account/details.tpl template. This code also includes several variables from the form processor. We will add these to the form processor shortly. Listing 9-1. Allowing Users to Configure Settings When Updating Their Account Details (details.tpl) {include file='header.tpl' section='account'}
              Update Your Details
              Account Settings
              CHAPTER 9 ■ PERSONALIZED USER AREAS298 9063Ch09CMP2 11/13/07 8:08 PM Page 298
              How many blog posts would you like to show on your home page?
              Would you like to display your blog posts on the web site home page?
              {include file='footer.tpl'} ■Tip To create standards-compliant XHTML, we must use selected="selected" to choose the prese- lected value in a ), checked="checked" should be used. For more information about this, refer to the “Attribute Minimization” section at http://www.w3.org/TR/ xhtml1/#h-4.5. This form can be viewed by logged-in users at http://phpweb20/account/details. Processing Changes to User Settings The next change we will make is to the form processor that processes the details.tpl tem- plate. First, we will retrieve the existing settings from the user profile so that they can be used in the form. Then we will process the submitted values and save them to the user profile. CHAPTER 9 ■ PERSONALIZED USER AREAS 299 9063Ch09CMP2 11/13/07 8:08 PM Page 299 Listing 9-2 shows the changes we will make to the UserDetails.php file in ./include/ FormProcessor. Listing 9-2. Changes to the User Details Form Processor (UserDetails.php) blog_public = $this->user->profile->blog_public; $this->num_posts = $this->user->profile->num_posts; } public function process(Zend_Controller_Request_Abstract $request) { // ... other code // process the user settings $this->blog_public = (bool) $request->getPost('blog_public'); $this->num_posts = max(1, (int) $request->getPost('num_posts')); $this->user->profile->blog_public = $this->blog_public; $this->user->profile->num_posts = $this->num_posts; // if no errors have occurred, save the user if (!$this->hasError()) { $this->user->save(); } // return true if no errors have occurred return !$this->hasError(); } } ?> It is now possible for users to update their settings by submitting the form shown in Figure 9-1. CHAPTER 9 ■ PERSONALIZED USER AREAS300 9063Ch09CMP2 11/13/07 8:08 PM Page 300 Figure 9-1. Allowing users to update account settings Creating Default User Settings If you were paying close attention to Figure 9-1, you might have noticed that the num_posts setting is empty. In other words, this setting won’t be set until the form has been submitted. It would be better to include some default value so the user has some reference point for chang- ing the setting when they use this form. In order to assign default settings to a new user account, we will modify the preInsert() method on the DatabaseObject_User class. This method is automatically called prior to a new user record being saved to the database—we used this method previously to create the pass- word for a new account. Listing 9-3 shows the changes we will make to the User.php file in ./include/DatabaseObject. I have set the default value for num_posts to be 10, and I chose false as the default setting for blog_public. You may prefer different values. Listing 9-3. Assigning Default Settings for Users (User.php) _newPassword = Text_Password::create(8); $this->password = $this->_newPassword; // default account settings $this->profile->blog_public = false; $this->profile->num_posts = 10; return true; } // ... other code } ?> ■Note You could present these settings to users when they register, thereby not requiring any defaults to be set here. However, you typically want to encourage people to register, so you want to make the process as simple as possible and allow them to further customize their account once they log in. To test that this functionality works correctly, try registering as a new user in the applica- tion. Once you have done so, you can either check the users_profile table in the database to see which values have been saved, or you can log in with the new account and visit the “Your Account Details” form we just modified to see if the setting values are prepopulated correctly. The UserController Class The next thing we will do is create a new controller for Zend_Controller_Front to display the public page. We will call it UserController. In this class, we will implement three main actions: • indexAction(): This method will be used to generate the home page for each user, accessible from http://phpweb20/user/username. On this page, we will list the most recent posts on the given user’s blog. The number of posts to be shown is controlled by the num_posts setting we added in the previous section. • archiveAction(): This method will be used to generate a list of all posts for a single month (which I refer to as a monthly archive). The output will be basically the same as that of indexAction(). By default, the current month will be selected. • viewAction(): This method will be used to display a single blog post. The posts listed on the indexAction() and monthAction() methods will link to this method. In the left column of each of these pages, a list of months that have blog posts will be shown, much like in the blog manager. The key difference is that this list is for visitors to view the blog archive, while the one in the blog manager allows the blog owner to access their posts to update them. CHAPTER 9 ■ PERSONALIZED USER AREAS302 9063Ch09CMP2 11/13/07 8:08 PM Page 302 In addition to these three main actions, we will also implement two methods called userNotFoundAction() and postNotFoundAction(), the first being used when a nonexistent user- name is present in the URL, while the second when trying to display a nonexistent blog post. Routing Requests to UserController For all the other controllers we have created so far, the access URL has been in the format http://phpweb20/controller/action; for example, the edit action of the blogmanager con- troller has a URL of http://phpweb20/blogmanager/edit. If no action is specified, index is the default action used for a controller. So in the case of blogmanager, the index action can be accessed using either http://phpweb20/blogmanager or http://phpweb20/blogmanager/index. In UserController, we will be altering the way URLs work, since all actions in this con- troller will relate to a particular user. In order to specify the user, we will change the URL scheme to be http://phpweb20/user/username/action. As you can see, we have inserted the username between the controller name (user) and the action. To achieve this, we must modify the router for our front controller. The router—an instance of Zend_Controller_Router—is responsible for determining the controller and action that should handle a user’s request based on the request URL. When Zend_Controller_Front is instantiated in our bootstrap index.php file, a set of default routes is automatically created to route requests using the http://phpweb20/controller/action scheme. We want to keep these routes intact for all other requests, but for the UserController we want an extra route. To do this, we must define the route, and then inject it into the front controller’s router. Creating a New Route To create a new route, there are three Zend_Controller classes that can be used (or you can develop your own). These are the existing classes: • Zend_Controller_Router_Route: This is the standard route used by Zend_Controller, allowing a combination of static and dynamic variables in a URL. A dynamic variable is indicated by preceding the variable name with a colon, such as :controller. The route we have used in this application so far has been /:controller/:action. For example, in http://phpweb20/blogmanager/edit, blogmanager is assigned to the controller request variable, while edit is assigned to the action request variable. • Zend_Controller_Router_Route_Static: In some cases, the URL you want to use doesn’t require any dynamic variables, and you can use this static route type. For example, if you wanted a URL such as http://phpweb20/sitemap, which internally was handled by a controller action called sitemapAction() in one of your controllers, you could route this URL accordingly, using /sitemap as the static route. • Zend_Controller_Router_Route_Regex: This type of route allows you to route URLs based on regular expression matches. For example, if you wanted to route all requests such as http://phpweb20/1234 (where 1234 could be any number), you could match the route using /([0-9]+). When used in combination with the default routes, any request that didn’t match this regular expression would be routed using the normal /:controller/:action route. CHAPTER 9 ■ PERSONALIZED USER AREAS 303 9063Ch09CMP2 11/13/07 8:08 PM Page 303 We will now create a new route to match a URL scheme of http://phpweb20/user/ username/action. Since this route will only be used for the UserController class we will be implementing shortly, we will hard-code the controller name (user), while the username and action values will be determined dynamically. If the action isn’t specified in the URL (as in the URL http://phpweb20/user/username), the action will default to index, just as it has previously. The route we will use is user/:username/:action/*. Since we are only using this route for UserController, we don’t include :controller in the string. When instantiating Zend_ Controller_Router_Route, the first argument is this string, while the second argument is an array that specifies the default parameters for the request. Since we know the controller for this request is user, we can specify this. We can also specify index as the default action. There- fore, the code we use to create this new route is as follows: $route = new Zend_Controller_Router_Route( 'user/:username/:action/*', array('controller' => 'user', 'action' => 'index') ); Injecting the Route into the Router Once the route has been created, it must be injected into the router so subsequent user requests will be matched against the route (in addition to any existing routes). The route is added by calling the addRoute() method on the Zend_Controller router, which can be accessed from the front controller by calling getRouter(). The first argument to addRoute() is a unique name to identify the route—it does not actually affect the behavior of the route. Listing 9-4 shows the code we will add to ./htdocs/index.php in order to create this route. The route should be added just prior to dispatching the request with $controller->dispatch(). Listing 9-4. Defining a New Route for User Home Pages (index.php) 'user', 'action' => 'index')); $controller->getRouter()->addRoute('user', $route); $controller->dispatch(); ?> CHAPTER 9 ■ PERSONALIZED USER AREAS304 9063Ch09CMP2 11/13/07 8:08 PM Page 304 ■Note An alternative solution to the route we have created in this section could be to create URLs like http://phpweb20/username without including the user controller name in the URL. While this is relatively easy to achieve, it requires some other changes in coding. For example, when users enter a username on the registration form, you would need to ensure that the entered username doesn’t conflict with an existing controller name (or file or directory name). You would also need to be wary of any future controllers you may want to create, as they will not be able to conflict with an existing username. Once this route has been added, you will be able to access the username parameter of the URL inside any of the actions in UserController by calling $request->getUserParam('username'). Dynamically Generating URLs for Custom Routes When we implemented the {geturl} Smarty plug-in—as well as the getUrl() method in the CustomControllerAction class—in Chapter 6, we used the Url helper. We used the simple() method from this class to generate a URL based on the controller and action name. This helper also provides a method called url(), which can be used to generate more complex URLs based on custom routes, such as the one we added in Listing 9-4. We will now use this method to generate the URL to the home page of each user. To generate a link using the url() method of the Url helper, you pass the route parame- ters (in our case, the name of the action and the username) as the first parameter, and the name of the route it is being built for as the second argument. The URL helper will then recon- struct a URL based on these parameters. Let’s now look at a specific example. In Listing 9-4, the name of the route we created was called user. Thus, if we wanted to generate a link to the home page of the user with a user- name of qz, the following code would be used: $helper = Zend_Controller_Action_HelperBroker::getStaticHelper('url'); $url = $helper->url( array('username' => 'qz'), 'user' ); This code would generate the following string: /user/qz/ We want to make use of this functionality in our own code, not only for the actions we will add in this chapter, but also for other actions we will add to this controller later in this book. To do this, we will add a new function to the CustomControllerAction.php file in ./include. Listing 9-5 shows the code for the getCustomUrl() method, which accepts the URL parameters as the first argument and the name of the route as the second argument. As described in Chapter 6, we can access the helper using $this->_helper->url from within a controller. CHAPTER 9 ■ PERSONALIZED USER AREAS 305 9063Ch09CMP2 11/13/07 8:08 PM Page 305 Listing 9-5. Building Complex URLs for Custom Routes (CustomControllerAction.php) getRequest()->getBaseUrl(), '/') . '/'; $url .= $this->_helper->url->simple($action, $controller); return $url; } public function getCustomUrl($options, $route = null) { return $this->_helper->url->url($options, $route); } // ... other code } ?> In order to generate URLs with this helper from within our templates, we will also make some changes to the {geturl} Smarty plug-in. We will modify this plug-in so that if a parame- ter called route is specified, we will use the url() method of the Url helper; otherwise we will revert back to the previous method of generating URLs (using simple()). For instance, to generate a URL back to the home page of the qz user from within a tem- plate, we will be able to use the following code in the template: {geturl route='user' username='qz'} Listing 9-6 shows the changes we will make to the function.geturl.php file in ./include/Templater/plugins. Listing 9-6. Extending the geturl Smarty Plug-In to Support Custom Routes (function.geturl.php) 0) { unset($params['route']); CHAPTER 9 ■ PERSONALIZED USER AREAS306 9063Ch09CMP2 11/13/07 8:08 PM Page 306 $url = $helper->url($params, $route); } else { $request = Zend_Controller_Front::getInstance()->getRequest(); $url = rtrim($request->getBaseUrl(), '/') . '/'; $url .= $helper->simple($action, $controller); } return $url; } ?> ■Note The url() method of the Url helper will automatically prepend the Zend_Controller base URL, but the simple() method does not. This is why we manually do this only for the simple() call in this code. Generating Other Required Routes In addition to the route added in Listing 9-4, we will add two more routes: one for displaying individual blog posts, and one for displaying the monthly archives of a user’s blog. When we implemented the blog-management tools in Chapters 7 and 8, we included a url field with each blog post. The value for this field is unique for every post in a single user’s blog. We will now use this value to create URLs for individual blog posts. Each blog post will have a URL in the form of /user/username/view/blog-post-url. The controller action that will handle requests to this route will be called viewAction()—we will implement this method later in this chapter. In this particular case, the controller and action name are hard-coded in the URL; it’s the username and blog post URL that are unique. Thus, we can use the following code to generate this new route: $route = new Zend_Controller_Router_Route( 'user/:username/view/:url/*', array('controller' => 'user', 'action' => 'view') ); For example, if I created a blog post with the title “My Holiday”, this would generate a unique URL of my-holiday. The full URL to this blog post (remembering that my username is qz) would be /user/qz/view/my-holiday. If I wanted to generate a link to this post from within a Smarty template, I could use the {geturl} plug-in we modified in Listing 9-6 as follows: {geturl user='qz' url='my-holiday' route='post'} CHAPTER 9 ■ PERSONALIZED USER AREAS 307 9063Ch09CMP2 11/13/07 8:08 PM Page 307 ■Note This assumes that when we inject the preceding route into the router, we use a name of post.We will do this shortly. Similarly, we can now create another route to handle blog post archives. The URL format for blog archives will be /user/username/archive/year/month. So to view my blog’s archive for, say, November 2007, the URL would be /user/qz/archive/2007/11. Once this route has been added (with a name of archive), we will be able to generate a link to this particular page in Smarty like this: {geturl user='qz' year=2007 month=11 route='archive'} The code we use to create this route is as follows: $route = new Zend_Controller_Router_Route( 'user/:username/archive/:year/:month/*', array('controller' => 'user', 'action' => 'archive') ); Listing 9-7 shows the changes we need to make to the bootstrap file (./htdocs/index.php) in order to create these new routes and add them to the router. Listing 9-7. Adding the Post and Archive Routes to the Router (index.php) 'user', 'action' => 'index') ); $controller->getRouter()->addRoute('user', $route); // set up the route for viewing blog posts $route = new Zend_Controller_Router_Route( 'user/:username/view/:url/*', array('controller' => 'user', 'action' => 'view') ); $controller->getRouter()->addRoute('post', $route); // set up the route for viewing monthly archives $route = new Zend_Controller_Router_Route( CHAPTER 9 ■ PERSONALIZED USER AREAS308 9063Ch09CMP2 11/13/07 8:08 PM Page 308 'user/:username/archive/:year/:month/*', array('controller' => 'user', 'action' => 'archive') ); $controller->getRouter()->addRoute('archive', $route); $controller->dispatch(); ?> Handling Requests to UserController Despite the fact that we have changed the routing rules for this particular controller, we still create actions in the same way as the other controllers. The only difference is that for all actions in this controller, there will be a request parameter called username available. Since each method in the controller is used to present data for a particular user, we want to load that user’s database record in every action. To aid with this, we will add code to UserController’s preDispatch() method, which is called automatically prior to the controller action method being called. Loading the user details in preDispatch() means the user data will be available to all actions in UserController. If the user record cannot be loaded (such as if we have an invalid username), we will forward control to a method we will implement shortly called userNotFoundAction(). Listing 9-8 shows the initial code for the UserController.php file, which is stored in the ./include/Controllers directory. Listing 9-8. Loading the Requested User Automatically for All Actions (UserController.php) getRequest(); // check if already dispatching the user not found action. if we are // then we don't want to execute the remainder of this method if (strtolower($request->getActionName()) == 'usernotfound') return; // retrieve username from request and clean the string $username = trim($request->getUserParam('username')); CHAPTER 9 ■ PERSONALIZED USER AREAS 309 9063Ch09CMP2 11/13/07 8:08 PM Page 309 // if no username is present, redirect to site home page if (strlen($username) == 0) $this->_redirect($this->getUrl('index', 'index')); // load the user, based on username in request. if the user record // is not loaded then forward to notFoundAction so a 'user not found' // message can be shown to the user. $this->user = new DatabaseObject_User($this->db); if (!$this->user->loadByUsername($username)) { $this->_forward('userNotFound'); return; } // Add a link to the breadcrumbs so all actions in this controller // link back to the user home page $this->breadcrumbs->addStep( $this->user->username . "'s Blog", $this->getCustomUrl( array('username' => $this->user->username, 'action' => 'index'), 'user' ) ); // Make the user data available to all templates in this controller $this->view->user = $this->user; } public function userNotFoundAction() { $username = trim($this->getRequest()->getUserParam('username')); $this->breadcrumbs->addStep('User Not Found'); $this->view->requestedUsername = $username; } public function indexAction() { } public function viewAction() { } CHAPTER 9 ■ PERSONALIZED USER AREAS310 9063Ch09CMP2 11/13/07 8:08 PM Page 310 public function postNotFoundAction() { } public function archiveAction() { } } ?> The first thing we do in this class is define the $user property, which holds an instance of DatabaseObject_User (created in Chapter 3). This variable will be automatically assigned to all templates in this controller (this is done on the final line of preDispatch()). We begin the preDispatch() method by first calling the parent preDispatch() method, as this contains code that we need executed for all actions (such as initializing the breadcrumbs trail and flash messenger created in Chapter 6). After this we must check whether the current action is the user-not-found action. If we don’t do this, the code will enter a recursive loop that cannot be broken (since it will continually redirect back to the userNotFoundAction() method). The preDispatch() method continues by initializing the username parameter from the request. If the string is empty (as will be the case if a URL of http://phpweb20/user is used), we ignore the request by just redirecting back to the home page. ■Note We could have used getParam() instead of getUserParam() on the request, but this would fall back to check “get” and “post” variables if an internal parameter was not found. This means that if you used http://phpweb20/user?username=validUser, the user record would be loaded. Typically, you don’t want people to be able to manipulate your applications in a way that wasn’t intended. If the string isn’t empty, we try to load a DatabaseObject_User record based on the user- name value. To do this, we implement a loader function called loadByUsername() in the DatabaseObject_User class, which is shown shortly in Listing 9-9. If the user record doesn’t load, we instantly forward the request to the userNotFound action and return from the current function. ■Tip Normally when you call _forward() in the Zend_Controller_Front controller, the current action is completed before calling the action to which you’re forwarding. If you call _forward() in preDispatch(), however, the original action is completely skipped and only the new action is executed. In Listing 9-8, we must still return after calling _forward() because the remainder of the code in preDispatch() will still be executed otherwise. CHAPTER 9 ■ PERSONALIZED USER AREAS 311 9063Ch09CMP2 11/13/07 8:08 PM Page 311 Next, we add a new step to the breadcrumb trail—one that will be automatically added to the trail for all actions in this controller. We use the getCustomUrl() method we added in Listing 9-5. Finally, we write the $this->user object to the view so it is available for all actions within this controller. As mentioned previously, we also need to implement a new record in the DatabaseObject_ User class to allow us to load a user record based on their username (previously we have used the record’s unique ID to load a record when using DatabaseObject). Listing 9-9 shows the code for the loadByUsername() method we will add to User.php in ./include/DatabaseObject. For a further description of how custom loader methods for DatabaseObject work, refer to the example in Chapter 7 (Listing 7-14). Listing 9-9. The loadByUsername() and getUrl() Functions for DatabaseObject_User (User.php) getSelectFields()), $this->_table); $query = $this->_db->quoteInto($query, $username); return $this->_load($query); } // ... other code } ?> We will finish off UserController.php for now by creating the userNotFoundAction() method. In order to tell the user specifically which username could not be found in the template, we will initialize it once again from the request and assign it to the template. Listing 9-10 shows a template you can use for userNotFoundAction(). This code belongs in the usernotfound.tpl template in ./templates/user. CHAPTER 9 ■ PERSONALIZED USER AREAS312 9063Ch09CMP2 11/13/07 8:08 PM Page 312 Listing 9-10. A Sample Template That Can Be Used When an Invalid User Is Specified in the URL (usernotfound.tpl). {include file='header.tpl'}

              The user "{$requestedUsername|escape}" could not be found.

              {include file='footer.tpl'} Displaying the User’s Blog Now that we know which user’s blog is being requested when the UserController class is invoked, we can load the relevant blog posts and display them. This works much like the blog index in the blog manager controller we created in Chapter 6. The key difference is in the pres- entation: •Only approved blog posts will ever be included in this controller. This applies to all actions, not just the index action. • The index page will only show recent blog posts (determined by the num_posts setting we implemented earlier in this chapter). All posts from previous months will be accessi- ble using the archive links. Each month in the archive will have a unique URL. •Rather than seeing edit and delete buttons, users will see a link to view the full blog post. Displaying the Blog Index Page On the blog index page, we want to show recent posts, although this differs from the blog manager in that we must be wary of the following: •If we only show posts from the current month, there may be no content to display. This is especially true when a new month begins. •If we show all content from the current month, there may be too much content to dis- play. If the user has been extremely active in the month, there could be 30 or 40 posts, which could result in a long loading time for the page. •If we don’t show all of the posts from the current month, the viewer may not be able to access posts from earlier in the month. We are going to solve each of these problems by displaying only a limited number of posts on the user’s home page (based on the num_posts setting) and providing a link to the monthly archives. CHAPTER 9 ■ PERSONALIZED USER AREAS 313 9063Ch09CMP2 11/13/07 8:08 PM Page 313 Implementing the indexAction() Method The first change we will make is to the indexAction() method in the UserController class. We created a placeholder for this method earlier in this chapter, but we will now implement it by retrieving the relevant posts using the GetPosts() method from DatabaseObject_BlogPost. This method begins by determining the number of posts to retrieve. We first check for the num_posts setting (making sure the value is at least 1 by using max()). If this setting isn’t found in the user profile, a default value of 10 is used. Next, we will build an array of options to pass to DatabaseObject_BlogPost::GetPosts(). In this array, we will include the $limit variable just created, and we’ll specify that only live blog posts should be loaded (by using the DatabaseObject_BlogPost::STATUS_LIVE constant). Listing 9-11 shows the code we will add to the indexAction() of the UserController.php file (in ./include/Controllers). Listing 9-11. Loading the Most Recent Posts in the Index Action (UserController.php) user->profile->num_posts)) $limit = max(1, (int) $this->user->profile->num_posts); else $limit = 10; $options = array( 'user_id' => $this->user->getId(), 'status' => DatabaseObject_BlogPost::STATUS_LIVE, 'limit' => $limit, 'order' => 'p.ts_created desc' ); $posts = DatabaseObject_BlogPost::GetPosts($this->db, $options); $this->view->posts = $posts; } // ... other code } ?> CHAPTER 9 ■ PERSONALIZED USER AREAS314 9063Ch09CMP2 11/13/07 8:08 PM Page 314 The preceding code includes a parameter called status that we use to ensure that only live posts are returned; however, the _GetBaseQuery() method in DatabaseObject_BlogPost doesn’t yet allow for this option. Listing 9-12 shows how we can make changes to the available options in _GetBaseQuery(), so that the changes are also available in other functions such as GetPosts() and GetPostsCount(). These changes are made in the BlogPost.php file in ./include/DatabaseObject. Listing 9-12. Filtering Posts Based on the Status Field (BlogPost.php) array(), 'status' => '', 'from' => '', 'to' => '' ); // ... other code // filter results based on post status if (strlen($options['status']) > 0) $select->where('status = ?', $options['status']); return $select; } } ?> Displaying Blog Posts on the User Home Page To output the posts retrieved in indexAction() of UserController, we will make a template called index.tpl, which we will store in the ./templates/user directory. Just as in the blog manager, this template will loop over each post and then call another template in the loop to control the actual output. This is done so we can reuse this template when outputting the monthly archives. Listing 9-13 shows the code for index.tpl. CHAPTER 9 ■ PERSONALIZED USER AREAS 315 9063Ch09CMP2 11/13/07 8:08 PM Page 315 Listing 9-13. Outputting the Most Recent Posts on the User’s Blog (index.tpl) {include file='header.tpl'} {if $posts|@count == 0}

              No blog posts were found for this user.

              {else} {foreach from=$posts item=post name=posts} {include file='user/lib/blog-post-summary.tpl' post=$post} {/foreach} {/if} {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl'} This code first checks whether any posts are in the $posts array. If there are none, it is safe to assume there are no approved posts in the user’s blog. We then loop over the $posts array, including the blog-post-summary.tpl template for each iteration. Using a separate template to output the blog post allows us to reuse the same code on other pages. ■Note By naming the {foreach} loop (that is, specifying the name parameter), we can access the {$smarty.foreach.name.last} parameter, which is a Boolean value that is true only for the last itera- tion of the loop. Similarly, Smarty makes the $smarty.foreach.name.first value available (among others). For more details, refer to the Smarty manual page at http://smarty.php.net/manual/en/ language.function.foreach.php. Next, we need to create the blog-post-summary.tpl template, which is stored in ./templates/default/user/lib. This template is shown in Listing 9-14. Listing 9-14. Displaying a Single Blog Post Teaser (blog-post-summary.tpl) {capture assign='url'}{geturl username=$user->username url=$post->url route='post'}{/capture}

              {$post->profile->title}

              CHAPTER 9 ■ PERSONALIZED USER AREAS316 9063Ch09CMP2 11/13/07 8:08 PM Page 316 {$post->ts_created|date_format:'%b %e, %Y %l:%M %p'}
              {$post->getTeaser(500)}
              At the beginning of this template, we generate a URL to the full page for the blog post. By doing this once at the start of the template, we can reuse the $url variable in this template, rather than having to call {geturl} for every spot we want to include the URL. Because {geturl} returns the generated URL to the template directly, we can use the {capture} Smarty plug-in (built into Smarty) to trap the output and assign it to the $url vari- able. This plug-in works similarly to output buffering in PHP. The remainder of the template simply outputs a summary of the blog post. In order to style this output, we can add several styles to the ./htdocs/css/styles.css file, as shown in Listing 9-15. Listing 9-15. Styling the Blog Post Preview (styles.css) .teaser { border-top : 1px dashed #eee; padding : 5px 0; margin : 10px 0; } .teaser h3 { margin : 0; } .teaser-date { font-size : 0.8em; color : #666; margin : 0 0 10px 0; } .teaser-links { font-size : 0.9em; background : #f7f7f7; padding : 5px; line-height : 1em; margin-top : 5px; clear : both; } CHAPTER 9 ■ PERSONALIZED USER AREAS 317 9063Ch09CMP2 11/13/07 8:08 PM Page 317 After creating these templates and making the changes to the style sheet, you should be able to view a user’s home page, as shown in Figure 9-2. Figure 9-2. Displaying posts on a user’s public home page ■Caution The template created in Listing 9-13 uses the left-column.tpl template, which we have not yet created. In order to emulate Figure 9-2, you can either remove this from the code temporarily or create the ./templates/user/lib/left-column.tpl file. We will implement this template later in this chapter. Displaying Individual Blog Posts In the previous section, we created the indexAction() method, which displayed a list of the most recent blog posts, each with a link to view the full details. We will now implement viewAction(), which will display the full details of the post. For now, all the page will show is the title, timestamp, and body content of the post, but we will expand this page in later chapters when we add more functionality to the blogging system (such as tags, images, and maps). Because of the custom route we added earlier in this chapter, the viewAction() method will be accessed using a URL of http://phpweb20/user/username/view/blogposturl. This CHAPTER 9 ■ PERSONALIZED USER AREAS318 9063Ch09CMP2 11/13/07 8:08 PM Page 318 means that in order to access the requested blog post URL, we must fetch the url user param- eter using $request->getUserParam('url'). We can then load the blog post that corresponds to this URL value for the current user. Loading Live Blog Posts Using the URL To load a blog post based on the loaded user record and the blog post URL, we must implement another loader method in the DatabaseObject_BlogPost class, similar to the loadForUser() method, but this time using the post URL instead of the post ID. Additionally, we must ensure that only live records are loaded and not blog posts that are still in draft. Listing 9-16 shows the code for the loadLivePost() method, which we will add to the BlogPost.php file in ./include/DatabaseObject. Listing 9-16. Loading Live Blog Posts Based on the URL (BlogPost.php) _db->select(); $select->from($this->_table, $this->getSelectFields()) ->where('user_id = ?', $user_id) ->where('url = ?', $url) ->where('status = ?', self::STATUS_LIVE); return $this->_load($select); } // ... other code } ?> Implementing the viewAction() Method Now that we have the ability to load a live blog post based on its URL, we will implement viewAction()—the method responsible for calling loadLivePost() and then displaying a blog post’s details. CHAPTER 9 ■ PERSONALIZED USER AREAS 319 9063Ch09CMP2 11/13/07 8:08 PM Page 319 Listing 9-17 shows the code we will add to the UserController.php file in ./include/ Controllers. I have also included the code for postNotFoundAction(), which is used if a blog post that isn’t found (or that isn’t live) is requested. Listing 9-17. Implementing the viewAction() and postNotFoundAction() Methods (UserController.php) getRequest(); $url = trim($request->getUserParam('url')); // if no URL was specified, return to the user home page if (strlen($url) == 0) { $this->_redirect($this->getCustomUrl( array('username' => $this->user->username, 'action' => 'index'), 'user' )); } // try and load the post $post = new DatabaseObject_BlogPost($this->db); $post->loadLivePost($this->user->getId(), $url); // if the post wasn't loaded redirect to postNotFound if (!$post->isSaved()) { $this->_forward('postNotFound'); return; } // build options for the archive breadcrumbs link $archiveOptions = array( 'username' => $this->user->username, 'year' => date('Y', $post->ts_created), 'month' => date('m', $post->ts_created) ); $this->breadcrumbs->addStep( date('F Y', $post->ts_created), $this->getCustomUrl($archiveOptions, 'archive') ); $this->breadcrumbs->addStep($post->profile->title); CHAPTER 9 ■ PERSONALIZED USER AREAS320 9063Ch09CMP2 11/13/07 8:08 PM Page 320 // make the post available to the template $this->view->post = $post; } public function postNotFoundAction() { $this->breadcrumbs->addStep('Post Not Found'); } // ... other code } ?> This method begins by retrieving the url parameter from the request. If this value is empty (if, for example, the URL http://phpweb20/user/username/view was requested), the visitor is redirected to the user’s home page. Next, the code attempts to load a live record based on the url parameter. If the record was not loaded, the request is forwarded to the postNotFoundAction() method, used to show a simple error message to the user. This would typically occur if a visitor bookmarked a blog post that was either deleted or changed from live to draft. We then add steps to the breadcrumb trail so the user can navigate to a list of other posts in the month of the current post. We use the getCustomUrl() method of CustomControllerAction to generate these URLs. Although we haven’t yet implemented the archiveAction() method, we added the archive route to the router earlier in this chapter. Finally, the post is assigned to the template so we can output it to the viewer. Displaying the Blog Post Details The next step is to make the template that will output the blog post details. In this template, we will output the timestamp of the blog and the blog post content. The title is displayed auto- matically, since we added it to the breadcrumb trail. When we add other features to the blog (such as images, tags, and maps) we will expand on this template to display those new ele- ments. Listing 9-18 shows the code for view.tpl, which we write to the ./templates/user directory. Listing 9-18. Outputting a Single Blog Post in Full (view.tpl) {include file='header.tpl'}
              {$post->profile->content}
              {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl'} CHAPTER 9 ■ PERSONALIZED USER AREAS 321 9063Ch09CMP2 11/13/07 8:08 PM Page 321 To style the date, we will add the styles shown in Listing 9-19 to the ./htdocs/css/styles.css file. Listing 9-19. Formatting the Display of the Blog Post Date (styles.css) .post-date { font-size : 0.8em; color : #666; margin : 0 0 10px 0; } Creating the Template for postNotFoundAction() Finally, we need to create a template to notify the visitor that the requested blog post couldn’t be found. This template will be shown if a visitor bookmarks a blog post that has subsequently been deleted or sent back to draft. Listing 9-20 shows the postnotfound.tpl template, which is stored in the ./templates/user directory. Listing 9-20. Displaying a “Post not Found” Template (postnotfound.tpl) {include file='header.tpl'}

              The selected post could not be found.

              Return to {$user->username|escape}'s blog

              {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl'} Generating Blog Archive Links Next, we must provide links to each of the months in a user’s blog so all previous posts can easily be accessed. Thankfully, we already implemented this in Chapter 8 when creating the blog manager. We will be adding these links in the side column, once again using the {get_monthly_ blog_summary} Smarty plug-in we created in Chapter 8. In order to use this plug-in, we must make one modification to it, which is to add an extra parameter to indicate that only live blog posts should be included. Listing 9-21 shows the changes we will make to the function.get_ monthly_blog_summary.php file in ./include/Templater/plugins. CHAPTER 9 ■ PERSONALIZED USER AREAS322 9063Ch09CMP2 11/13/07 8:08 PM Page 322 Listing 9-21. Modifying the Plug-In to Only Include Live Posts (function.get_monthly_blog_summary.php) We can now create a new template to display content in the left column, just like in the blog manager. We will call this template left-column.tpl and save it in the ./templates/user/ lib directory. This file is shown in Listing 9-22. The templates we created earlier in this chapter use this template (view.tpl, usernotfound.tpl, and postnotfound.tpl). Note that this tem- plate is similar to the corresponding blog manager file (./templates/blogmanager/lib/ left-column.tpl), except that the user ID is specified using $user->getId() instead of $identity->user_id (since we want the user ID of the blog, not of the logged-in user). Listing 9-22. Displaying the Monthly Summary for the Current Blog (left-column.tpl) {get_monthly_blog_summary user_id=$user->getId() assign=summary liveOnly=true} {if $summary|@count > 0}

              {$user->username|escape}'s Blog Archive

              {/if} CHAPTER 9 ■ PERSONALIZED USER AREAS 323 9063Ch09CMP2 11/13/07 8:08 PM Page 323 ■Note An interesting aspect of this template is in the year and month arguments of the call to {geturl}. Here we use modifiers on a function argument, whereas previously we’ve only used modifiers when out- putting a variable directly. Once you have implemented this template, you will now be able to view a blog post as well as have links to all the months in your blog. This is shown in Figure 9-3. Figure 9-3. Viewing the details for a single blog post Displaying the Monthly Archive The next step is to create a page that displays all posts for a single month. This is the page that the links generated in Listing 9-22 link to. To do this, we will implement the archiveAction() method of UserController. This is the method used by the archive route we created earlier in this chapter. Implementing the archiveAction() Method The archiveAction() method that we use to display all the posts for a single month is somewhat trivial to implement. All of the pieces are already in place to retrieve this data (DatabaseObject_BlogPost::GetPosts()) and to display it (the blog-post-summary.tpl template)—we just now need to glue the pieces together. CHAPTER 9 ■ PERSONALIZED USER AREAS324 9063Ch09CMP2 11/13/07 8:08 PM Page 324 Listing 9-23 shows the code for archiveAction() as it appears in the UserController.php file in ./include/Controllers. Note that unlike the blog manager, where we manually parsed the month and year, we can now simply fetch them out of the request because of the new route that was created in Listing 9-7. Listing 9-23. Retrieving All Posts for a Single Month (UserController.php) getRequest(); // initialize requested date or month $m = (int) trim($request->getUserParam('month')); $y = (int) trim($request->getUserParam('year')); // ensure month is in range 1-12 $m = max(1, min(12, $m)); // generate start and finish timestamp for the given month/year $from = mktime(0, 0, 0, $m, 1, $y); $to = mktime(0, 0, 0, $m + 1, 1, $y) - 1; // get live posts based on timestamp with newest posts listed first $options = array( 'user_id' => $this->user->getId(), 'from' => date('Y-m-d H:i:s', $from), 'to' => date('Y-m-d H:i:s', $to), 'status' => DatabaseObject_BlogPost::STATUS_LIVE, 'order' => 'p.ts_created desc' ); $posts = DatabaseObject_BlogPost::GetPosts($this->db, $options); $this->breadcrumbs->addStep(date('F Y', $from)); // assign the requested month and the posts found to the template $this->view->month = $from; $this->view->posts = $posts; } // ... other code } ?> CHAPTER 9 ■ PERSONALIZED USER AREAS 325 9063Ch09CMP2 11/13/07 8:08 PM Page 325 Finally, we will implement the archive.tpl template, which we will store in the ./templates/user directory. This is shown in Listing 9-24. It is very similar to the ./templates/ user/index.tpl template we implemented earlier in this chapter. Listing 9-24. Outputting the Monthly Archive (archive.tpl) {include file='header.tpl'} {if $posts|@count == 0}

              No blog posts were found for this month.

              {else} {foreach from=$posts item=post name=posts} {include file='user/lib/blog-post-summary.tpl' post=$post} {/foreach} {/if} {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl'} Populating the Application Home Page Now that we have created a home page for each user on the site, we will implement the main application home page. This will work similarly to the user home page, except that it will com- bine blog posts from all users on the site instead of just displaying posts for one user at a time. When we implemented settings management earlier in this chapter, one of the settings users could customize was the blog_public setting. If this value is set to true, the user’s posts will be included on the home page. In this section, we will first make some changes to the GetPosts() method in DatabaseObject_BlogPost so we can select posts only for users who have public posts. Then we will implement the indexAction() method of the IndexController class. This is the method that handles the application home page. Finally, we will change the template for this method so the blog posts are displayed. Loading Recent Public Posts The first thing we will do is extend the _GetBaseQuery() method in the DatabaseObject_ BlogPost class. We do this so that when we call GetPosts() we are able to return posts only for users who have set the blog_public setting to true. Listing 9-25 shows the changes we must make to the BlogPost.php file in ./include/ DatabaseObject. Listing 9-25. Selecting Posts Only for Users Who Have Public Blogs (BlogPost.php) array(), 'public_only' => false, 'status' => '', 'from' => '', 'to' => '' ); // ... other code if ($options['public_only']) { $select->joinInner(array('up' => 'users_profile'), 'p.user_id = up.user_id', array()) ->where("profile_key = 'blog_public'") ->where('profile_value = 1'); } return $select; } // ... other code } ?> These changes work by joining against the users_profile table if the public_only option is set to true. It joins using the user_id column that exists in both the blog_posts and users_ profile table, using the profile_key value of blog_public. Note that the Profile class (intro- duced in Chapter 3) stores Boolean values as integers. Thus, true is stored as 1 and false as 0. Implementing the Application Home Page The next step is to implement the application home page. The action handler for this page is the indexAction() method of the IndexController class. This is the very first controller we created in this book (see Chapter 2). Our goal in this method is to retrieve the latest blog posts (we will use the 20 most recent) and assign them to the template. We will use the GetPosts() method from DatabaseObject_ BlogPost to achieve this, specifying the public_only option we created in Listing 9-25. The other thing we need to do in this method is load the corresponding DatabaseObject_User record for each post that is returned. We do this so that when we list each post on the home page we can link back to each users’ home page. CHAPTER 9 ■ PERSONALIZED USER AREAS 327 9063Ch09CMP2 11/13/07 8:08 PM Page 327 Loading Multiple User Records In order to fetch the multiple user records, as just described, we need to implement a method that allows us to do so. We will do this in a similar manner to the GetPosts() method we implemented in Chapter 8. The main difference is that we are now selecting data from the users table instead of the blog_posts table. Listing 9-26 shows the code for the GetUsers(), GetUsersCount(), and _GetBaseQuery() methods we will add to the User.php file in ./include/DatabaseObject. For a detailed descrip- tion on how these methods work, you can refer to the “Fetching Blog Posts from the Database” section in Chapter 8. Listing 9-26. Adding the Ability to Retrieve Multiple User Records at Once (User.php) 0, 'limit' => 0, 'order' => 'u.username' ); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } $select = self::_GetBaseQuery($db, $options); // set the fields to select $select->from(null, 'u.*'); // set the offset, limit, and ordering of results if ($options['limit'] > 0) $select->limit($options['limit'], $options['offset']); $select->order($options['order']); // fetch user data from database $data = $db->fetchAll($select); // turn data into array of DatabaseObject_User objects $users = parent::BuildMultiple($db, __CLASS__, $data); if (count($users) == 0) return $users; CHAPTER 9 ■ PERSONALIZED USER AREAS328 9063Ch09CMP2 11/13/07 8:08 PM Page 328 $user_ids = array_keys($users); // load the profile data for loaded posts $profiles = Profile::BuildMultiple($db, 'Profile_User', array('user_id' => $user_ids)); foreach ($users as $user_id => $user) { if (array_key_exists($user_id, $profiles) && $profiles[$user_id] instanceof Profile_User) { $users[$user_id]->profile = $profiles[$user_id]; } else { $users[$user_id]->profile->setUserId($user_id); } } return $users; } public static function GetUsersCount($db, $options) { $select = self::_GetBaseQuery($db, $options); $select->from(null, 'count(*)'); return $db->fetchOne($select); } private static function _GetBaseQuery($db, $options) { // initialize the options $defaults = array('user_id' => array()); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } // create a query that selects from the users table $select = $db->select(); $select->from(array('u' => 'users'), array()); // filter results on specified user ids (if any) if (count($options['user_id']) > 0) $select->where('u.user_id in (?)', $options['user_id']); return $select; } } ?> CHAPTER 9 ■ PERSONALIZED USER AREAS 329 9063Ch09CMP2 11/13/07 8:08 PM Page 329 Retrieving the Latest Posts for the Home Page We can now retrieve the latest posts for users who have public blogs, as well as retrieving their user records so we can correctly link back to their blogs when we output their posts. Listing 9-27 shows the code we will add to the IndexController.php file in ./include/Controllers to do this. Listing 9-27. Retrieving the Latest Public Posts for the Home Page (IndexController.php) DatabaseObject_BlogPost::STATUS_LIVE, 'limit' => 2, 'order' => 'p.ts_created desc', 'public_only' => true ); // retrieve the blog posts $posts = DatabaseObject_BlogPost::GetPosts($this->db, $options); // determine which users' posts were retrieved $user_ids = array(); foreach ($posts as $post) $user_ids[$post->user_id] = $post->user_id; // load the user records if (count($user_ids) > 0) { $options = array( 'user_id' => $user_ids ); $users = DatabaseObject_User::GetUsers($this->db, $options); } else $users = array(); // assign posts and users to the template $this->view->posts = $posts; $this->view->users = $users; } } ?> This method begins by defining the options to be passed to the GetPosts() method. Unlike when we used GetPosts() to retrieve posts for the user home page (see Listing 9-11), CHAPTER 9 ■ PERSONALIZED USER AREAS330 9063Ch09CMP2 11/13/07 8:08 PM Page 330 we don’t specify which user_id value to filter the results on. Instead, we use the new public_only parameter, which will then make use of all public user blogs. We can then call GetPosts() to retrieve an array of blog posts to display on the home page. The next step is to determine which users’ blog posts were used. We do this by looping over the posts and adding the user_id field to the $user_ids array. Using the ID as the key is a little trick used to prevent duplication in the array (in case one user has multiple posts on the home page). We can then retrieve an array of user records, which we write to the $users template vari- able (along with the blog posts in $posts). Creating the Application Home Page Template Finally, we can create the template used to output the blog posts that were retrieved in Listing 9-27. Once again, we will make use of the blog-post-summary.tpl template to output the template teaser. We will make a slight change to this template now, so that in addition to linking to the blog post it represents, it will also link back to the post owner’s home page. Listing 9-28 shows the changes we will make to the blog-post-summary.tpl template in ./templates/user/lib. In this template, we now check for the $linkToBlog variable. If this variable is set to true, we will provide a link back to the user’s home page. Listing 9-28. Linking Back to a User’s Home Page (blog-post-summary.tpl) Next, we implement the home page template. Since blog-post-summary.tpl expects a variable called $user (which has been automatically assigned in previous methods where we’ve used this template), we must retrieve the correct user object from the $users array assigned in Listing 9-27. Additionally, we specify the $linkToBlog variable. Listing 9-29 shows the code for the index.tpl template, which is stored in the ./templates/index directory. Listing 9-29. Displaying Posts on the Application Home Page (index.tpl) {include file='header.tpl' section='home'} {if $posts|@count == 0}

              No blog posts were found!

              CHAPTER 9 ■ PERSONALIZED USER AREAS 331 9063Ch09CMP2 11/13/07 8:08 PM Page 331 {else} {foreach from=$posts item=post name=posts} {assign var='user_id' value=$post->user_id} {include file='user/lib/blog-post-summary.tpl' post=$post user=$users.$user_id linkToBlog=true} {/foreach} {/if} {include file='footer.tpl'} In the preceding code, we assign the user_id value to a temporary variable in the tem- plate called $user_id. This is done so that we can retrieve the correct value from the $users array. Smarty syntax doesn’t allow us to use $users[$post->user_id]. Once you have added the code in this section, your application home should now list recent posts from all users who have chosen to have a public home page, as shown in Figure 9-4. Figure 9-4. The application home page, showing posts from all users with public blogs CHAPTER 9 ■ PERSONALIZED USER AREAS332 9063Ch09CMP2 11/13/07 8:08 PM Page 332 Summary In this chapter, we primarily focused on creating a public page for each user that has regis- tered in our web application. In order to create friendly URLs for user pages, we looked at how to create new request routes for Zend_Controller_Front. This meant that not only does each user have a short and simple URL for their home page, but each post within their blog has a short and simple URL also. Prior to doing this, we gave registered users the ability to customize various account set- tings. This was done in a manner that easily scales, since new settings can be added with only minor changes to code and no changes to the database. We also looked at how to set default account settings. After creating user home pages, we then implemented the application home page func- tionality. On this page we included posts that were recently submitted by all users who chose to have a public blog (using the settings-management system we implemented). In the next chapter, we will continue to expand the blog by implementing a number of Web 2.0 features, including adding tags to blog posts, formatting HTML using microformats, and creating web feeds for user blogs. CHAPTER 9 ■ PERSONALIZED USER AREAS 333 9063Ch09CMP2 11/13/07 8:08 PM Page 333 9063Ch09CMP2 11/13/07 8:08 PM Page 334 Implementing Web 2.0 Features Up until now in this book, we have looked at various techniques of using Ajax (that is, the XMLHttpRequest object with the help of Prototype) to manipulate data in the web application we are developing. Some examples of this include updating the HTML content on a web page dynamically, validating forms, and using JSON as a means to exchange data between the client and the server. Although these techniques are very useful and somewhat straightforward to implement, they are not the only features that define a web site as being “Web 2.0.” In this chapter, we will look at some of the other features of Web 2.0, which include the following: • Tags. A tag is typically one or two words used to describe some arbitrary item. Because a tag is usually very concise, it is used as a way to categorize said items. One item can have multiple tags, and if used properly, one tag will belong to multiple items. • Web feeds. A web feed is a stream of a web site’s content provided by the site’s owner in order to allow other publishers to display that data. This is often done using the Atom or RSS standard, although other formats are available. • Microformats. A microformat is a simple data format used to formally structure certain kinds of HTML data. For instance, whenever you want to list a person’s contact details on a web page, you would mark it up in HTML according to the hCard microformat (which we will look at later in this chapter). You can still style and lay out the contact details however you’d like in your CSS, but by marking it up in this way a microformat reader can recognize this data easily as a person’s contact details. For more information about microformats, visit http://microformats.org. In this chapter, we will extend the capabilities of the blogging system we have created to allow users to assign tags to posts. We will then change the output of their home page to cate- gorize their blog posts based on the assigned tags. Next we will take a look at web feeds by creating an Atom feed of a user’s blog using the Zend_Feed component of the Zend Framework. Finally, we will look at microformats and how to consume them using the Operator plug- in for Firefox. We will use the rel-tag microformat on the tagging system we create, as well as extend user accounts to allow a public profile. We will then display the created profile using the hCard microformat, allowing contact details to be easily exported to other programs. 335 CHAPTER 10 9063Ch10CMP2 11/11/07 5:18 PM Page 335 Tags Tags are used as a way to categorize items on a web site. The type of item being tagged can be anything really, such as a news article, an image, a product, or a link. By assigning a series of keywords to that item (that is, tags), it is easy to find related items based on the keyword of choice. Let’s look at a practical example of how tagging could be used. If you were to categorize this book, you could say it is about PHP, Web 2.0, Ajax, and MySQL, among other things. Each of the italicized terms would be perfectly acceptable as tags. Now consider if you had a web site that listed a catalog of books. Each book would have its own relevant set of tags, just like the tags we just mentioned. To find every book that had something to do with PHP (assuming it had been tagged correctly), you could simply search for all books with a tag of PHP. We will be implementing a system like this, but instead of tagging books in a library or catalog, we will be tagging blog posts. Technically speaking, we won’t be tagging posts—we will provide the blog owner with the tools to tag their own posts. Additionally, we will then provide the means to filter posts by one or more tags. We will implement this on a per-user basis by listing each of a user’s unique tags in a list on their pub- lic home page. In Chapter 12, we will extend the tagging functionality we create here to allow users to search for tags. Implementing Tagging To implement a tagging system, we must first create a database table in which to store tags. Since each blog post can have multiple tags, we create a table with two columns: one to indi- cate the post ID and another to store the actual tag, as shown in Listing 10-1. This means if two posts share the same tag, each post will have its own record in this table for that tag. Listing 10-1. Database Table to Store Blog Post Tags In (schema-mysql.sql) create table blog_posts_tags ( post_id bigint unsigned not null, tag varchar(255) not null, primary key (post_id, tag) ) type = InnoDB; Next we must create some tag management functions in the DatabaseObject_BlogPost class. The functions we will create are as follows: • getTags(): Retrieves all tags for a blog post • hasTag(): Checks whether a blog post has the specified tag • addTags(): Adds one or more tags to a blog post • deleteTags(): Deletes one or more tags from a blog post • deleteAllTags(): Deletes all tags from a blog post CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES336 9063Ch10CMP2 11/11/07 5:18 PM Page 336 Listing 10-2 shows the getTags() method, which returns an array of all tags that belong to the loaded post. This code (along with the other four methods) belongs in the BlogPost.php file in the ./include/DatabaseObject directory. Listing 10-2. Retrieving All Tags Belonging to a Post with getTags() (BlogPost.php) isSaved()) return array(); $query = 'select tag from blog_posts_tags where post_id = ?'; // sort tags alphabetically $query .= ' order by lower(tag)'; return $this->_db->fetchCol($query, $this->getId()); } This method starts by ensuring the post has been loaded before trying to retrieve the tags. In the SQL query we sort the retrieved tags alphabetically. In MySQL, this sort is not case- sensitive, but by using the lower() function, tags will be returned correctly in other database servers such as PostgreSQL that are case-sensitive. Next we look at hasTag(), which we use to check whether a blog post has a specific tag. Listing 10-3 shows this method. Listing 10-3. Checking Whether a Post Has a Specific Tag (BlogPost.php) public function hasTag($tag) { if (!$this->isSaved()) return false; $select = $this->_db->select(); $select->from('blog_posts_tags', 'count(*)') ->where('post_id = ?', $this->getId()) ->where('lower(tag) = lower(?)', trim($tag)); return $this->_db->fetchOne($select) > 0; } CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 337 9063Ch10CMP2 11/11/07 5:18 PM Page 337 Here we use the Zend_Db_Select class that we first used in Chapter 8. For short queries such as in Listing 10-2, I tend not to bother using this class, but when the query gets longer as in this listing, it is definitely worth using. This query retrieves the number of rows for the cur- rent blog that have the given tag (this value should be only 1 or 0 since the same tag can’t be used more than once for a post). This method returns true if the count is greater than zero. The next function we’ll look at is addTags(), which is shown in Listing 10-4. This function begins once again by making sure the record is loaded and then continues by cleaning the tags that have been passed in using the $tags argument. This function will accept either a string or an array of strings to use as the tags. Listing 10-4. Adding One or More Tags to a Post Using addTags() (BlogPost.php) public function addTags($tags) { if (!$this->isSaved()) return; if (!is_array($tags)) $tags = array($tags); // first create a clean list of tags $_tags = array(); foreach ($tags as $tag) { $tag = trim($tag); if (strlen($tag) == 0) continue; $_tags[strtolower($tag)] = $tag; } // now insert each into the database, first ensuring // it doesn't already exist for the current post $existingTags = array_map('strtolower', $this->getTags()); foreach ($_tags as $lower => $tag) { if (in_array($lower, $existingTags)) continue; $data = array('post_id' => $this->getId(), 'tag' => $tag); $this->_db->insert('blog_posts_tags', $data); } } As part of this function, we must first ensure that no duplicates have been passed to the function. We are ignoring case in the tags (so AJAX and Ajax would be treated as the same tag). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES338 9063Ch10CMP2 11/11/07 5:18 PM Page 338 Additionally, to ensure that no duplicate tags are inserted, we retrieve all tags using getTags() and then make them all lowercase using array_map(). Finally, each tag is inserted into the database. We could instead use hasTag() to check whether the new tag already exists, but this would result in one lookup query for each tag, whereas doing it this way requires only one lookup query. The next function we implement is deleteTags(), which we use to remove one or more tags from a blog post, as shown in Listing 10-5. Listing 10-5. Deleting One or More Blog Post Tags with deleteTags() (BlogPost.php) public function deleteTags($tags) { if (!$this->isSaved()) return; if (!is_array($tags)) $tags = array($tags); $_tags = array(); foreach ($tags as $tag) { $tag = trim($tag); if (strlen($tag) > 0) $_tags[] = strtolower($tag); } if (count($_tags) == 0) return; $where = array('post_id = ' . $this->getId(), $this->_db->quoteInto('lower(tag) in (?)', $tags)); $this->_db->delete('blog_posts_tags', $where); } Just as when inserting tags, we must clean up the tags that are passed in (which can be either a single tag or an array of tags). Once this has been done, we can use the Zend_Db’s delete() method to remove the matching rows. Finally, we include the deleteAllTags() method, which takes no arguments and removes every tag associated with a single post, as shown in Listing 10-6. This is primarily used in the preDelete() method, which will we update shortly. Listing 10-6. Deleting All of a Post’s Tags (BlogPost.php) public function deleteAllTags() { if (!$this->isSaved()) return; CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 339 9063Ch10CMP2 11/11/07 5:18 PM Page 339 $this->_db->delete('blog_posts_tags', 'post_id = ' . $this->getId()); } // ... other code } ?> As mentioned, we must call this function in the preDelete() method of DatabaseObject_ BlogPost, which is called automatically prior to a blog post being deleted. This is shown in Listing 10-7. We do this so prior to a blog post being deleted, the associated tags are deleted, ensuring that the foreign key constraints don’t prevent the post from being deleted. Listing 10-7. Deleting All Tags for a Post When a Post Is Deleted (BlogPost.php) profile->delete(); $this->deleteAllTags(); return true; } // ... other code } ?> Managing Blog Post Tags The next step in implementing tagging is to add it to the blog manager interface. We will add a simple form to the blog post preview page that lists all existing tags and includes a form to add a tag to the given post. First, we add a new action handler to the BlogmanagerController class to add or remove tags. We call this method tagsAction(), as shown in Listing 10-8. This method expects three items in the HTTP post data: the ID of the of the blog post, the presence of an add or delete variable (defined by the form submit buttons), and the tag being either added or deleted. Listing 10-8. Adding and Removing Tags from Blog Posts (BlogmanagerController.php) getRequest(); $post_id = (int) $request->getPost('id'); $post = new DatabaseObject_BlogPost($this->db); if (!$post->loadForUser($this->identity->user_id, $post_id)) $this->_redirect($this->getUrl()); $tag = $request->getPost('tag'); if ($request->getPost('add')) { $post->addTags($tag); $this->messenger->addMessage('Tag added to post'); } else if ($request->getPost('delete')) { $post->deleteTags($tag); $this->messenger->addMessage('Tag removed from post'); } $this->_redirect($this->getUrl('preview') . '?id=' . $post->getId()); } } ?> After a tag is added or removed, an appropriate message is written to the flash messenger, and then the user is redirected back to the preview page. Next we must list the existing tags in the preview.tpl template (found in ./templates/ blogmanager), as well as the form used to add a new tag. This is shown in Listing 10-9, fitting in between the post status and its date and time. Listing 10-9. Showing the Tags on the Blog Post Preview Page (preview.tpl)
              Tags
                {foreach from=$post->getTags() item=tag}
              • {$tag|escape}
                CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 341 9063Ch10CMP2 11/11/07 5:18 PM Page 341
              • {foreachelse}
              • No tags found
              • {/foreach}
              Finally, in order to display these tags in a user-friendly manner, we add some extra styles to the site CSS file. By using display : inline, the list items are shown horizontally instead of vertically. Listing 10-10 shows the new styles added to styles.css. Listing 10-10. Displaying the Tag Management Area in a User-Friendly Manner (styles.css) #preview-tags { background : #f7f7f7; padding : 5px; } #preview-tags input { font-size : 0.95em; } #preview-tags a { font-size : 0.95em; } #preview-tags ul { margin : 0; padding : 0; } #preview-tags li { margin : 0; padding : 0 5px; display : inline; } CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES342 9063Ch10CMP2 11/11/07 5:18 PM Page 342 #preview-tags form, #preview-tags div { display : inline; } To generate valid XHTML, form elements cannot exist directly inside a
              tag, which is why we wrapped them in
              tags in Listing 10-9. Because we want all existing tags to appear next to each other, we must change the form and div elements to display inline instead of block. Figure 10-1 shows what the preview page looks like now with the tags displayed. It is very straightforward for a user to add or remove tags. Figure 10-1. Managing tags for a blog post Note that you could use Ajax to control this form, but in all honesty, it won’t make much difference to the user’s experience at all; you shouldn’t necessarily use Ajax just for the sake of it if it isn’t really needed. If instead you wanted more advanced functionality (such as allowing the user to order tags themselves rather than alphabetically), then you may instead choose to use Ajax for adding, removing, and reordering tags. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 343 9063Ch10CMP2 11/11/07 5:18 PM Page 343 Displaying a User’s Tags on Their Blog The next step in implementing a tagging system is to display the tags to the people who use the site. We will do this simply by listing all tags in the side column with a count of the number of posts that use that tag in the blog. When one of these tags is clicked, the user will be taken to a page listing all posts with that tag. The URL of this page will be in the format http://phpweb20/user/username/tag/tagname. ■Note Such a page is called a tag space.The microformats rel-tag specification (http://microformats. org/wiki/rel-tag#Tag_Spaces) defines a tag space as a well-defined URI from which an embedded tag can be mechanically extracted. Specifically, the last segment of a path (after the final slash) denotes that tag (not taking into account any URL parameters or anchors). So, in the case of the URL mentioned earlier, tagname is the last segment and therefore denotes the tag of that tag space. We will look at the rel-tag microformat later in this chapter. To generate the list of tags and the number of posts that have that tag, we must write another new function for DatabaseObject_BlogPost, which we call GetTagSummary(). To retrieve the number of posts for each tag, we must use the following SQL statement: SELECT count(*) as count, t.tag FROM blog_posts_tags t INNER JOIN blog_posts p ON p.post_id = t.post_id WHERE p.user_id = [user id] AND p.status = 'L' GROUP BY t.tag The only problem with this query is that it differentiates between uppercase and lower- case versions of the same tag, whereas we don’t want it to do so. To deal with this, we add some extra processing to GetTagSummary(). Listing 10-11 shows the full function to go in ./include/DatabaseObject/BlogPost.php. Listing 10-11. Retrieving a Summary of All Tags for a Single User (BlogPost.php) select(); $select->from(array('t' => 'blog_posts_tags'), array('count(*) as count', 't.tag')) ->joinInner(array('p' => 'blog_posts'), 'p.post_id = t.post_id', CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES344 9063Ch10CMP2 11/11/07 5:18 PM Page 344 array()) ->where('p.user_id = ?', $user_id) ->where('p.status = ?', self::STATUS_LIVE) ->group('t.tag'); $result = $db->query($select); $tags = $result->fetchAll(); $summary = array(); foreach ($tags as $tag) { $_tag = strtolower($tag['tag']); if (array_key_exists($_tag, $summary)) $summary[$_tag]['count'] += $tag['count']; else $summary[$_tag] = $tag; } return $summary; } } ?> Next we write a Smarty plug-in that calls this function, just as we have done previously when listing the monthly blog archive. This works almost identically, except it returns a sum- mary of tags rather than months. Listing 10-12 shows the code for this plug-in, which we can then access in templates using {get_tag_summary}. Listing 10-12. A Smarty Plug-in Used to Retrieve a User’s Tag Summary (function.get_tag_ summary.php) 0) $smarty->assign($params['assign'], $summary); } ?> CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 345 9063Ch10CMP2 11/11/07 5:18 PM Page 345 The next step is to create a template that calls this plug-in. Since we already created a left-column.tpl template in which to display the monthly archive, we will now create a tem- plate called right-column.tpl to hold the tags. Obviously you can swap these around if you prefer. Listing 10-13 shows the contents of right-column.tpl, which we store in ./templates/user/lib. Listing 10-13. Displaying the Summary of Tags (right-column.tpl) {get_tag_summary user_id=$user->getId() assign=summary} {if $summary|@count > 0}

              {$user->username|escape}'s Tags

                {foreach from=$summary item=tag}
              • {$tag.tag|escape} ({$tag.count} post{if $tag.count != 1}s{/if})
              • {/foreach}
              {/if} Finally, we must include this template in the appropriate places by specifying the right- column attribute when including footer.tpl. In the index.tpl, archive.tpl and view.tpl templates in ./templates/user, we change the last line, as shown in Listing 10-14. Listing 10-14. Including the right-column.tpl Template As Required (index.tpl, archive.tpl, and view.tpl) {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl' rightcolumn='user/lib/right-column.tpl'} Figure 10-2 shows how the user’s blog now looks with tags being displayed on the right side. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES346 9063Ch10CMP2 11/11/07 5:18 PM Page 346 Figure 10-2. Displaying a user’s tags, each linked to their relevant tag space Displaying a Tag Space As mentioned, the URLs we created for tags are known as a tag space. We must now write a new action handler to output the tag space. This is simply a matter of extending the routing capabilities of Zend_Controller_Front once again and then displaying a list of posts based on the specified tags. Retrieving Posts Based on a Tag First, we must extend the capabilities of DatabaseObject_BlogPost::GetPosts() to allow us to filter posts by the specified tag. To do this, we modify the _GetBaseQuery() function to gener- ate the appropriate SQL. Listing 10-15 shows the changes we make to _GetBaseQuery() in BlogPost.php. After these changes have been applied, we can simply pass the tag parameter in the options array for GetOptions() (as well as the other functions that also use _GetBaseQuery()). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 347 9063Ch10CMP2 11/11/07 5:18 PM Page 347 Listing 10-15. Modifying _GetBaseQuery() to Join Against the Tags Table (BlogPost.php) array(), 'public_only' => false, 'status' => '', 'tag' => '', 'from' => '', 'to' => '' ); // ... other code $options['tag'] = trim($options['tag']); if (strlen($options['tag']) > 0) { $select->joinInner(array('t' => 'blog_posts_tags'), 't.post_id = p.post_id', array()) ->where('lower(t.tag) = lower(?)', $options['tag']); } return $select; } // ... other code } ?> Routing Requests to the Tag Space Just as we did in Chapter 9 when setting up the user’s home page, we must now add a new route to Zend_Controller_Front so requests to http://phpweb20/user/username/tag/tagname reach the new action handler we will write shortly. Listing 10-16 shows the new route we add to the index.php bootstrap file. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES348 9063Ch10CMP2 11/11/07 5:18 PM Page 348 Listing 10-16. Adding a New Route for Tag Spaces (index.php) 'user', 'action' => 'tag')); $controller->getRouter()->addRoute('tagspace', $route); $controller->dispatch(); ?> Handling Requests to the Tag Space The next step is to write a new action handler called tagAction() for the UserController class, which is where requests matching the previous route are directed. Just like when we implemented the archiveAction() in the same file, we use $request->getUserParam() to retrieve the value from the URL. In this case, the value is called tag, meaning we use $request->getUserParam('tag') to retrieve the requested tag. We can use archiveAction() as a basis for the tagAction() function, exchanging the requested dates for the request tag. Additionally, we add a check for an empty tag that results in redirecting to the user’s home page. Listing 10-17 shows this method, which belongs in the UserController.php file in the ./include/Controllers directory. Listing 10-17. Retrieving All Posts for the Specified Tag (UserController.php) getRequest(); $tag = trim($request->getUserParam('tag')); if (strlen($tag) == 0) { $this->_redirect($this->getCustomUrl( array('username' => $this->user->username, 'action' => 'index'), 'user' )); } CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 349 9063Ch10CMP2 11/11/07 5:18 PM Page 349 $options = array( 'user_id' => $this->user->getId(), 'tag' => $tag, 'status' => DatabaseObject_BlogPost::STATUS_LIVE, 'order' => 'p.ts_created desc' ); $posts = DatabaseObject_BlogPost::GetPosts($this->db, $options); $this->breadcrumbs->addStep('Tag: ' . $tag); $this->view->tag = $tag; $this->view->posts = $posts; } // ... other code } ?> Outputting the Tag Space The final step in creating the tag space is to output the matching posts. To do this, we create a new template called tag.tpl for which we use the archive.tpl template as a basis. In fact, this template is identical except for the message displayed if no matching posts are found. Listing 10-18 shows tag.tpl, which is stored in the ./templates/user directory. Listing 10-18. Displaying All Posts for a Single Tag (tag.tpl) {include file='header.tpl'} {if $posts|@count == 0}

              No blog posts were found for this tag.

              {else} {foreach from=$posts item=post name=posts key=post_id} {include file='user/lib/blog-post-summary.tpl' post=$post} {if $smarty.foreach.posts.last} {assign var=date value=$post->ts_created} {/if} {/foreach} {/if} {include file='footer.tpl' leftcolumn='user/lib/left-column.tpl' rightcolumn='user/lib/right-column.tpl'} CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES350 9063Ch10CMP2 11/11/07 5:18 PM Page 350 Displaying Tags on Each Post The final step in implementing the tagging system on user blogs is to display the tags on each post. There are no significant code changes required because we already implemented the getTags() method in DatabaseObject_BlogPost earlier this chapter. All we need to do is to call this function in the view.tpl template and loop over each tag just as we did in the blog manager. For each of the tags associated with the post, we link to the relevant tag space. Listing 10-19 shows the additions we make to view.tpl in the ./templates/user directory, including a simple little Smarty trick to place a comma at the end of each tag except for the last. This is achieved by the checking whether the current iteration is the last of the {foreach} loop. This can be checked using $smarty.foreach.loopname.last, where loopname is the value of the name argument in the {foreach} tag. Listing 10-19. Outputting Each of a Post’s Tags and Linking Back to the Tag Space (view.tpl) {include file='header.tpl'}
              Tags: {foreach from=$post->getTags() item=tag name=tags} {$tag}{if !$smarty.foreach.tags.last},{/if} {foreachelse} (none) {/foreach}
              Web Feeds A web feed is a stream of a web site’s content in a format (typically XML) that can be easily interpreted by other programs. The feed will usually contain a summary of recent items (such as news articles or, in our case, blog posts) with a link to a more detailed version of the item. By providing one or more feeds, a web site owner can syndicate their content so others can easily access without needing to “scrape” the content from the web site HTML (which can be slow, difficult, susceptible to breaking, and possibly illegal). Modern browsers have the ability to save and update feeds, meaning users can easily sub- scribe to their favorite feeds and be notified by their browser when the content is updated. Feeds can also be used in other applications, such as podcasts in Apple’s iTunes. If you subscribe to a podcast, iTunes will automatically download any new episodes that are pub- lished (according to the data contained in the podcast web feed). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 351 9063Ch10CMP2 11/11/07 5:18 PM Page 351 Data Formats for Web Feeds There are several data formats that can be used for web feeds. The most popular of these are RSS and Atom, which both use XML. Really Simple Syndication (RSS) is arguably the most widely used format for web feeds. Atom, on the other hand, was born out of the shortcomings of RSS and aims to address some of the problems with RSS. For example, Atom allows the developer to indicate exactly what kind of data is being included in the payload, whether it is plain text, HTML, or binary data (included using Base64 encoding). This is a significant improvement since people who consume RSS feeds may not know exactly how to treat the data. Some feeds will include HTML tags in the feed data, while others won’t. An RSS feed may contain either (or both) of the following two lines: Some plain text Some HTML text Using Atom, the type can be explicitly set, allowing the consumer of the feed to decide how to present the data: Some plain text Some HTML text We will use the Zend_Feed component of the Zend Framework to create an Atom feed for each user in our system who has a blog. Note that we will not be concerning ourselves with the specific formats for Atom; we will allow Zend_Feed to take care of this for us. ■Note Zend_Feed supports both RSS and Atom, so if you prefer to use RSS, the changes required to your PHP code will be minimal, as you will shortly see. Creating an Atom Feed with Zend_Feed It is relatively straightforward to create a web feed using Zend_Feed. Although we will imple- ment this shortly, the general process is as follows. The first step is to build an array of the data that will form the web feed. There is a specifi- cation of how this array should be structured in the Zend Framework manual at http:// framework.zend.com/manual/en/zend.feed.importing.html. The next step is to call Zend_Feed::importArray() to create the actual feed. Other meth- ods are available for creating feeds (such as using another feed). The first argument to this method is the array to use to build the feed, while the second argument indicates the type of feed to build. In our case, we will pass atom as the second argument. To create an RSS feed, this value would be rss. Finally, we call the send() method on the object returned from importArray(), which will send the appropriate headers (such as Content-type: application/atom+xml) and then output the feed. You could instead call saveXml() to write the XML to a variable rather than calling send() (such as if you wanted to write it to a file or output it with different headers). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES352 9063Ch10CMP2 11/11/07 5:18 PM Page 352 Adding the Feed to UserController To create an Atom feed of a user’s article, the process is to create a function very similar to the indexAction() function of UserController.php. We will call this new function feedAction(), also stored in the same file. The difference between feedAction() and indexAction() is that feedAction() loops over the returned data to build an array (which we call $feedData) to pass to Zend_Feed, while indexAction() simply passes the returned feeds to the template. Listing 10-20 shows the first part of the code for feedAction(), which retrieves the ten most recent posts from the database for the user. Just like with the user’s normal blog index, you may want to adjust this number. Listing 10-20. Retrieving the Most Recent Posts from the Database (UserController.php) $this->user->getId(), 'status' => DatabaseObject_BlogPost::STATUS_LIVE, 'limit' => 10, 'order' => 'p.ts_created desc' ); $recentPosts = DatabaseObject_BlogPost::GetPosts($this->db, $options); Next we create the $feedData array, as shown in Listing 10-21. This is the data that describes the feed. That is, it sets the feed title, its base URL, and its character set. Additionally, we initialize the entries array item, which we will populate shortly. Note that we also generate the base URL based on the currently requested domain, since the getUrl() methods we have implemented for users and blog posts generate only local URLs. Listing 10-21. Describing the Atom Feed (UserController.php) // base URL for generated links $domain = 'http://' . $this->getRequest()->getServer('HTTP_HOST'); // url for web feed $url = $this->getCustomUrl( array('username' => $this->user->username, 'action' => 'index'), 'user' ); CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 353 9063Ch10CMP2 11/11/07 5:18 PM Page 353 $feedData = array( 'title' => sprintf("%s's Blog", $this->user->username), 'link' => $domain . $url, 'charset' => 'UTF-8', 'entries' => array() ); ■Note I have hard-coded the HTTP scheme to the generated URL given previously in an effort not to get bogged down in the little details. If this feed is accessed using HTTPS, then the generated URL would be incorrect (you can check whether $this->getRequest()->getServer('HTTPS') == 'on'). You may want to use a different method to generate the domain, such as specifying it in the application configuration. Next we must populate the $feedData['entries'] array, which is what holds the informa- tion about each individual blog post. We populate this array with the posts we retrieved in Listing 10-20. Listing 10-22 shows the code we use to loop over the blogs and build the entries array. Additionally, we retrieve the tags for each post and add them to the feed also. Note that this code calls the getTeaser() method on the blog post that we defined in Chapter 8. Listing 10-22. Creating the Feed Entries by Looping Over the Posts (UserController.php) // build feed entries based on returned posts foreach ($recentPosts as $post) { $url = $this->getCustomUrl( array('username' => $this->user->username, 'url' => $post->url), 'post' ); $entry = array( 'title' => $post->profile->title, 'link' => $domain . $url, 'description' => $post->getTeaser(200), 'lastUpdate' => $post->ts_created, 'category' => array() ); // attach tags to each entry foreach ($post->getTags() as $tag) { $entry['category'][] = array('term' => $tag); } $feedData['entries'][] = $entry; } CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES354 9063Ch10CMP2 11/11/07 5:18 PM Page 354 Finally, we can create the feed by passing the $feedData array to Zend_Feed::importArray(). After the feed has been created, we can output it using the feed’s send() method, as shown in Listing 10-23. Note that we must also disable Zend_Controller autorendering since we are not outputting using a template in this action handler. Listing 10-23. Creating the Feed and Sending It to the Browser (UserController.php) // create feed based on created data $feed = Zend_Feed::importArray($feedData, 'atom'); // disable auto-rendering since we're outputting an image $this->_helper->viewRenderer->setNoRender(); // output the feed to the browser $feed->send(); } // ... other code } ?> ■Tip As an exercise, try extending feedAction() to be able to provide a separate feed for each tag. That is, so if you went to http://phpweb20/user/username/feed/php, the resulting feed would include only those items tagged with php.You may need to add a new route to achieve this, as well as passing the tag parameter to GetPosts() accordingly. Remember also to change the title of the feed to reflect that it is showing posts only for a specific tag. Linking to Your Feed The next step is to provide links to the feeds just created. Where you add these links is entirely up to you; however, we are going to link to the feed from a user’s home page. There are two ways we do this: •By providing a normal HTML hyperlink () to the feed so the user can see it •By using the HTML tag to tell the browser a web feed is present Since the tag belongs in the portion of an HTML page, we must add this link to header.tpl. We don’t want this included on every page in the site, so we make it dependent on the URL and title of the feed being present. First we must change index.tpl (in ./templates/user) so it specifies the $feedUrl and $feedTitle variables, as shown in Listing 10-24. These are variables we will check for in the site header template. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 355 9063Ch10CMP2 11/11/07 5:18 PM Page 355 Listing 10-24. Linking to a User’s Atom Feed from Their Home Page (index.tpl) {capture assign='url'}{geturl route='user' username=$user->username action='feed'}{/capture} {include file='header.tpl' feedTitle="%s's Blog"|sprintf:$user->username feedUrl=$url} Next we check for the presence of $feedUrl and $feedTitle in header.tpl (in ./templates) and output the tag accordingly, as shown in Listing 10-25. Listing 10-25. Adding the Ability to Include Feed Details to the Page Template (header.tpl) {if $feedUrl|strlen > 0 && $feedTitle|strlen > 0} {/if} We can now use the $feedTitle and $feedUrl variables to include a feed icon next to the page title, also in header.tpl. Icons for identifying web feeds can be downloaded from http://www.feedicons.com. From the downloadable archive of sample images, I have copied the feed-icon-14x14.png file to the ./htdocs/images directory. Listing 10-26 shows the changes we make to the page title to include a link to the web feed. Listing 10-26. Linking to the Web Feed Using a Hyperlink (header.tpl)

              {$title|escape} {if $feedUrl|strlen > 0 && $feedTitle|strlen > 0} {$feedTitle|escape} {/if}

              CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES356 9063Ch10CMP2 11/11/07 5:18 PM Page 356 If you were to now visit the user’s home page (http://phpweb20/user/username) in Inter- net Explorer 7, you would see the Web Feeds icon highlighted, allowing you to easily subscribe to the feed, as well as the feed icon next to the page title, as shown in Figure 10-3. Figure 10-3. Internet Explorer 7 automatically detects web feeds found on a page. Similarly, Firefox displays the Web Feeds icon in the address bar when a feed is found. Other Feed Options It is possible to include a lot of different data in your feeds, depending on what you want to make available to subscribers. For instance, we specified only the description parameter; if you wanted, you could also include the full article. However, doing so may cause users to stop visiting your site directly. As mentioned, the Zend Framework manual lists all the options you can include in the array for Zend_Feed::importArray(); you can find it at http://framework.zend.com/manual/en/ zend.feed.importing.html. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 357 9063Ch10CMP2 11/11/07 5:18 PM Page 357 Microformats Microformats are a series of specifications for adding a consistent structure to certain kinds of data that appear on web pages. For example, whenever you needed to list the contact details for a person on a page in your web site (such as their name, e-mail address, and phone num- ber), you would structure the HTML code used to output these contact details according to the appropriate microformat. In this particular case (of displaying contact details), you would use the hCard microfor- mat (the microformats adaptation of the vCard standard). There are several published microformats (see http://microformats.org/wiki/ for a more comprehensive list) that can be used: • hCard. Used for representing people or organizations (based on the vCard standard) • hCalendar. Used for representing events and calendars (based on the iCalendar standard) • hAtom. Used to represent data just as an Atom feed would Although this may give the impression of being restrictive in how you structure your code, it is in fact not restrictive at all. Microformats are used by applying certain class names or HTML attributes to the HTML code you are already creating. An Example of Using Microformats To demonstrate this, I’ll use hCard as an example. If I wanted to list my contact details without using microformats on a web page, I might use the HTML snippet in Listing 10-27. Figure 10-4 after the listing shows how this HTML would be rendered in Firefox. Listing 10-27. Showing Contact Details on a Web Page Without Using Microformats (listing-10-27.html) Quentin Zervaas
              Email: foo@example.com
              Phone: (123) 1234-5678 Technically speaking, although we want the name to stand out from the e-mail address and phone number, we shouldn’t necessarily be using to do so. Good markup prac- tice would have you label each element in the address details and apply formatting in CSS accordingly. This is where microformats step in. To make the contact details use the hCard microfor- mat, it is simply a matter of adding structure and applying the correct class names. You can find the hCard specification at http://microformats.org/wiki/hcard (although you may find the guide at http://microformatique.com/?page_id=134 easier to understand). According to this document, it would have us change the HTML in Listing 10-27 to that of Listing 10-28. I have included the full HTML document in this listing, including the CSS required to make this HTML render the same as Listing 10-27. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES358 9063Ch10CMP2 11/11/07 5:18 PM Page 358 Figure 10-4. Rendering the HTML code from Listing 10-27 Listing 10-28. Using the hCard Microformat to Mark Up a Person’s Contact Details (listing-10-28.html) My Contact Details
              Quentin Zervaas
              Phone: (123) 1234-5678
              CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 359 9063Ch10CMP2 11/11/07 5:18 PM Page 359 ■Note The actual HTML tags we used in this example are not important—it’s the names of the classes and where the classes are applied that is important. Although many more options are available in the hCard microformat, this is still a com- plete and working example. The code begins by using the vcard class on the root element of the contact details (that is, the element that wraps the contact information). This is to indicate the remainder of the details are contained within this element. Next we use the fn property, which is the only required property of hCard. This stands for formatted name and usually contains the person’s first and last name. Following this, we spec- ify the email and phone properties accordingly. Note that we applied the email property to a hyperlink. There is no required order for these parameters; you could list the value for fn last if you wanted. ■Caution If you choose to include your e-mail address in a published hCard, you are making it easy for the e-mail address to be spammed, since the e-mail value must comply with §3.3.2 of RFC 2426 (available from http://www.ietf.org/rfc/rfc2426.txt). Unless your hCard is available only to trusted users, a better option may be not to include the e-mail address at all. Why Use Microformats? It may seem as though from the previous example that we’re not actually doing anything dif- ferently than what we would normally. Indeed, this is true, except by using microformats we are forced to name particular elements in a certain way. This provides a uniformity between all sites that use microformats. It is fair to say that an extremely large majority of web users will have no idea you are using microformats, because currently it doesn’t actually change their experience in any way. However, if you make a conscious effort to use microformats wherever you can, already you are forcing yourself to write clean and consistent code. Although I am only speculating, I believe as the uptake of microformats continues and its popularity amongst web developers increases, it will become a crucial and widely used tool by end users, just as the popularity of RSS and Atom feeds has grown in the past few years. All major browsers now have built-in web feed readers (Microsoft has put an emphasis on web feeds with the release of Internet Explorer 7 and Windows Vista in the past year). It is highly possible that in upcoming releases of web browsers that microformat readers will be integrated. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES360 9063Ch10CMP2 11/11/07 5:18 PM Page 360 The Firefox Operator Plug-In A plug-in for Firefox has been developed that is specifically designed to read microformats on web pages and use the data accordingly. Operator—developed by Michael Kaply (http://www. kaply.com/weblog)—will automatically detect all microformatted data on a page and make various actions available within your browser. You can download Operator from the Firefox Add-Ons site at http://addons.mozilla.org/en-US/firefox/addon/4106. Some of the functions it provides are as follows: • Contact details. It finds all contacts on a page (by finding data using the hCard specifi- cation we just looked at). • Events. Any events on a page marked up using hCalendar will be found, allowing you to easily add them to your Google Calendar. We will use the hCalendar microformat in Chapter 13. • Tag spaces. Earlier this chapter we looked at tag spaces. Shortly we will look at how to link to tag spaces with the rel-tag microformat. Operator will find all tag spaces speci- fied on a page. • Locations. Any geographical information using the GEO microformat will be found, pro- viding links to mapping services such as Google Maps. We will use GEO in Chapter 13. Figure 10-5 shows the Operator plug-in in action on the hCard example we created in Listing 10-29. The contact details can easily be exported to your computer’s address book. Figure 10-5. Using Operator to capture contact details in Firefox CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 361 9063Ch10CMP2 11/11/07 5:18 PM Page 361 Although still in its early days, Operator allows you to customize to a certain extent which actions will be performed when microformatted data is selected. Microformatting Your Tags The rel-tag microformat is used to apply a tag to the current page, simply by including the rel="tag" attribute within a hyperlink. Specifically, we apply this attribute to hyperlinks that link to the relevant tag space for the page. The HTML 4.01 specification (http://www.w3.org/TR/html401/struct/links.html#adef-rel) defines the rel attribute as “describing the relationship from the current document to the anchor specified by the href attribute.” To apply this to the tagging system we created earlier this chapter, we make this slight modification to the links in the view.tpl template in ./templates/user. If you refer to the code we developed in Listing 10-19, we now add the rel attribute to these links as in Listing 10-29. Listing 10-29. Defining the Tag Space by Using the rel-tag Microformat (view.tpl) {include file='header.tpl'}
              Tags: {foreach from=$post->getTags() item=tag name=tags} {if !$smarty.foreach.tags.last},{/if} {foreachelse} (none) {/foreach}
              Importantly, though, we do not use rel-tag for the navigation on the right where we out- put all of a user’s tags. This is because these links do not necessarily reflect the content of the current page, whereas rel-tag is used to define the tag space of the current page. Figure 10-6 shows how the Operator plug-in for Firefox detects the tag spaces and makes various options available for these tags. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES362 9063Ch10CMP2 11/11/07 5:18 PM Page 362 Figure 10-6. Using Operator to capture contact details in Firefox Allowing Users to Create a Public Profile Let’s now look at a more concrete example of using microformats. Once again we will use the hCard microformat (we will look at more microformats in later chapters), but we will now cater to a wider range of field types, as well as showing a variable number of fields depending on the data provided by the user. Integrating hCard into our web application essentially involves two steps: modifying the user account section to allow users to create their public profile and outputting their public profile on their home page. Allowing Users to Create a Public Profile Since the user system we created is somewhat flexible, we can easily add new properties to user accounts. We are going add several fields to the “Your Account Details” page available to users, allowing them to enter data that is publicly available for all users to see. We will allow users to enter the following fields: • First name and last name. If they don’t provide these, we will simply use their user- name instead. • Phone numbers. We will allow users to enter their home phone number and their work phone number. • E-mail address. Even though user accounts already have an e-mail address, we’ll give users the option to display a different address. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 363 9063Ch10CMP2 11/11/07 5:19 PM Page 363 Typically people will be somewhat apprehensive about providing this sort of data, but we are using this example only to demonstrate various concepts. Figure 10-7 shows how this page will look once we have added the public profile options. Figure 10-7. Allowing users to specify a public profile for their public home page Processing the User Details Form To simplify the implementation of processing the user profile data, I will specify all the avail- able fields in a PHP array. This allows us to loop over the fields in the template, as well as looping over them in the form processor. Additionally, we are simply going to allow free-form fields that the user can enter any con- tent into that they like. That is, we’re not going to check for a valid e-mail address, although you may prefer to do so. Listing 10-30 shows the additions we make to the FormProcessor_UserDetails class (found in ./include/FormProcessor/UserDetails.php). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES364 9063Ch10CMP2 11/11/07 5:19 PM Page 364 Listing 10-30. Processing Changes Made to a User’s Public Profile (UserDetails.php) 'First Name', 'public_last_name' => 'Last Name', 'public_home_phone' => 'Home Phone', 'public_work_phone' => 'Work Phone', 'public_email' => 'Email' ); public function __construct($db, $user_id) { // ... other code foreach ($this->publicProfile as $key => $label) $this->$key = $this->user->profile->$key; } public function process(Zend_Controller_Request_Abstract $request) { // ... other code // process the public profile foreach ($this->publicProfile as $key => $label) { $this->$key = $this->sanitize($request->getPost($key)); $this->user->profile->$key = $this->$key; } // ... other code } } ?> Displaying the User Profile Options Next we must add a new section to the details.tpl template found in ./templates/account. Listing 10-31 shows the additions we make to this file. Luckily all of the fields are similar in nature, allowing us to loop over the fields and display text input for each field. If you wanted to accept other types of data (such as a user’s date of birth), you would have to modify this accordingly. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 365 9063Ch10CMP2 11/11/07 5:19 PM Page 365 Listing 10-31. Displaying the Public Profile Options in Account Management (details.tpl)
              Update Your Details
              Account Settings
              Public Profile {foreach from=$fp->publicProfile key='key' item='label'}
              {include file='lib/error.tpl' error=$fp->getError($key)}
              {/foreach}
              Displaying a User’s Profile Now that a user has the ability to create a public profile through their account management tools, we can change the output of their public home page to display their profile. We will cre- ate a new box in the left column of their public page to include their profile. Listing 10-32 shows the changes we begin with in the left-column.tpl template from ./templates/user/lib. Since we are displaying it in the side column of our site, we must use the .box class; however, since it is also using hCard, we must apply the .vcard class. All of the code we are now adding goes at the start of this file (that is, before the blog monthly summary). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES366 9063Ch10CMP2 11/11/07 5:19 PM Page 366 Listing 10-32. Beginning a New hCard (left-column.tpl)

              {$user->username|escape}'s Profile

              Next we output the user’s name. We output their first and last name if available; other- wise, we fall back to simply showing their username (which we know no matter what). As you can see in Listing 10-33, we use the user’s name or username as the mandatory fn property. To specify the first or last name, we must also use the n property and then use the given-name and family-name subproperties, respectively. Alternatively, if we fall back to using the username, then we apply the username property. Listing 10-33. Displaying the User’s First Name and Last Name or Their Username (left-column.tpl) {if $user->profile->public_first_name|strlen > 0 || $user->profile->public_last_name|strlen > 0}
              {if $user->profile->public_first_name|strlen > 0} {$user->profile->public_first_name|escape} {/if} {if $user->profile->public_last_name|strlen > 0} {$user->profile->public_last_name|escape} {/if}
              {else}
              {$user->username}
              {/if} Next we output the user’s e-mail address, as shown in Listing 10-34. CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 367 9063Ch10CMP2 11/11/07 5:19 PM Page 367 Listing 10-34. Displaying the User’s E-mail Address (left-column.tpl) {if $user->profile->public_email|strlen > 0} {/if} Next we output the user’s home and work phone if they are available, as shown in Listing 10-35. Note that in the earlier example we looked at we simply specified a single value directly in the tel property. Now that we have two different types of phone numbers available, we can specify the type of phone number accordingly by using the type and value subproperties. The other text within tel but not in these subproperties is ignored. Listing 10-35. Displaying the User’s Home and Work Phone Numbers (left-column.tpl) {if $user->profile->public_home_phone|strlen > 0}
              Phone (Home): {$user->profile->public_home_phone|escape}
              {/if} {if $user->profile->public_work_phone|strlen > 0}
              Phone (Work): {$user->profile->public_work_phone|escape}
              {/if}
              If you look at Figure 10-8, you can see how the public profile is displayed, as well as how the Windows Address Book sees the data after it has been exported using Operator. This screenshot was taken using my account’s public home page (http://phpweb20/user/qz). CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES368 9063Ch10CMP2 11/11/07 5:19 PM Page 368 CHAPTER 10 ■ IMPLEMENTING WEB 2.0 FEATURES 369 Figure 10-8. The public profile as displayed in Windows Address Book Summary In this chapter, we looked at some of the other web development techniques that are used to develop Web 2.0 applications. Specifically, we implemented a tagging system, we provided web feeds of our data using Atom, and we used microformats to mark up data on our web site in a standardized manner. We implemented the rel-tag and hCard microformats on our web site, first by using rel- tag with the tags system we created at the start of the chapter and then by allowing users to create a public profile. In the coming chapters, we will look at other available microformats, including geo and hCalendar. In the next chapter, we will implement a dynamic image gallery on our blog. 9063Ch10CMP2 11/11/07 5:19 PM Page 369 9063Ch10CMP2 11/11/07 5:19 PM Page 370 A Dynamic Image Gallery So far, the web application we have developed restricts users to only publishing text-based information in their blogs. While we have allowed users a degree of control by permitting a limited subset of HTML to be used (including the use of the tag), users are still unable to upload their own images. In this chapter, we will extend the functionality of our blogging system to allow users to upload one or more photos to each of their blog posts. While this may sound like a fairly trivial process, there are a number of different issues to consider, such as these: • Storage of images. We must store the images on the server and link them to blog posts. • Sending images to browsers. When a user views posts with images in them, we must send the images. This includes dealing with correct MIME headers as well as caching images in the user’s browser. • Dynamic image sizing. Since users will upload different sizes and types of images, we must manipulate the images for a consistent layout. We will simplify the process of image publishing by predetermining the layout of images within a blog post, although users will also have the ability to link to their images via the WYSIWYG editor we implemented in Chapter 7. One extra feature we will add will allow users to change the order in which their photos appear on a page. We will use Scriptaculous to provide a simple interface for reordering images, and we will use Ajax to save the order of the images. This will be similar to the exam- ple in Chapter 5. The steps we will cover in this chapter are as follows: 1. Adding an image-upload form to the blog post preview page. 2. Adding a new controller action to output uploaded images. 3. Displaying images on blog posts. 4. Displaying a thumbnail on the blog index for each post with images. 5. Allowing users to reorder and delete images from each blog post. 371 CHAPTER 11 9063Ch11CMP2 11/15/07 8:13 AM Page 371 ■Note While this chapter deals specifically with images, many of the principles we will look at also apply to general file uploads (after all, an image is a file). The only things that don’t apply to non-image files are resizing the images and displaying them in HTML using the tag. Storing Uploaded Files The first thing we must decide is how we will store files uploaded by users: in the database or on the filesystem. Each method has its own advantages and disadvantages. Here are some of the reasons you might prefer to store files in the database: • Doing so provides easy access to all the image information. When using the filesystem, some of the data will still be stored in the database, meaning there is a slight redun- dancy. Additionally, deleting the image from the database is simply a matter of removing the database record, while on the filesystem you must also delete the image file. It is easier to roll back a failed transaction if you are only using a database. •Keeping backups of your web application is simpler, since you only need to back up the database and no separate uploaded files. Now let’s take a look at why you may prefer to store images using the filesystem: •Cross-platform compatibility is easier to achieve. Since most database servers will use different methods for storing binary data, a separate implementation may be required for each type of database server your application is used on. •It is much simpler to perform filesystem operations on files that are already on the filesystem. For example, if you were to use ImageMagick (a suite of image manipulation tools) to create thumbnails of images, you would find it much simpler to work with files already stored on the filesystem. ■Note We will be using the GD image functions that are built with PHP instead of ImageMagick—I simply used this as an example of filesystem operations that may take place on uploaded files. There are some other considerations we must take into account. For example, we need to store metadata for each of the images. As mentioned earlier, we want to allow users to change the order of images belonging to each blog post (since a blog post may have several images). As such, not only do we need to track which images belong to which blog posts, but we must also track the order of the images. My preferred method is to store all uploaded images on the filesystem, and to also use a database table to store information about the images and to link each image to its blog post. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY372 9063Ch11CMP2 11/15/07 8:13 AM Page 372 ■Note If you prefer to store your images in the database, you shouldn’t have too much trouble extending the SQL we will create here to do so. However, to produce and save the thumbnail images as the way I describe in this chapter, it is likely that you will still need to store some files on the filesystem. Creating the Database Table for Image Data We will first create a table in the database to store information about each uploaded image. This table will hold the filename of the original image as well as a foreign key that links this table to the blog_posts table. The final ranking column will be used to record the order of the images in a blog post. ■Note The name of the ranking column isn’t too important; however, order is a reserved word in SQL, so we cannot use it. The schema for this table, which we call blog_posts_images, is shown in Listing 11-1. This SQL code can be found in the schema-mysql.sql file, and the corresponding PostgreSQL code can be found in schema-pgsql.sql. Listing 11-1. Creating the Database Table Used to Store Image Information (schema-mysql.sql) create table blog_posts_images ( image_id serial not null, filename varchar(255) not null, post_id bigint unsigned not null, ranking int unsigned not null, primary key (image_id), foreign key (post_id) references blog_posts (post_id) ) type = InnoDB; Controlling Uploaded Images with DatabaseObject Next, we will create a child class of DatabaseObject that we will use to manage both database records for uploaded files and the stored files on the filesystem. As noted previously, we will use a database record to store image data and store the file on the filesystem, as this allows us to easily link the image to the correct blog post. It also allows us to store other data with each image if required (such as an original filename or a caption). CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 373 9063Ch11CMP2 11/15/07 8:13 AM Page 373 This child class, called DatabaseObject_BlogPostImage, will write the file to the filesystem upon successful upload, and it will delete the file from the filesystem and the database record from the table if the user chooses to delete the image. For now, we will just create the basic skeleton of the DatabaseObject_BlogPostImage class, as shown in Listing 11-2; we will add more advanced functionality to this class as we continue on in this chapter. This code should be stored in BlogPostImage.php, which resides in the ./include/DatabaseObject class. Listing 11-2. Beginning the Blog Post Image-Management Class (BlogPostImage.php) add('filename'); $this->add('post_id'); $this->add('ranking'); } } ?> At this stage, the key functionality we need to add to this class involves writing the image file to the filesystem and deleting the file when the record is removed. Before we add this func- tionality, however, we will look at how to upload files via HTTP in PHP. Uploading Files Traditionally speaking, HTTP hasn’t been a very good method for uploading files over the Internet. There are several reasons for this: • Unreliable. If a file upload doesn’t complete, it is not possible to resume the upload, meaning large files may never be uploaded. Additionally, some browsers may decide after a prolonged period of time that an error has occurred, typically resulting in an error message being displayed to the user. • Restrictive. While a file is being uploaded, the user cannot navigate away from the page they are uploading to without interrupting the upload. • Cumbersome. Due to security concerns, the capabilities of file-upload forms are some- what restricted. For instance, very few styles can usually be applied to file inputs. Additionally, file inputs allow only single selections, meaning a user cannot choose multiple files at once—if the form allows multiple file inputs, the files must be chosen one at a time. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY374 9063Ch11CMP2 11/15/07 8:13 AM Page 374 • Uninformative. There is no built-in way in HTTP to notify the user of the status of their upload. This means there is no easy way to know how much of the upload is complete, or how much longer it will take. Thankfully the increased speeds of Internet connections over recent years have alleviated some of these problems; however, since HTTP hasn’t changed, these issues still exist. In our web application, we will only be uploading images (not other file types, such as PDF files). Compared to other types of files, images are small. For instance, using the Photo- shop “Save for Web” tool to save a 1024 ✕ 768 pixel JPEG photo will typically result in a file under 100KB. In this section, we will create an image-upload form in our web application, as well as a new form processor to deal with this upload. Setting the Form Encoding To upload files over HTTP, a traditional HTML form is used (that is, using tags), but you must add one extra attribute to this tag: the enctype attribute. This notifies the web server what kind of data the web browser is trying to send. Normally you don’t need to specify this attribute. If it is not specified, the default value of enctype is application/x-www-form-urlencoded. In other words, the following two lines of HTML are equivalent: This indicates to the web server that the browser is sending URL-encoded form data using the HTTP POST method. In order to have the web server recognize uploaded image files, we must specify the enctype as multipart/form-data. In other words, the form will probably be sending multiple types of data: normal URL-encoded form data as well as binary data (such as an image). Adding the Form Let’s now add a new form to the web application that will allow users to upload images to their blog posts once they have been created. We will add the form shown in Listing 11-3 to the preview.tpl file in ./templates/blogmanager. ■Note We could include the image-upload form on the blog post editing page, but uploading files with normal form data can pose new challenges, since if a form error occurs, the user may need to upload the file again. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 375 9063Ch11CMP2 11/15/07 8:13 AM Page 375 Listing 11-3. Creating a File-Upload Form Specifying the Form Encoding Type (preview.tpl)
              Images
              The target script for this form is a new action handler called images in the blogmanager controller. We will create this handler later. We also include the ID of the blog post the image is being uploaded for, so it can be linked to the post. In addition to handling uploads, we will use the images action handler to save changes to the ordering of the images and to delete images. The submit button is named upload so we know that we are handling a file upload when processing this form. By adding some new styles to the site style sheet (in ./htdocs/css/styles.css), we can make this block look like the tag management area that is also on this page. Listing 11-4 shows the CSS we need to add to styles.css, while Figure 11-1 shows how the form looks on the blog post preview page. Listing 11-4. Styling the Image-Management Area of the Blog Post Preview (styles.css) #preview-images { margin : 5px 0; padding : 5px; } #preview-images input { font-size : 0.95em; } CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY376 9063Ch11CMP2 11/15/07 8:13 AM Page 376 Figure 11-1. The image management area for blog posts Specifying the File Input Type The other element in Listing 11-3 that we have not yet discussed is the file input. This is a spe- cial type of form element that allows the user to select a file from their computer for upload. It is typically made up of a text input on the left and a button on the right (used to open the file- selection dialog box). Browsers typically give developers less control over the look and feel of file inputs than for other inputs, as there would be security implications if they did not do this. Here are some of the things you can and can’t do with file inputs (although different browsers will behave slightly differently): •You can observe the onchange event, so you can detect when the user has chosen a file (or removed their selection). •You can retrieve the value of the form element. This does not mean you can read the contents of the selected file—you can simply read the path and/or filename of the file as it is stored on the user’s computer. •You can change the font size and color of the file input element, but you cannot change the text (most browsers will use “Browse…”). CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 377 9063Ch11CMP2 11/15/07 8:13 AM Page 377 •You cannot use a custom image as the browse button, nor can you hide the text input that shows the file path. However, you can manipulate the input by changing its posi- tion in CSS or making it fully transparent (allowing you to add styled buttons behind it). Some web developers have been quite creative in how they style this input in an effort to customize their own site layout fully. We will be using the plain-vanilla version of the file input control, as shown in Listing 11-3. Setting the Maximum File Size The next step is to look at how maximum file upload sizes are specified. You will want to impose some kind of restriction on the maximum size of uploaded files to prevent abuse from users. There are several ways to achieve this, both within the HTML form as well as on the server: • MAX_FILE_SIZE:By including a hidden form element called MAX_FILE_SIZE, you can set the maximum number of bytes in an uploaded file by specifying that value as the form element value. Using this feature is somewhat pointless, since it can easily be fooled by somebody manually manipulating the form data. • post_max_size: This is a php.ini setting that specifies the maximum size POST request data can be. Note that if there are several files being uploaded within a single form, this value applies to the total amount of data being included. In a default PHP installation, this value is set to 8MB (using the value 8M). • upload_max_filesize: This php.ini setting specifies the maximum size for a single file that is uploaded. By default, this value is set to 2MB. In combination with the post_max_size value (of 8M), you could upload three 1.5MB files (since each is below 2MB and the total is approximately 4.5MB), but uploading 10 1MB files would fail since the total would be about 10MB. You should use the post_max_size and upload_max_filesize settings to specify upload limits and ignore the MAX_FILE_SIZE form directive. In addition to these limits, you may also want to impose other artificial limits on users, such as the maximum number of photos per blog post or a total quota for their account. Realistically, you won’t need to make any changes to your configuration to deal with max- imum file sizes. However, if you wanted to allow users to upload other types of files (such as PDF or MP3 files), you would want to increase these configuration settings. ■Note We won’t be implementing any such restrictions in this chapter; however, as an exercise, you may want to add this functionality. Note that if you choose to restrict the number of photos per blog post, the user can get around this limit by simply creating more posts. As such, using an absolute number as a restriction may be a better solution (such as 20MB per user). CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY378 9063Ch11CMP2 11/15/07 8:13 AM Page 378 Handling Uploaded Files As mentioned previously, we must create a new action handler in the BlogmanagerController class to deal with image-handling operations. The different operations that can take place include uploading, reordering, and deleting. At this stage, we will only look at the upload operation. In addition to creating the new action handler, we must also create a new form processor to save the uploaded files as well as to report on any errors that may have occurred. Creating the Blog Manager Action Handler We can use the tagsAction() function we created in Chapter 10 as a basis for the new imagesAction() function. The functionality of these two functions is almost identical: in both we must first load a blog post; next, we must determine the action to take (in this case, it’s whether to upload, reorder, or delete an image; in tagsAction() it was whether to add or delete a tag); finally, we will redirect the browser back to the blog post preview. Listing 11-5 shows the code we will add to the BlogmanagerController.php class (in ./include/Controllers) in order to manage images. For now we will simply include place- holders for the other image operations. Listing 11-5. The Action Handler for Image Management (BlogmanagerController.php) getRequest(); $post_id = (int) $request->getPost('id'); $post = new DatabaseObject_BlogPost($this->db); if (!$post->loadForUser($this->identity->user_id, $post_id)) $this->_redirect($this->getUrl()); if ($request->getPost('upload')) { $fp = new FormProcessor_BlogPostImage($post); if ($fp->process($request)) $this->messenger->addMessage('Image uploaded'); else { foreach ($fp->getErrors() as $error) $this->messenger->addMessage($error); } } else if ($request->getPost('reorder')) { // todo CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 379 9063Ch11CMP2 11/15/07 8:13 AM Page 379 } else if ($request->getPost('delete')) { // todo } $url = $this->getUrl('preview') . '?id=' . $post->getid(); $this->_redirect($url); } } ?> One key aspect of this code is that we now use the flash messenger to hold any error mes- sages that occur in the upload form. This means that if any errors occur (such as a file being too large or a file type being invalid), the error messages will be shown in a message area at the top of the right column of our web application. Showing errors in this manner is slightly different from what we have done so far in this book (previously errors have been shown below the form input related to the error). I have simply done this to show you an alternative way of displaying error messages. Creating the Image-Upload Form Processor In Listing 11-5, we used a class called FormProcessor_BlogPostImage. We will now create this class, which we will use to process the uploaded image. This class has several responsibilities: •Ensuring the upload completed correctly • Checking the type of file that was uploaded and ensuring it is an image •Writing the file to the filesystem and creating the database record using the DatabaseObject_BlogPostImage class we created earlier in this chapter Listing 11-6 shows the constructor for this class, which we will store in a file called BlogPostImage.php in the ./include/FormProcessor directory. The constructor instantiates DatabaseObject_BlogPost and sets the ID of the blog post the image is being uploaded for. Listing 11-6. The Constructor of the Image-Upload Processing Form (BlogPostImage.php) post = $post; // set up the initial values for the new image CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY380 9063Ch11CMP2 11/15/07 8:13 AM Page 380 $this->image = new DatabaseObject_BlogPostImage($post->getDb()); $this->image->post_id = $this->post->getId(); } Next, we will implement the process() method of this class, which will process any uploaded images. As we saw earlier in this chapter, we must specify the multipart/form-data encoding type to upload files using HTML forms. When we set this attribute, PHP will know to create the superglobal array called $_FILES, which stores information about uploaded files (even though the form is submitted using HTTP POST, the image-upload information is stored in $_FILES, not in $_POST). There is one entry in $_FILES for each file that is uploaded, with the array key being the value of the name attribute in the form input (in our case, we used image). Each element in $_FILES is an array consisting of the following elements: • name: The original filename of the uploaded file as it was stored on the client computer (typically not including the path). We will store this value in the database for each uploaded image. • type: The mime type of the uploaded file. For example, if the uploaded file was a PNG image, this would have a value of image/png. Since this is set by the browser, we should not trust this value. In the process() method we will not use this value and instead will verify the type of data manually. • size: The size of the uploaded file in bytes. If you want to impose a restriction on the size of uploaded files (in addition to the PHP configuration settings), you can use this value. • tmp_name: The full path on the server where the uploaded file is stored. This is a tempo- rary location, so you must move or copy the file from this location in order to keep it (we will do this shortly using the move_uploaded_file() function). • error: The error code associated with the uploaded file. There are several different codes that can be set (which we will look at in the following code). We must check this value using the built-in PHP constants and generate an appropriate error message. If the file upload is successful, the value of error will be 0 (which we can check using the constant UPLOAD_ERR_OK). To begin implementing the process() method, the first thing we must do is check for the presence of the uploaded file in the $_FILES superglobal. This is shown in Listing 11-7. Once we know it exists, we can assign it to the $file variable. Listing 11-7. Ensuring the $_FILES Array Is Set Correctly (BlogPostImage.php) public function process(Zend_Controller_Request_Abstract $request) { if (!isset($_FILES['image']) || !is_array($_FILES['image'])) { $this->addError('image', 'Invalid upload data'); return false; } $file = $_FILES['image']; CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 381 9063Ch11CMP2 11/15/07 8:13 AM Page 381 Next, we will check the error code, as set in the error element of $file. If this value is not equal to UPLOAD_ERR_OK, an error has occurred. We will check for each error code explicitly, as this allows us to create a more informative error message for the user. (These codes are docu- mented at http://www.php.net/manual/en/features.file-upload.errors.php.) Listing 11-8 shows the switch() statement in BlogPostImage.php that will check each of the different error codes. Listing 11-8. Checking the Error Code for the Uploaded Image (BlogPostImage.php) switch ($file['error']) { case UPLOAD_ERR_OK: // success break; case UPLOAD_ERR_FORM_SIZE: // only used if MAX_FILE_SIZE specified in form case UPLOAD_ERR_INI_SIZE: $this->addError('image', 'The uploaded file was too large'); break; case UPLOAD_ERR_PARTIAL: $this->addError('image', 'File was only partially uploaded'); break; case UPLOAD_ERR_NO_FILE: $this->addError('image', 'No file was uploaded'); break; case UPLOAD_ERR_NO_TMP_DIR: $this->addError('image', 'Temporary folder not found'); break; case UPLOAD_ERR_CANT_WRITE: $this->addError('image', 'Unable to write file'); break; case UPLOAD_ERR_EXTENSION: $this->addError('image', 'Invalid file extension'); break; default: $this->addError('image', 'Unknown error code'); } if ($this->hasError()) return false; CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY382 9063Ch11CMP2 11/15/07 8:13 AM Page 382 In this code, note that if an error has occurred, we return from the process() function immediately, since there’s nothing else to do. The remainder of the code relies on a file being successfully uploaded. Next, we must ensure that the uploaded file is in fact an image. Since we cannot rely on the mime type specified by the user’s web browser, we must check the data manually. This is fairly straightforward for images, since PHP has the getImageSize() function, which returns an array of information about image files. This function will return false if the file is not an image. The getImageSize() function supports a wide range of image types, but since we only want to allow JPEG, GIF, and PNG files (since these are the three types of files commonly sup- ported in web browsers), we must first check the type of image. The getImageSize() function returns an array: the first and second elements are the width and height of the image, and the third element (index of 2) specifies the image type. Listing 11-9 shows the code we will add to fetch the image information and check its type. We will use built-in constants to check for JPEG, GIF, and PNG images. Listing 11-9. Ensuring the Uploaded File Is a JPEG, GIF,or PNG Image (BlogPostImage.php) $info = getImageSize($file['tmp_name']); if (!$info) { $this->addError('type', 'Uploaded file was not an image'); return false; } switch ($info[2]) { case IMAGETYPE_PNG: case IMAGETYPE_GIF: case IMAGETYPE_JPEG: break; default: $this->addError('type', 'Invalid image type uploaded'); return false; } At this point in the code, we can assume a valid file was uploaded and that it is a JPEG, GIF, or PNG image (it doesn’t matter to us which one). Now we must write the file to the filesystem (it is currently stored in a temporary area) and save the database record. To move the file from the temporary area, we will call the uploadFile() method, which we will implement shortly. Additionally, we will set the filename of the uploaded file and save the database record, as shown in Listing 11-10. Listing 11-10. Saving the Image File and the Database Record (BlogPostImage.php) // if no errors have occurred, save the image if (!$this->hasError()) { $this->image->uploadFile($file['tmp_name']); $this->image->filename = basename($file['name']); $this->image->save(); CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 383 9063Ch11CMP2 11/15/07 8:13 AM Page 383 } return !$this->hasError(); } } ?> ■Note Be sure to use basename() on the value in $file['name'], since this value is supplied by the browser. The basename() method is used to strip out the path from a full filesystem path (so /path/to/ foo.jpg becomes foo.jpg). As mentioned earlier, most browsers will not include the full path, but you should still call basename() just in case. Writing Files to the Filesystem Now that we have completed the action handler and the form processor, we must make the necessary changes to the DatabaseObject_BlogPostImage class to save the uploaded image. There are a number of functions we must write, including the uploadFile() function we briefly looked at in Listing 11-10. The first function we will write is one that returns the path on the filesystem where we will be storing the uploaded images. In Chapter 1 we created a directory called uploaded-files in the ./data directory—this is where the uploaded images will be stored. Listing 11-11 shows GetUploadPath(), a static function we will call to determine where files will be stored. Listing 11-11. Determining the Base Location for Uploaded Files (BlogPostImage.php) paths->data); } } ?> Next, we will write a function to determine the full path where an uploaded file is stored for a particular database record. To simplify this process, rather than storing files with the names they used on the client’s computer, we will store them in the uploaded file directory using their database ID. If we need to refer back to their original filenames, we can get this information from the database record. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY384 9063Ch11CMP2 11/15/07 8:13 AM Page 384 Listing 11-12 shows the getFullpath() function, which returns the full path to the uploaded file. This basically just combines the GetUploadPath() function with the record ID. Listing 11-12. Retrieving the Full Filesystem Path of an Uploaded File (BlogPostImage.php) getId()); } // ... other code } ?> Next, we will implement the uploadFile() function. All this function does is store the temporary path of the uploaded file in anticipation of the save() method being called. When save() is called on a new record of DatabaseObject_BlogPostImage, the preInsert() and postInsert() callbacks will be executed. The copying of the file from its temporary location to its new location will occur on postInsert(). Listing 11-13 shows the code for uploadFile(), which writes the temporary path to an object property for later use. Note that it also does some basic error checking to ensure the temporary file exists and is readable. Listing 11-13. Setting the Location of the Uploaded File so It Can Be Copied Across (BlogPostImage.php) _uploadedFile = $path; } CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 385 9063Ch11CMP2 11/15/07 8:13 AM Page 385 // ... other code } ?> Next, we will implement the preInsert() callback, which is called before the database record is inserted into the database. This function first ensures that the upload location exists and is writable, which will help us solve any permissions errors if the upload area hasn’t been created properly. Then the ranking value for the image is determined, based on the other images that have been uploaded for the blog post. The ranking system simply uses numbers from 1 to N, where N is the number of images for a single post. Since the new image is considered to be the last image for the blog, we can use the SQL max() function to determine its ranking. The only problem with this is that if no images exist for the given blog post, a value of null is returned. To avoid this problem, we will use the coalesce() function, which returns the first non-null value from its arguments. The code for preInsert() is shown in Listing 11-14. Listing 11-14. Ensuring the File Can Be Written, and Determining Its Ranking (BlogPostImage.php) _table, $this->post_id ); $this->ranking = $this->_db->fetchOne($query); return true; } // ... other code ?> CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY386 9063Ch11CMP2 11/15/07 8:13 AM Page 386 Finally, we will implement the postInsert() callback. This is the function responsible for copying the image file from its temporary upload location to the uploaded files area of our web application. We will do this in postInsert() because if any SQL errors occurred before this point, the whole transaction could be easily rolled back, preventing the file from being incorrectly moved into the web application. To move the file, we will use the PHP move_uploaded_file() function. This function is used for security reasons, as it will automatically ensure that the file being moved was in fact uploaded via PHP. This function will return true if the file was successfully moved and false if not. Thus we can use the return value as the postInsert() return value. Remember that returning false from this callback will roll back the database transaction. In other words, if the file could not be copied for some reason, the database record would not be saved. Listing 11-15 shows the postInsert() method, which completes the image-upload func- tionality for the web application. Listing 11-15. Moving the Uploaded File to the Application File Storage Area (BlogPostImage.php) _uploadedFile) > 0) return move_uploaded_file($this->_uploadedFile, $this->getFullPath()); return false; } // ... other code } ?> Once you have added this code, you will be able to upload images to blog posts via the form we added to the post preview page. Currently, though, we haven’t implemented code to display these uploaded images, so to verify that your code is working, you should check that there are records present in the database table by using the following query: mysql> select * from blog_posts_images; You should also check that the file you uploaded is in /var/www/phpweb20/data/uploaded-files. Sending Images Now that users can upload photos to their blog posts, we must display their images both on the blog post preview page and on the actual blog page. Before we do this, however, we must write the code to send the images. We could use the built-in file serving from the web server, CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 387 9063Ch11CMP2 11/15/07 8:13 AM Page 387 but since the original images as well as generated thumbnails will be stored in the application data directory, we will serve these files using PHP code. To begin, we will simply output uploaded images in full, just as they were uploaded. We will build on this functionality later by adding the ability to resize images. ■Note The image resizing we will implement will generate thumbnails on demand. In other words, the first time a thumbnail of a particular size is requested, it will be generated and saved for later reuse. The advan- tage of doing this over creating thumbnails when the image is uploaded is that we can easily choose what size thumbnails we want in the template rather than deciding in the PHP code at upload time. To send blog post images, we will create a new action handler in the UtilityController class we created earlier in this book. Currently this controller is used only for outputting CAPTCHA images, but we will now make it also send blog post images. Listing 11-16 shows the start of the imageAction() method we will add to UtilityController.php. This file can be found in ./include/Controllers. Listing 11-16. Initial Setup of the Image-Output Function (UtilityController.php) getRequest(); $response = $this->getResponse(); $id = (int) $request->getQuery('id'); // disable autorendering since we're outputting an image $this->_helper->viewRenderer->setNoRender(); As in many other action handlers in this book, we begin by retrieving the request object. In this function, we also retrieve the response object because we are going to send some addi- tional HTTP headers. Namely, we are going to send the content-type header (to specify the type of data) and content-length header (to specify the amount of data in bytes). Next, we retrieve the requested image ID from the URL. This means that to request the image with an ID of 123, the URL http://phpweb20/utility/image?id=123 would be used. The next step is to disable the automatic view renderer, since we are outputting an image and not an HTML template. At this point, we will try to load the DatabaseObject_BlogPostImage record specified by the $id variable, as shown in Listing 11-17. If the image cannot be found, we use the $response object to send a 404 header and return. If the image does load, we simply proceed in the func- tion. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY388 9063Ch11CMP2 11/15/07 8:13 AM Page 388 Listing 11-17. Loading the DatabaseObject_BlogPostImage Record (UtilityController.php) $image = new DatabaseObject_BlogPostImage($this->db); if (!$image->load($id)) { // image not found $response->setHttpResponseCode(404); return; } At this point in the function, we can assume that a blog post image has been successfully loaded. As such, we must now determine what type of image it is and send the appropriate content-type header. The getImageSize() function we looked at earlier in this chapter also includes an appropriate header in the mime index of the returned array. In addition to sending this header, we will also send the content-length header. This tells the browser how much data to expect. We can use the PHP filesize() function to determine the value for this (specified in bytes). ■Note Why is the content-length header important? Perhaps you have downloaded a large file in your browser, and the browser was unable to give you an estimate of remaining time. This is because the content-length header was not sent. The browser simply receives data until no more is available— without the header, it is not able to determine how much data is still to come. This is usually more of an issue for larger files. Listing 11-18 shows the remainder of the imageAction() function. This code begins by retrieving the full filesystem path using getFullPath(). It then determines which type of image the file is and sends headers for the type and the size of the image using the setHeader() function. Finally, the actual image data is sent. Listing 11-18. Sending the Image Headers and Then the Image Itself (UtilityController.php) $fullpath = $image->getFullPath(); $info = getImageSize($fullpath); $response->setHeader('content-type', $info['mime']); $response->setHeader('content-length', filesize($fullpath)); echo file_get_contents($fullpath); } } ?> This completes the minimum code required to output uploaded blog post images. In order to test it, you can upload an image using the form we created earlier in this chapter, and then manually enter the image ID into the following URL: http://phpweb20/utility/ image?id=ImageID. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 389 9063Ch11CMP2 11/15/07 8:13 AM Page 389 Resizing Images Depending on the context, you will often want to display uploaded images at different resolutions in different areas of your site. For example, on the blog post index page in our application, you might want small thumbnails (perhaps around about 100 pixels by 75 pixels) while on the blog post detail page you might want to show somewhat larger images (such as about 200 pixels by 150 pixels). In addition, you may want to allow the user to click on an image to show the image at full size. In this section, we will build a simple mechanism to generate resized versions (that is, thumbnails) of uploaded images. We will build this system such that the desired dimensions can be specified in the URL and an image the appropriate size will be returned. In order to do this, we will use the GD functions that are included with PHP. A popular alternative to GD is ImageMagick, but it requires that ImageMagick also be installed on the server, while GD is typically included in most PHP installations. ■Note The techniques we use here can be achieved using ImageMagick’s convert tool. You can use this tool either by calling convert directly within your script, or by using the imagick PECL package. After lying dormant for several years, this package has recently gained new life and provides a simple interface to ImageMagick. The biggest drawback to using this package is that it needs to be built into the PHP server in addition to ImageMagick being installed on the server. More information about convert can be found at http://www.imagemagick.org/script/convert.php. Creating Thumbnails The image thumbnailer we will now create is fairly straightforward when you look at the indi- vidual pieces. We will create a new method in the DatabaseObject_BlogPostImage class called createThumbnail(), which generates a thumbnail for the loaded record based on the width and height arguments specified. This method will return the full filesystem path to the created thumbnail, which allows us to easily link the thumbnailer into the existing code to load and display images. Additionally, it allows us to simply return the path of the original file if the requested thumbnail is bigger than the original image. This saves unnecessary duplication of the image on the filesystem. The other thing createThumbnail() will do is cache the thumbnails. Since creating thumb- nails can be processor-intensive (depending on the size of the input and output images), we want to make this process as efficient as possible. Fortunately, it is very straightforward to cache the created thumbnails, as we will soon see. Before we write createThumbnail(), we will add in another method, which we will call GetThumbnailPath(). This method will return the filesystem path to where created thumbnails should be stored. We will use the ./data/tmp directory as the base directory for this, and use a subdirectory within it called thumbnails, as shown in Listing 11-19. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY390 9063Ch11CMP2 11/15/07 8:13 AM Page 390 Listing 11-19. Retrieving the Thumbnail Storage Path (BlogPostImage.php) paths->data); } } ?> Next, we can look at createThumbnail(), in which we begin by retrieving the path of the original file and some basic information about this file, which we will use later in the function. Listing 11-20 shows beginning of createThumbnail(). Listing 11-20. Determining the Image Attributes for Later Use (BlogPostImage.php) getFullpath(); $ts = (int) filemtime($fullpath); $info = getImageSize($fullpath); Determining the Width and Height of the Thumbnail The first (and probably the most complicated) step of creating a thumbnail image is to determine the dimensions of the thumbnail. The createThumbnail() function accepts the maximum width of a thumbnail as its first argument, and the maximum height as the second argument. Note that the proportions remain the same as the original regardless of the speci- fied width and height; we simply use these values to determine the maximum size. We will allow for either of these arguments (but not both) to be set to 0. This means the image will be constrained only by the specified value (so if a maximum width of 100 is speci- fied with a maximum height of 0, the image can be any height as long as it is no wider than 100 pixels). We use the width and height values returned from getImageSize() in combination with the specified maximum width and height ($maxW and $maxH) to determine the width and height of the thumbnail ($newW and $newH). This code is shown in Listing 11-21, and is explained in the comments. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 391 9063Ch11CMP2 11/15/07 8:13 AM Page 391 Listing 11-21. Calculating the Width and Height of the Thumbnail (BlogPostImage.php) $w = $info[0]; // original width $h = $info[1]; // original height $ratio = $w / $h; // width:height ratio $maxW = min($w, $maxW); // new width can't be more than $maxW if ($maxW == 0) // check if only max height has been specified $maxW = $w; $maxH = min($h, $maxH); // new height can't be more than $maxH if ($maxH == 0) // check if only max width has been specified $maxH = $h; $newW = $maxW; // first use the max width to determine new $newH = $newW / $ratio; // height by using original image w:h ratio if ($newH > $maxH) { // check if new height is too big, and if $newH = $maxH; // so determine the new width based on the $newW = $newH * $ratio; // max height } if ($w == $newW && $h == $newH) { // no thumbnail required, just return the original path return $fullpath; } Determining the Input and Output Functions In order to create thumbnails with GD, we must turn the original image into a GD image resource (a special type of PHP variable). There is a different function to do this for each of the image types we support (JPEG, GIF, and PNG). Once the thumbnail has been created, we need to output the new GD image resource to the filesystem. We must determine which function to use for this, also based on the type of image. While we could simply use the same image type for all thumbnails, we will use the input image type as the output image type. Just as we did when writing the image uploader (Listing 11-9), we can check the third index of the getImageSize() result to determine which functions to use. This is shown in Listing 11-22. Listing 11-22. Determining the GD Input and Output Image Functions (BlogPostImage.php) switch ($info[2]) { case IMAGETYPE_GIF: $infunc = 'ImageCreateFromGif'; $outfunc = 'ImageGif'; break; CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY392 9063Ch11CMP2 11/15/07 8:13 AM Page 392 case IMAGETYPE_JPEG: $infunc = 'ImageCreateFromJpeg'; $outfunc = 'ImageJpeg'; break; case IMAGETYPE_PNG: $infunc = 'ImageCreateFromPng'; $outfunc = 'ImagePng'; break; default; throw new Exception('Invalid image type'); } Generating the Thumbnail Filename Next, we will generate a filename for the newly created thumbnail. We generate this based on the height and width of the thumbnail, as well as on the image ID and the date the original file was created. By using the creation date, the thumbnail will be regenerated if the file is ever modified. ■Note We haven’t actually implemented functionality to allow the user to edit an uploaded image, but if you did, this timestamp would ensure new thumbnails would be generated automatically for the new image. In addition to creating the filename, we must also determine the full path of the thumb- nail and ensure that we can write to that directory, as shown in Listing 11-23. If the destination directory doesn’t exist, we will create it. Note that this will typically only occur the first time this function is called. Listing 11-23. Generating the Thumbnail Filename, and Creating the Target Directory (BlogPostImage.php) // create a unique filename based on the specified options $filename = sprintf('%d.%dx%d.%d', $this->getId(), $newW, $newH, $ts); // autocreate the directory for storing thumbnails $path = self::GetThumbnailPath(); if (!file_exists($path)) mkdir($path, 0777); if (!is_writable($path)) throw new Exception('Unable to write to thumbnail dir'); CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 393 9063Ch11CMP2 11/15/07 8:13 AM Page 393 Creating the Thumbnail Now that we know the dimensions of the thumbnail, the input and output functions, and the thumbnail destination path, we can create the actual thumbnail. The very first thing we will do, however, is check whether the thumbnail already exists. This simple check (in combina- tion with the previous filename generation) is the caching functionality. If the thumbnail exists, we simply skip the generation part of this code. If the thumbnail doesn’t exist, we read in the image to GD using the input determined in Listing 11-22. So if the original image is a PNG file, ImageCreateFromPng() is used. If an error occurs reading the image, we throw an exception and return from the function. The first por- tion of the thumbnail-creation code is shown in Listing 11-24. Listing 11-24. Reading the Image into GD (BlogPostImage.php) // determine the full path for the new thumbnail $thumbPath = sprintf('%s/%s', $path, $filename); if (!file_exists($thumbPath)) { // read the image in to GD $im = @$infunc($fullpath); if (!$im) throw new Exception('Unable to read image file'); When resizing an image with GD, the original image resource remains unchanged while the resized version is written to a secondary GD image resource (which in this case we will call $thumb). We must first create this secondary GD image using ImageCreateTrueColor(), with the $newW and $newH variables specifying the size. We then use GD’s ImageCopyResampled() function to copy a portion of the source image ($im) to the $thumb. The target image resource is the first argument, and the source image resource is the second argument. The remainder of the arguments indicate the X and Y coordinates of the target and source images respectively, followed by the width and height of both images. This function is fairly powerful, and it also allows you to easily crop or stretch images. The code to create the target image and resample the original image onto the new image is shown in Listing 11-25. Listing 11-25. Resampling the Original Image onto the New Image Resource (BlogPostImage.php) // create the output image $thumb = ImageCreateTrueColor($newW, $newH); // now resample the original image to the new image ImageCopyResampled($thumb, $im, 0, 0, 0, 0, $newW, $newH, $w, $h); Finally, we write this new image to the filesystem using the output function we selected in Listing 11-22 (stored in $outfunc). So if the original image was a PNG image, we would use ImagePng() to write the image to disk. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY394 9063Ch11CMP2 11/15/07 8:13 AM Page 394 ■Note The second argument to the output functions (ImagePng(), ImageJpeg(), and ImageGif()) spec- ifies where on the filesystem the image file should be written. If this isn’t specified, the image data is output directly to the browser. You could choose to take advantage of this if you didn’t want to write the generated images to the filesystem. Finally, we ensure that the image was written to the system, and if so we return the path to the newly created thumbnail. Listing 11-26 shows the code that writes the image to the filesystem and returns from createThumbnail(). Listing 11-26. Writing the Thumbnail and Returning from createThumbnail() (BlogPostImage.php) $outfunc($thumb, $thumbPath); } if (!file_exists($thumbPath)) throw new Exception('Unknown error occurred creating thumbnail'); if (!is_readable($thumbPath)) throw new Exception('Unable to read thumbnail'); return $thumbPath; } // ... other code } ?> Linking the Thumbnailer to the Image Action Handler Now that we have the capability to easily create image thumbnails, we must hook this into our web application. We will do this by making some simple modifications to the imageAction() function we created in Listing 11-16. We are going to provide the ability to specify the desired width and height in the URL, so it will be extremely simple to generate thumbnails as required. This means you can decide on the dimensions of the thumbnail in the templates that output the image, rather than having to hard-code these dimensions in your PHP code. Because users could potentially abuse a system that allows them to generate thumbnails of any size, we will add a mechanism to make it more difficult for this to occur. This mecha- nism works as follows: 1. When an image is requested, the URL must include a parameter called a hash in addi- tion to the image ID, width, and height. This parameter will be generated based on the ID, width, and height. 2. The imageAction() method will check the supplied hash against what the hash should be for the combination of ID, width, and height. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 395 9063Ch11CMP2 11/15/07 8:13 AM Page 395 3. If the two hash values are different, we will assume the image was requested incor- rectly, and a 404 error is sent back. 4. If the hash value is correct, we generate the thumbnail and send it back. If the user manually changes the width or height in the URL, the hash will not match the request, so the thumbnail won’t be generated. Generating an Image Hash To implement this system, we first need the ability to generate an image hash based on the given parameters. We will use this method both in the generation of URLs in the template, as well as to generate a hash based on the ID, width, and height supplied in the request URL. Listing 11-27 shows the GetImageHash() method, which generates a string based on the supplied arguments using md5(). This code should be added to the BlogPostImage.php file in ./include/DatabaseObject. Listing 11-27. Generating a Hash for the Given Image ID,Width, and Height (BlogPostImage.php) Generating Image Filenames Next, we will implement a new Smarty plug-in called imagefilename, which is used to generate image filenames using the desired image ID, width, and height. This plug-in will allow us to include image thumbnails in our templates very easily. For example, to include a thumbnail that is 100 pixels by 75 pixels of an image with an ID of 12, the following code would be used in the template: Based on the arguments in this example, we would want to generate a URL as follows: /utility/image?id=12&w=100&h=75&hash=[hash] CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY396 9063Ch11CMP2 11/15/07 8:13 AM Page 396 Similarly, if you wanted to generate the image path for the full-sized image, you would use the following: In order to generate this URL, we would use the {geturl} plug-in created earlier, in conjunc- tion with the arguments and the GetImageHash() method. Listing 11-28 shows the code for the function.imagefilename.php file, which we will store in ./include/Templater/plugins. Listing 11-28. The imagefilename Plug-In, Used to Generate a Thumbnail Image Path (function.imagefilename.php) _get_plugin_filepath('function', 'geturl'); $hash = DatabaseObject_BlogPostImage::GetImageHash( $params['id'], $params['w'], $params['h'] ); $options = array( 'controller' => 'utility', 'action' => 'image' ); return sprintf( '%s?id=%d&w=%d&h=%d&hash=%s', smarty_function_geturl($options, $smarty), $params['id'], $params['w'], $params['h'], $hash ); } ?> CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 397 9063Ch11CMP2 11/15/07 8:13 AM Page 397 This function begins by initializing the parameters (the image ID, as well as the desired width and height). Next, it loads the geturl plug-in so we can generate the /utility/image part of the URL (the controller and action values are specified in the $options array that we create in this function). Next, we generate the hash for the given ID, width, and height, and then finally combine all of the parameters together into a single string and return this value from the plug-in. Updating imageAction() to Serve the Thumbnail We can now update the imageAction() method to look for the w, h, and hash parameters so a thumbnail can be served if required. We simply need to generate a new hash based on the id, w, and h parameters, and then compare it to the hash value in the URL. Once we have deter- mined that the supplied hash is valid and that the image could be loaded, we continue on by generating the thumbnail and sending it. Instead of calling getFullPath(), we will call createThumbnail(), which returns the full path to the generated thumbnail. Since createThumbnail() throws various exceptions, we will call getFullPath() as a fallback. In other words, if the thumbnail creation fails for some rea- son, the original image is displayed instead. You may prefer instead to output an error. The other code in imageAction() operated on the returned path from getFullPath(), so we don’t need to change any of it—createThumbnail() also returns a full filesystem path. Listing 11-29 shows the new version of imageAction(), which belongs in the UtilityController.php file in ./include/Controllers. Listing 11-29. Modifying imageAction() to Output Thumbnails on Demand (UtilityController.php) getRequest(); $response = $this->getResponse(); $id = (int) $request->getQuery('id'); $w = (int) $request->getQuery('w'); $h = (int) $request->getQuery('h'); $hash = $request->getQuery('hash'); $realHash = DatabaseObject_BlogPostImage::GetImageHash($id, $w, $h); // disable autorendering since we're outputting an image $this->_helper->viewRenderer->setNoRender(); $image = new DatabaseObject_BlogPostImage($this->db); if ($hash != $realHash || !$image->load($id)) { CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY398 9063Ch11CMP2 11/15/07 8:13 AM Page 398 // image not found $response->setHttpResponseCode(404); return; } try { $fullpath = $image->createThumbnail($w, $h); } catch (Exception $ex) { $fullpath = $image->getFullPath(); } $info = getImageSize($fullpath); $response->setHeader('content-type', $info['mime']); $response->setHeader('content-length', filesize($fullpath)); echo file_get_contents($fullpath); } } ?> Managing Blog Post Images Now that we have the ability to view uploaded images (both at their original size and as thumbnails) we can display the images on the blog post preview page. In this section, we will modify the blog manager to display uploaded images, thereby allowing the user to easily delete images from their blog posts. Additionally, we will implement Ajax code using Prototype and Scriptaculous that will allow the user to change the order in which the images in a single post are displayed. Automatically Loading Blog Post Images Before we can display the images on the blog post preview page, we must modify DatabaseObject_BlogPost to automatically load all associated images when the blog post record is loaded. To do this, we will change the postLoad() function to automatically load the images. Currently this function only loads the profile data for the blog post, but we will add a call to load the images, as shown in Listing 11-30. Additionally, we must initialize the $images array. Listing 11-30. Automatically Loading a Blog Post’s Images When the Post Is Loaded (BlogPost.php) profile->setPostId($this->getId()); $this->profile->load(); $options = array( 'post_id' => $this->getId() ); $this->images = DatabaseObject_BlogPostImage::GetImages($this->getDb(), $options); } // ... other code } ?> The code in Listing 11-30 calls a method called GetImages() in DatabaseObject_ BlogPostImage, which we must now implement. This function, which we will add to BlogPostImage.php in ./include/DatabaseObject, is shown in Listing 11-31. Note that we use the ranking field as the sort field. This ensures the images are returned in the order specified by the user (we will implement the functionality to change this order shortly). Listing 11-31. Retrieving Multiple Blog Post Images (BlogPostImage.php) array()); foreach ($defaults as $k => $v) { $options[$k] = array_key_exists($k, $options) ? $options[$k] : $v; } $select = $db->select(); $select->from(array('i' => 'blog_posts_images'), array('i.*')); // filter results on specified post ids (if any) if (count($options['post_id']) > 0) $select->where('i.post_id in (?)', $options['post_id']); $select->order('i.ranking'); CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY400 9063Ch11CMP2 11/15/07 8:13 AM Page 400 // fetch post data from database $data = $db->fetchAll($select); // turn data into array of DatabaseObject_BlogPostImage objects $images = parent::BuildMultiple($db, __CLASS__, $data); return $images; } } ?> Displaying Images on the Post Preview The next step in managing images for a blog post is to display them on the preview page. To do this, we must make some changes to the preview.tpl template in the ./templates/ blogmanager directory, as well as adding some new styles to ./htdocs/css/styles.css. Earlier in this chapter we created a new element in this template called #preview-images. The code in Listing 11-32 shows the additions we must make to preview.tpl to display each of the images. We will output the images in an unordered list, which will help us later when we add the ability to reorder the images using Scriptaculous. Listing 11-32. Outputting Images on the Blog Post Preview Page (preview.tpl)
              Images {if $post->images|@count > 0}
                {foreach from=$post->images item=image}
              • {$image->filename|escape}
              • {/foreach}
              {/if} CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 401 9063Ch11CMP2 11/15/07 8:13 AM Page 401
              As you can see in the code, we use the new imagefilename plug-in to generate the URL for an image thumbnail 200 pixels wide and 65 pixels high. We also include a form to delete each image in this template. We haven’t yet implemented this functionality (you may recall that we left a placeholder for the delete command in the blog manager’s imagesAction() method), but this will be added shortly. Listing 11-33 shows the new styles we will add to styles.css in ./htdocs/css. These styles format the unordered list so list items are shown horizontally. We use floats to position list items next to each other (rather than using inline display), since this gives greater control over the style within each item. Note that we must add clear : both to the div holding the upload form in order to keep the display of the page intact. Listing 11-33. Styling the Image-Management Area (styles.css) #preview-images ul { list-style-type : none; margin : 0; padding : 0; } #preview-images li { float : left; font-size : 0.85em; text-align : center; margin : 3px; padding : 2px; border : 1px solid #ddd; background : #fff; } #preview-images img { display : block; } CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY402 9063Ch11CMP2 11/15/07 8:13 AM Page 402 #preview-images div { clear : both; } Once this code has been added, the image display area should look like the page in Figure 11-2. Figure 11-2. Displaying the images on the blog post preview page Deleting Blog Post Images The next step in the management of blog post images is to implement the delete functionality. We will first implement a non-Ajax version to delete images, and then modify it slightly to use Scriptaculous for a fancier solution. Before we complete the delete section of the images action in the blog manager con- troller, we must make some small changes to the DatabaseObject_BlogPostImage class. Using DatabaseObject means we can simply call the delete() method on the image record to remove it from the database, but this will not delete the uploaded image from the filesystem. As we saw in Chapter 3, if we define the postDelete() method in a DatabaseObject subclass, it is automatically called after a record has been deleted. We will implement this method for DatabaseObject_BlogPostImage so the uploaded file is removed from the filesystem. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 403 9063Ch11CMP2 11/15/07 8:13 AM Page 403 Additionally, since thumbnails are automatically created for each image, we will clean up the thumbnail storage area for the image being deleted. Note that this is quite easy, since we prefixed all generated thumbnails with their database ID. Listing 11-34 shows the postDelete() function as it should be added to DatabaseObject_ BlogPostImage in ./include/DatabaseObject. First, we use unlink() to delete the main image from the filesystem. Next, we use the glob() function, which is a useful PHP function for retrieving an array of files based on the specified pattern. We loop over each of the files in the array and unlink() them. Listing 11-34. Deleting the Uploaded File and All Generated Thumbnails (BlogPostImage.php) getFullPath()); $pattern = sprintf('%s/%d.*', self::GetThumbnailPath(), $this->getId()); foreach (glob($pattern) as $thumbnail) { unlink($thumbnail); } return true; } // ... other code } ?> Now when you call the delete() method on a loaded blog post image, the filesystem files will also be deleted. Remember to return true from postDelete()—otherwise the SQL transac- tion will be rolled back. The other method we must add to this class is one that gives us the ability to load an image for a specified blog post. This is similar to the loadForUser() function we implemented for blog posts. We do this so that only the logged-in user will be able to delete an image on their blog posts. Listing 11-35 shows the code for the loadForPost() function, which is also added to BlogPostImage.php. Listing 11-35. Restricting the Load of Images to a Particular Blog Post (BlogPostImage.php) getSelectFields()), $this->_table, $post_id, $image_id ); return $this->_load($query); } // ... other code } ?> Now that these changes have been made to DatabaseObject_BlogPostImage, we can implement the non-Ajax version of deleting an image. To do this, we simply need to imple- ment the delete part of imagesAction() in BlogmanagerController.php. Remember that we left a placeholder for this when we originally created this method in Listing 11-5. The code used to delete an image is shown in Listing 11-36. Listing 11-36. Deleting an Image from a Blog Post (BlogmanagerController.php) getPost('delete')) { $image_id = (int) $request->getPost('image'); $image = new DatabaseObject_BlogPostImage($this->db); if ($image->loadForPost($post->getId(), $image_id)) { $image->delete(); CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 405 9063Ch11CMP2 11/15/07 8:13 AM Page 405 $this->messenger->addMessage('Image deleted'); } } // ... other code } } ?> If you now click on the “Delete” button below an image, the image will be deleted from the database and filesystem, and a message will appear in the top-right flash messenger when the page reloads. Using Scriptaculous and Ajax to Delete Images Now that we have a non-Ajax solution for deleting images, we can enhance this system slightly to use Ajax. Essentially what we will do is send an Ajax request to delete the image when the “Delete” button is clicked, and use Scriptaculous to make the image disappear from the screen. There are a number of different Scriptaculous effects that can be used to hide elements, such as Puff, SwitchOff, DropOut, Squish, Fold, and Shrink, but we are going to use the Fade effect. Note, however, that we are not applying this effect to the image being deleted; we will apply it to the list item (
            1. ) surrounding the image. Modifying the PHP Deletion Code In the imagesAction() function of BlogmanagerController.php, the code redirects the browser back to the blog post preview page after completing the action (uploading, reordering, or deleting). This is fine for non-Ajax solutions, but if this occurs when using XMLHttpRequest, the contents of the preview page will unnecessarily be returned in the background. To prevent this, we will make a simple change to the redirection code at the end of this function. As we have done previously, we will use the isXmlHttpRequest() function provided by Zend_Controller_Front to determine how to proceed. Because we want to check whether or not the image deletion was successful in the JavaScript code, we will also modify the code so it sends back JSON data about the deleted image. We will send this back using the sendJson() method we added in Chapter 6. Listing 11-37 shows the changes to this method in BlogmanagerController.php. This code now only writes the deletion message to the messenger if the delete request did not use Ajax. If this distinction about writing the message isn’t made, you could delete an image via Ajax and then refresh the page, causing the “image deleted” message to show again. Listing 11-37. Handling Ajax Requests in imageAction() (BlogmanagerController.php) getPost('upload')) { // ... other code } else if ($request->getPost('reorder')) { // ... other code } else if ($request->getPost('delete')) { $image_id = (int) $request->getPost('image'); $image = new DatabaseObject_BlogPostImage($this->db); if ($image->loadForPost($post->getId(), $image_id)) { $image->delete(); if ($request->isXmlHttpRequest()) { $json = array( 'deleted' => true, 'image_id' => $image_id ); } else $this->messenger->addMessage('Image deleted'); } } if ($request->isXmlHttpRequest()) { $this->sendJson($json); } else { $url = $this->getUrl('preview') . '?id=' . $post->getid(); $this->_redirect($url); } } } ?> Creating the BlogImageManager JavaScript Class To create an Ajax solution for deleting blog post images, we will write a new JavaScript class called BlogImageManager. This class will find all of the delete forms in the image-management section of preview.tpl and bind the submit event listener to each of these forms. We will then implement a function to handle this event. Listing 11-38 shows the constructor for this class, which we will store in a file called BlogImageManager.class.js in the ./htdocs/js directory. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 407 9063Ch11CMP2 11/15/07 8:13 AM Page 407 Listing 11-38. The Constructor for BlogImageManager (BlogImageManager.class.js) BlogImageManager = Class.create(); BlogImageManager.prototype = { initialize : function(container) { this.container = $(container); if (!this.container) return; this.container.getElementsBySelector('form').each(function(form) { form.observe('submit', this.onDeleteClick.bindAsEventListener(this)); }.bind(this)); }, This class expects the unordered list element that holds the images as the only argument to the constructor. We store it as a property of the object, since we will be using it again later when implementing the reordering functionality. In this class, we find all the forms within this unordered list by using the getElementsBySelector() function. This function behaves in the same way as the $$() function we looked at in Chapter 5, except that it only searches within the element the func- tion is being called from. We then loop over each form that is found and observe the submit event on it. We must bind the onDeleteClick() event handler to the BlogImageManager instance so it can be referred to within the correct context when the event is handled. The next thing we need to do is implement the onDeleteClick() event handler, as shown in Listing 11-39. Listing 11-39. The Event Handler Called When a Delete Link Is Clicked (BlogImageManager.class.js) onDeleteClick : function(e) { Event.stop(e); var form = Event.element(e); var options = { method : form.method, parameters : form.serialize(), onSuccess : this.onDeleteSuccess.bind(this), onFailure : this.onDeleteFailure.bind(this) } message_write('Deleting image...'); new Ajax.Request(form.action, options); }, CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY408 9063Ch11CMP2 11/15/07 8:13 AM Page 408 The first thing we do in this method is stop the event so the browser doesn’t submit the form normally—a background Ajax request will be submitting the form instead. Next, we determine which form was submitted by calling Event.element(). This allows us to perform an Ajax request on the form action URL, thereby executing the PHP code that is used to delete a blog post image. We then create a hash of options to pass to Ajax.Request(), which includes the form val- ues and the callback handlers for the request. Before instantiating Ajax.Request(), we update the page status message to tell the user that an image is being deleted. The next step is to implement the handlers for a successful and unsuccessful request, as shown in Listing 11-40. Listing 11-40. Handling the Response from the Ajax Image Deletion (BlogImageManager.class.js) onDeleteSuccess : function(transport) { var json = transport.responseText.evalJSON(true); if (json.deleted) { var image_id = json.image_id; var input = this.container.down('input[value=' + image_id + ']'); if (input) { var options = { duration : 0.3, afterFinish : function(effect) { message_clear(); effect.element.remove(); } } new Effect.Fade(input.up('li'), options); return; } } this.onDeleteFailure(transport); }, onDeleteFailure : function(transport) { message_write('Error deleting image'); } }; In Listing 11-37 we made the delete operation in imagesAction() return JSON data. To determine whether the image was deleted by the code in Listing 11-40, we check for the deleted element in the decoded JSON data. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 409 9063Ch11CMP2 11/15/07 8:13 AM Page 409 Based on the image_id element also included in the JSON data, we try to find the corre- sponding form element on the page for that image. We do this by looking for a form input with the value of the image ID. Once we find this element, we apply the Scriptaculous fade effect to make the image disappear from the page. We don’t apply this effect to the actual image that was deleted; rather, we remove the surrounding list item so the image, form, and surrounding code are completely removed from the page. When the fade effect is called, the element being faded is only hidden when the effect is completed; it is not actually removed from the DOM. In order to remove it, we define the afterFinish callback on the effect, and use it to call the remove() method on the element. The callbacks for Scriptaculous effects receive the effect object as the first argument, and the ele- ment the effect is applied to can be accessed using the element property of the effect. We also use the afterFinish function to clear the status message. After we’ve defined the options, we can create the actual effect. Since we want to remove the list item element corresponding to the image, we can simply call the Prototype up() func- tion to find it. Loading BlogImageManager in the Post Preview Next, we will load the BlogImageManager JavaScript class in the preview.tpl template. In order to instantiate this class, we will add code to the blogPreview.js file we created in Chapter 7. Listing 11-41 shows the changes we will make to preview.tpl in the ./templates/ blogmanager directory to load BlogImageManager.class.js. Listing 11-41. Loading the BlogImageManager Class (preview.tpl) {include file='header.tpl' section='blogmanager'} Listing 11-42 shows the changes we will make to blogPreview.js in ./htdocs/js to instan- tiate BlogImageManager automatically. Listing 11-42. Instantiating BlogImageManager Automatically (blogPreview.js) Event.observe(window, 'load', function() { // ... other code var im = new BlogImageManager('post_images'); }); If you now try to delete an image from a blog post, the entire process should be com- pleted in the background. Once the “Delete” button is clicked, the background request to delete the image will be initiated, and the image will disappear from the page upon successful completion. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY410 9063Ch11CMP2 11/15/07 8:13 AM Page 410 Deleting Images when Posts Are Deleted One thing we have not yet dealt with is what happens to images when a blog post is deleted. As the code currently stands, if a blog post is deleted, any associated images will not be deleted. Because of the foreign key constraint on the blog_posts_images table, the SQL to delete a blog post that has one or more images will fail. We must update the DatabaseObject_BlogPost class so images are deleted when a post is deleted. Doing this is very straightforward, since the instance of DatabaseObject_BlogPost we are trying to delete already has all the images loaded (so we know exactly what needs to be deleted), and it already has a delete callback (we implemented the preDelete() function earlier). This means we can simply loop over each image and call the delete() method. ■Note DatabaseObject automatically controls transactions when saving or deleting a record. You can pass false to save() or delete() so transactions are not used. Because a transaction has already been started by the delete() call on the blog post, we must pass false to the delete() call for each image. Listing 11-43 shows the two new lines we need to add to preDelete() in the BlogPost.php file in the ./include/DatabaseObject directory. Listing 11-43. Automatically Deleting Images When a Blog Post Is Deleted (BlogPost.php) images as $image) $image->delete(false); return true; } // ... other code } ?> Now when you try to delete a blog post, all images associated with the post will also be deleted. CHAPTER 11 ■ A DYNAMIC IMAGE GALLERY 411 9063Ch11CMP2 11/15/07 8:13 AM Page 411 Reordering Blog Post Images We will now implement a system that will allow users to change the order of the images asso- ciated with a blog post. While this may not seem overly important, we do this because we are controlling the layout of images when blog posts are displayed. Additionally, in the next section we will modify the blog index to display an image beside each blog post that has one. If a blog post has more than one image, we will use the first image for the post. Drag and Drop In the past, programmers have used two common techniques to allow users to change the order of list items, both of which are slow and difficult to use. The first method was to provide “up” and “down” links beside each item in the list, which moved the items up or down when clicked. Some of these implementations might have included a “move to top” and “move to bottom” button, but on the whole they are difficult to use. The other method was to provide a text input box beside each item. Each box contained a number, which determined the order of the list. To change the order, you would update the numbers inside the boxes. For our implementation, we will use a drag-and-drop system. Thanks to Scriptaculous’s Sortable class, this is not difficult to achieve. We will implement this by extending the BlogImageManager JavaScript class we created earlier this chapter. ■Note As an exercise, try extending this reordering system so it is accessible for non-JavaScript users. You could try implementing this by including a form on the page within