============================================== Developing an Image Gallery with eZ components ============================================== :Author: Tobias Schlitt :Date: Wednesday 19 April 2006 10:35:00 am .. contents:: Table of Contents This tutorial shows you how to program a web application that stores photographs in an image gallery. It's based on libraries included with the eZ components, and so gives you a practical example of the library usage. The sample code of this article is available for download. This tutorial uses the eZ components to create a simple image gallery for a website. You can use the image gallery to upload photos and to organize them into albums. The gallery scales the photos to a given size, and also produces a thumbnail for previewing the images on an overview page. The image gallery application that will result from doing this tutorial is not completely ready for use in a production environment, but with a little more work, and using the concepts learned from this tutorial, it could be offered to the world (and your Auntie Erna). Setting up the environment ========================== Preparing the database ---------------------- Before we can start, we need to set up a database that will store the structure of our albums and the data of the photos. For development I used MySQL, but you can use any database supported by the eZ components database abstraction layer. Just create the following two tables and a user to access the database:: CREATE TABLE album ( id int(11) unsigned NOT NULL auto_increment, title varchar(200) NOT NULL default '', description text NOT NULL, PRIMARY KEY (id) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; This table will be used to store our album structure. As you can see, it is very simple. Each album has a title, a description and a numeric ID:: CREATE TABLE photo ( id int(11) unsigned NOT NULL auto_increment, album int(11) NOT NULL default '0', title varchar(200) NOT NULL default '', description text NOT NULL, PRIMARY KEY (id) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; The table to store data about our photos is as simple as the album table. It contains the numeric ID and the ID of the album where the photo belongs. It also contains a title and a description to tell Auntie Erna something about the pictures she's looking at. Creating Directories -------------------- After we create the database, we need to create the basic directory structure for our gallery: =========== =================================================================== Directory Description =========== =================================================================== actions/ The actions/ directory will contain controllers for the actions we want to implement. (As a good OO programmer, we use the Model View Controller (MVC) pattern.) ----------- ------------------------------------------------------------------- data/ data/ will contain the photos. ----------- ------------------------------------------------------------------- interfaces/ In interfaces/ we'll store some basic interfaces, for example our action controllers. ----------- ------------------------------------------------------------------- models/ models/ will contain the model classes for an album and a photo. ----------- ------------------------------------------------------------------- pos/ The pos/ directory is somewhat special and will contain definitions for the persistent objects. A persistent object is basically the implementation of the active record pattern in the eZ components. ----------- ------------------------------------------------------------------- templates/ The templates/ directory will store the HTML parts (the view of our MVC). Installing eZ components =========== =================================================================== Now we need to install the eZ components. (Hey, this is a tutorial about them, remember?!) The most convenient way to get them is to use the PEAR Installer. If you have PEAR installed, simply type:: $ pear channel-discover components.ez.no $ pear install ezc/eZComponents If your PEAR installation is properly configured, you're done! If you experience problems, check that your include_path is set correctly. Maybe you prefer to not use PEAR. Instead, you can download the eZ components tarball from our website. Crack it into a directory of your choice and make sure your include_path contains the directory you choose for extraction. The eZ UserInput component relies on the PECL extension ext/filter, so you have to install that too. The easiest way to do this is using the PEAR installer again:: $ pecl install filter-beta If you don't use PEAR, you can download the source package from the PECL website and compile the extension by hand. Note, that you have to install the pre-compiled binary distribution of the extension by hand, if you are running PHP on Windows. After these steps, you have to add the extension to your php.ini. MVC in the main script ====================== Finally we can get started with coding! The first piece of code goes into the file gallery.php, which is located in our application root directory. This is the main file that we will call from the web browser. It contains a lot of code, namely the autoload mechanism, the main controller class and the code to run it. But let's start slowly... Accessing the database and eZ components ---------------------------------------- :: First we require the eZ components base file. This is the only file we ever need to explicitly require. It contains the autoload mechanism, which we are activating by defining the __autoload() function right after the require statement. This is basically all we need to do to access all the classes contained in the eZ components library. Next we require a configuration file. (You need to change the values shown below to suit your environment.) Basically, the config file contains:: The DSN is a string that describes the connection to your database. The first part specifies the database engine to use. After that, the user and password are specified (in a similar manner to describing a URI). This account is used to connect to your database host (usually "localhost"). After the last slash ("/"), specify the database you created earlier. The second defined constant indicates where the files to include are located. You can change this if needed, but the example should usually work. The main controller class ------------------------- :: This is the basic structure of our main controller. Beside the constructor, it contains a run() method (that will dispatch to our action controllers) and a display() method (that will display the view after the action was executed). Additionally, we define two static methods as a singleton pattern for our database session ( getSession()) and the image converter (which I'll describe later). This will allow us to gain access to those instances by simply calling ezcGallery::getSession() anywhere in our application. Initialization -------------- So let's start with the constructor:: 'actions/listing.php', 'show' => 'actions/show.php', 'create' => 'actions/create.php', 'add' => 'actions/add.php', ); private $currentAction; public function __construct() { $action = 'listing'; if ( ezcInputForm::hasGetData() ) { $definition = array( 'action' => new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'string' ), ); $form = new ezcInputForm( INPUT_GET, $definition ); $action = ( $form->hasValidData( 'action' ) ) ? $form->action : 'listing'; } if ( !isset( $this->actions[$action] ) ) { throw new Exception( "Invalid action <".htmlentities($action).">." ); } require_once $this->actions[$action]; $this->currentAction = new $action; } ?> First we define the actions we want our controller to dispatch. Defining those hard-coded in the controller is the most secure way to avoid potential security issues when using user-provided data to require files. Each action is assigned to the file we have to include to load the specific action. The second private property we need in the constructor is $currentAction, which will contain the action controller object that will handle the action. At first we initialize the variable $action to listing, which should be the action shown when no action is submitted by the user. After that we check for GET data in general, using the eZ UserInput component. If GET data is available, we define a rule set for UserInput to validate the data we expect - a variable action, which contains a string and must be set. Now we create an instance of ezcInputForm, which handles our GET data. Finally (at the end of the if statement) we assign the received action, if it was valid, or set the default action again. The next step is to check if the action we received is valid. If it is not, we throw an exception because we need a valid action to proceed. If the action was valid, we require the assigned file and instantiate the action controller. That's it for the initialization. Execution --------- Let's take a look at the run method:: currentAction->run(); } ?> This one is really short. It simply dispatches the run to the action controller. Display ------- So now let's go on to displaying the HTML. Because the eZ Template component is not ready yet, we are using HTML with embedded PHP for the View part of the MVC:: currentAction->getTemplateVars(); ob_start(); $res = include 'templates/' . $this->currentAction->getTemplate(); $html = ob_get_contents(); ob_end_clean(); if ( $res === false ) { throw new Exception( 'Template error.' ); } echo $html; } ?> As the first line of the display() method shows, every action controller implements a getTemplateVars() method to retrieve the objects needed to view the desired action. To avoid nasty errors displayed on our web site, we use PHP's output buffering to get the content of the filled template file. Every action controller knows which template to use since one action can have different templates (for example, one for a form and one for showing the results after submitting the form). Finally we display the HTML (if no error occurred). The getSession() and getConverter() methods are not of special interest right now. We will deal with them when we need them. Script execution ---------------- Finally, in gallery.php, we need to run our main controller, which is done like this (as you have probably already guessed):: run(); $gallery->display(); } catch ( Exception $e ) { die( $e->getMessage() ); } ?> With this, we have the base structure of our application ready. Now let's move on and try out some of the other eZ components beside the short code piece for UserInput which we already saw. Listing photo albums ==================== The first action we want to look at is for listing albums. If you want to run the application, you should now add a few test records to your database. The listing action resides in actions/listing.php. It requires two files from our application. Since we did not hook into the eZ components autoload mechanism, we need to require those explicitly:: The album model --------------- Our album model ( models/album.php) is fairly simple:: $this->id, 'title' => $this->title, 'description' => $this->description, ); } public function setState( array $properties ) { foreach ( $properties as $key => $val ) { $this->$key = $val; } } } ?> It contains three public properties which represent the fields of our database table. Since we want to use objects of this class as a persistent object, we need the two methods setState() and getState(), which will allow the eZ PersistentObject component to serialize the model to the database and fetch it back from there. Both methods deal with arrays that have the property names of the class as the keys, assigned to the values. Quite easy, isn't it? :) A persistent session -------------------- Before we take a look at the actual code of the action controller for listing albums, we should look at the ezcGallery::getSession() method, which we skipped earlier:: The method actually does not return a database connection, but a "persistent session." The difference is quite easy to explain: a database connection can be used to perform hand-written SQL actions on a database. A persistent session is more mighty and allows us to get our SQL generated and simply store, restore, update, delete, etc, our model objects without writing the SQL by hand. We introduce a new private, static member variable for our getSession() method: $persistentSession, which will keep the single instance of the session. If this variable is not yet set, we create it. Else, we simply return the instance it contains. To create a persistent session, we first create a new database connection using ezcDbFactory and the DSN we defined earlier in the main controller file. The eZ Database component already has a singleton mechanism in place which is initialized by ezcDbInstance::set(). Using this freshly created database connection, we go on and create a persistent session. The second parameter to ezcPersistentSession is a code manager, which deals with our persistent definitions. Remember that we set up a directory for those earlier? Give the name of this directory to the code manager. This is basically all we need to do to create the persistent session. Defining persistent objects --------------------------- The last thing we need to store our model in the database is a definition for the ezcPersistentCodeManager. This resides in the pos/ directory and has to be named according to the model class we want to store (album.php):: table = "album"; $def->class = "Album"; $def->idProperty = new ezcPersistentObjectIdProperty(); $def->idProperty->columnName = 'id'; $def->idProperty->propertyName = 'id'; $def->idProperty->generator = new ezcPersistentGeneratorDefinition( 'ezcPersistentSequenceGenerator' ); $def->properties['title'] = new ezcPersistentObjectProperty(); $def->properties['title']->columnName = 'title'; $def->properties['title']->propertyName = 'title'; $def->properties['title']->propertyType = 'string'; $def->properties['description'] = new ezcPersistentObjectProperty(); $def->properties['description']->columnName = 'description'; $def->properties['description']->propertyName = 'description'; $def->properties['description']->propertyType = 'string'; return $def; ?> Our definition is an instance of ezcPersistentObjectDefinition. It has basically three attributes that define the database table to use for the objects, the class for the model, and the property of the persistent object to use as the ID. While the first two are simple strings, the third consists of a struct class, named ezcPersistentObjectIdProperty. The column name and property name again refer to the table and the class. The generator defines the way in which the IDs for the object are generated. Basically, when we store a new object, we want to generate a new ID, so we use the ezcPersistentSequenceGenerator. This one relies on the database-specific sequence mechanism, so in my case on MySQL's auto_increment. Finally, we define the mappings for the rest of our model class attributes to table columns, which is quite self-explanatory, and return the definition. (I did not know this before, but you can return something from a file in PHP, which will then be returned from the require or include statement when calling it. Nice feature!) Handling persistent objects --------------------------- Now we have all preconditions in place to start dealing with the persistent objects. Remember, we did the following so far: * Wrote the model class * Instantiated a persistent session * Defined the mapping between the model class and the database table Now let's see how the action controller works with the object:: albums = ezcGallery::getSession()->find( ezcGallery::getSession()->createFindQuery( 'Album' ), 'Album' ); } public function getTemplate() { return 'listing.php'; } public function getTemplateVars() { return array( 'albums' => $this->albums ); } } ?> The constructor of the class does nothing. It just has to be in place, because I defined it inside the action interface. The run method fetches all of our albums and stores them in the private member variable $albums. This is a very interesting part: for fetching our persistent objects, we call the method find() on our persistent session. The find method returns an array of objects it found for the find query we submitted. We have to specify the model class we are looking for in the database (second parameter to find()). The find query we use is again built on the basis of the persistent session. In this case, we only use a very simple query which basically generates a SELECT * FROM Album for us, since we want to grab all albums. Later in this tutorial, we will see more on this. Hey, we're almost done! That was easy, wasn't it? The getTemplate() method just returns a hard-coded template name and getTemplateVars() returns the albums to display. So let's go on to the view associated with this action. Viewing the listing ------------------- ::
[Create album]
The head.php and foo.php files contain nothing special - only the basic HTML, which is used on every page (for the menu and stuff like that). In the foreach loop, we iterate through the album's array (remember, we got this one from getTemplateVars()) and access the public attributes for output. That's all here. :) .. image:: ../../images/articles/2006-04-19-image-gallery-the_main_gallery_page.png Creating new photo albums ========================= Now it's time to look at the creation of an album. We already took a look at the model and know how to deal with it. Therefore, we'll skip that and go directly to the action controller. Since we already know the basic structure of an action, let's jump directly to the methods. The constructor is again empty, so let's look at the run() method. Storing data in a persistent object ----------------------------------- Since the run() method of the create action is a bit longer than the one we had before, I'll build it up in steps. You can see the resulting function at the end of this chapter. :: template = 'create_form.php'; } ?> The major if statement checks if POST data was submitted. If not, we need to display the form to the user. Therefore, we set the private member $template accordingly. Now let's see what happens if we received POST data. Validation ---------- :: new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'string' ), 'description' => new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'string' ), ); $form = new ezcInputForm( INPUT_POST, $definition ); if ( !$form->hasValidData( 'title' ) ) { throw new Exception( 'Title missing.' ); } if ( !$form->hasValidData( 'description' ) ) { throw new Exception( 'Desription missing.' ); } ?> This piece of code grabs the POST data and performs sanity checks on it. As we already saw in the main controller, we have to create a definition array for the data we expect. This is the title and the description of the album. Then we check if both were submitted and valid. If not, we throw an exception to indicate an error. Storage ------- After that, we use the following code to insert the new album into the database:: album = new Album(); $this->album->title = $form->title; $this->album->description = $form->description; ezcGallery::getSession()->save( $this->album ); $this->template = 'create_submit.php'; ?> We instantiate a new object of our model class and place it into another private member, called album, to display it later in the template. We set both properties for the title and the description. After that, we instruct the persistent session to store the object. Cool! That was easy. Although our gallery does not support editing, note that ezcPersistentSession also supports an update() method, as well as delete() and load(). The methods loadIfExists() and saveOrUpdate() are convenient here. As promised, here is the complete code of the run() method:: new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'string' ), 'description' => new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'string' ), ); $form = new ezcInputForm( INPUT_POST, $definition ); if ( !$form->hasValidData( 'title' ) ) { throw new Exception( 'Title missing.' ); } if ( !$form->hasValidData( 'description' ) ) { throw new Exception( 'Desription missing.' ); } $this->album = new Album(); $this->album->title = $form->title; $this->album->description = $form->description; ezcGallery::getSession()->save( $this->album ); $this->template = 'create_submit.php'; } else { $this->template = 'create_form.php'; } } ?> Neat. The getTemplate() and getTemplateVars() methods are not significantly different from the ones we used for the listing, so I'm skipping those here. The same applies to the HTML parts. The create_submit.php only displays the album data as we have already seen and create_form.php just contains a form. So let's move on to the next action. .. image:: ../../images/articles/2006-04-19-image-gallery-form_to_create_new_albums.png :alt: Form to create new albums Adding photos ============= So now we can create new albums, but what we really want is to upload photos. Therefore, let's look at the add action, which is located in actions/add.php. I'll skip the definition of the photo model here, since it's similar to the definition we already saw for the album. Loading persistent objects -------------------------- In the constructor of the photo-adding action we load the album from the database to which the photo should be added:: new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'int' ), ); $form = new ezcInputForm( INPUT_GET, $definition ); if ( !$form->hasValidData( 'album' ) ) { throw new Exception( 'Album ID missing.' ); } $this->album = ezcGallery::getSession()->load( 'Album', $form->album ); } else { throw new Exception( 'Album ID missing.' ); } } ?> Again we use the eZ UserInput component to grab the user data, but this time we expect an integer value. As you can see, the component ensures that you really retrieve an int value (using hasValidData()). If everything is correct, we try to fetch the object from the database. Note that in this case, ezcPersistentSession might throw an ezcPersistentException if the requested album does not exist. We skip the handling of that exception here and simply let it bubble up to the main controller catch, to keep the example simple. Manipulating images ------------------- Next we have the run() method. Again we have to fetch user data, but this time from the POST array, namely the title and description of the photo. Since we already know how that code works, I'll skip the code piece here. You can see it at the end of this section, where I will give you the complete code of the run method again. File Upload ----------- :: convertPhoto( $_FILES['photo']['tmp_name'] ); $this->photo = new Photo(); $this->photo->album = $this->album->id; $this->photo->title = $form->title; $this->photo->description = $form->description; ezcGallery::getSession()->save( $this->photo ); $this->movePhoto( $this->photo ); $this->template = 'add_submit.php'; ?> As you can see, the handling of file uploads is not done by UserInput yet. Therefore, we deal with the raw data from the FILES array. The insertion of the photo is similar to the insertion of the album, which we already saw. After that, we move the uploaded photo to its Final Destination ®. Converter --------- Before we look at the code for convertPhoto(), let's jump back to the beginning and examine the getConverter() method of our main controller. Since it is a bit long, I will show it to you in parts. :: The singleton structure is already known from the getSession() method, so let's skip into the if:: This code piece creates a new image converter. This converter can apply different filters to images. While creating the converter instance, we have to provide the handlers the converter should utilize. We select the ImageMagick handler and the one that uses ext/GD. Note that you need ImageMagick installed for this configuration; otherwise you can simply remove the handler. Now we have a working image converter we can use to apply filters to an image. Configuration ------------- Since we already know which filters we need, we define those as a transformation:: createTransformation( 'photo', array( new ezcImageFilter( 'scale', array( 'width' => 640, 'height' => 480, 'direction' => ezcImageGeometryFilters::SCALE_DOWN, ) ), ), array( 'image/jpeg', ) ); ?> An image converter can be configured to handle transformations. Each transformation is identified by a name and contains one or more filters, as well as an output MIME type. In this case, we define a transformation that will be applied to all uploaded photos, because we don't want to waste bandwidth and make Auntie Erna look at an eight mega-pixel photo in full size. All photos will be scaled down to 640x480. If they are already smaller than this size, they will be left untouched (because the filter only scales down). Since JPEG is the best choice for photos, we make this filter output just JPEG images. But we also want to create preview thumbnails of our photos, so we define another transformation:: createTransformation( 'thumb', array( new ezcImageFilter( 'scale', array( 'width' => 150, 'height' => 113, 'direction' => ezcImageGeometryFilters::SCALE_DOWN, ) ), new ezcImageFilter( 'border', array( 'width' => 5, 'color' => array( 255, 255, 255, ), ) ), new ezcImageFilter( 'colorspace', array( 'space' => ezcImageColorspaceFilters::COLORSPACE_SEPIA, ) ), new ezcImageFilter( 'border', array( 'width' => 1, 'color' => array( 0, 0, 0, ), ) ), ), array( 'image/jpeg', ) ); ?> Since Auntie Erna is a little confused by this InterWeb thingy, we want to show her the thumbnails in a familiar way. First we scale the image down, this time much smaller, to 150x113 pixels. Then we add a white border to the image. After that, the image is converted to a Sepia colorspace, which makes it look like a very old-fashioned photo. Last we add another border, which is one pixel thick and black. :: createTransformation( 'photo', array( new ezcImageFilter( 'scale', array( 'width' => 640, 'height' => 480, 'direction' => ezcImageGeometryFilters::SCALE_DOWN, ) ), ), array( 'image/jpeg', ) ); self::$imageConverter->createTransformation( 'thumb', array( new ezcImageFilter( 'scale', array( 'width' => 150, 'height' => 113, 'direction' => ezcImageGeometryFilters::SCALE_DOWN, ) ), new ezcImageFilter( 'border', array( 'width' => 5, 'color' => array( 255, 255, 255, ), ) ), new ezcImageFilter( 'colorspace', array( 'space' => ezcImageColorspaceFilters::COLORSPACE_SEPIA, ) ), new ezcImageFilter( 'border', array( 'width' => 1, 'color' => array( 0, 0, 0, ), ) ), ), array( 'image/jpeg', ) ); } return self::$imageConverter; ?> Applying image transformations ------------------------------ Now we have configured the image converter which is capable of creating the photos we want to display and the thumbnails for previewing. Let's take a look at the convertPhoto() method that applies these transformations:: tmpData['photoPath'] = $imagePath; $this->tmpData['thumbPath'] = tempnam( '', 'thumb' ); try { ezcGallery::getConverter()->transform( 'photo', $this->tmpData['photoPath'], $this->tmpData['photoPath'] ); ezcGallery::getConverter()->transform( 'thumb', $this->tmpData['photoPath'], $this->tmpData['thumbPath'] ); } catch ( ezcImageTransformationException $e ) { throw new Exception( 'Image conversion error: <' . $e->getMessage() . '>' ); } } ?> After the sanity check, we store the paths for image creation globally because we need them later on. We need a second temporary filename for the thumbnail, and we reuse the original temporary location of the uploaded image for the full size version. Now the converter comes in. We first scale down to our desired photo size and then create the thumbnail. Since it is possible that an exception will occur here (like someone uploading something other than a picture), we catch that and repack the error message. That's all. Easy, isn't it? What is missing is just the movePhoto() method, which moves the photo and thumbnail to the data directory. :: tmpData['photoPath'] ); if ( rename( $this->tmpData['photoPath'], 'data/' . $photo->id . '.jpg' ) === false ) { throw new Exception( 'Unable to store photo.' ); } if ( rename( $this->tmpData['thumbPath'], 'data/' . $photo->id . '_thumb' . '.jpg' ) === false ) { throw new Exception( 'Unable to store thumbnail.' ); } } ?> Now you see why we stored the temporary paths. After we insert the information to the database, we know the ID of the photo record. We use this as part of the filename to store our photos. That's it! We'll skip the HTML part again. I think you all know what a form looks like. :) Browsing albums =============== The last missing action is the ability to browse albums, which is located in the show.php file under actions/. In the constructor of this action we retrieve the ID of the album to view and fetch it from the database:: new ezcInputFormDefinitionElement( ezcInputFormDefinitionElement::REQUIRED, 'int' ), ); $form = new ezcInputForm( INPUT_GET, $definition ); if ( !$form->hasValidData( 'album' ) ) { throw new Exception( 'No valid album ID.' ); } $this->album = ezcGallery::getSession()->load( 'Album', $form->album ); } else { throw new Exception( 'No album selected.' ); } } ?> We already know this part. We're using the eZ UserInput component again to fetch the integer ID of the album we need to display. Then we fetch the desired album from the database. So, nothing really special. Let's jump to the run() method. Finding persistent objects -------------------------- :: createFindQuery( 'Photo' ); $query->where( $query->expr->eq( 'album', $this->album->id ) ); $this->photos = ezcGallery::getSession()->find( $query, 'Photo' ); } ?> All we have to do here is fetch all photos from the album. This takes very little code, another benefit of the eZ Database component. First we request a find query object from our persistent session, which will search in the table Photo. Then we add a where expression to the query. We need all records where the field "album" is equal to the album ID we selected. Therefore, we tell the query object to create an expression (more specifically, an equals expression) and give the column and the value as parameters. After that, we instruct our persistent session to find the records defined by this query. Since find always returns an array, we are already done. The rest of this action works as usual. The template gives us a list of thumbnails, linked to the full-size version of the photos. .. image:: ../../images/articles/2006-04-19-image-gallery-browsing_images.png :alt: Browsing images Conclusion ========== Hey, we have a fully functional image gallery application! Sure, it's missing a little functionality (you would soon wish that you could edit and delete albums and photos). But implementing those features should now be a very easy deal for you. So now you have learned how to access your database using the eZ Database component and how to store objects in the database using the PersistentObject package. You saw how to securely deal with user-supplied data using the eZ UserInput component, and how to manipulate images using the ImageConversion package. I hope this tutorial was fun and useful. Thanks for reading! Resources ========= * eZ components `API documentation`__ * eZ components tutorials__ * Article Image Manipulation with eZ components * PECL filter__ extension * OO patterns in PHP __ /docs/api __ /docs/tutorials __ http://pecl.php.net/filter