Saturday, January 27, 2007

Setting Up the Zend Framework

Setting up the Zend Framework is easy even on shared hosts since no binaries need to be installed. But if you don't know the intricacies of Apache and PHP, certain requests can fail to work for seemingly no apparent reason.

I originally learned from an older version of a tutorial called Getting Started with the Zend Framework [pdf] from Akra's DevNotes, so I will follow his lead with a few stylistic choices. You may want to check out that tutorial or a newer version of it if it's out.

This is just a bare-bones setup. Nothing fancy. No databases, forms, etc. Just "hello world" to get you started. You'll want to get this working before moving on, of course.
  1. Zend Library
    I'm using version 0.7.0. Unzip ZF and copy the library directory (ZendFramework-0.7.0/library/) into your webroot so that you have /webroot/library/. This directory should contain Zend.php and a Zend subdirectory. This is really all you need to start using Zend, but there are a few standard things that still need to be done for a website.
  2. PHP Config
    You'll want to make sure the following two php.ini settings are set.
    register_globals = Off
    magic_quotes_gpc = Off


    If you're using PHP as an Apache module, remember to restart Apache for changes to take effect.
  3. Apache Config
    Right now, anyone can navigate to or request a file in your library directory. To prevent this, use a .htaccess file in the library directory, /webroot/library/, with the following directive. You will also want to copy this into your application directory, /webroot/application/, where you store all your application-specific MVC files.
    deny from all
  4. Bootstrapping
    Bootstrapping allows all URL requests to be processed through a single entry-point, called the bootstrap file. This way, URLs can be mapped to controller actions. In /webroot/.htaccess, add the following lines which internally redirect all URLs to the index.php file.
    RewriteEngine on
    RewriteBase /
    RewriteRule .* index.php


    However, certain files like client scripts, style sheets, and images should be served as normal without involving the framework. So in your public directory, /webroot/public/, where such files are stored, add the following in another .htaccess file.
    RewriteEngine off

    If you're using phpMyAdmin in a subdirectory on a local server, you'll want to do the same for that directory to serve files as usual.

    Now /webroot/index.php is the bootstrap file and the entry-point for all requests, besides in the /webroot/public/ directory.
  5. Standard Bootstrap File and Dispatching
    The following is a bare-bones bootstrap file that uses standard routing. Put this in /webroot/index.php.
    <?php
    error_reporting(E_ALL|E_STRICT);
    date_default_timezone_set('America/New_York');

    // Load Zend.
    set_include_path('.' . PATH_SEPARATOR . './library'
    . PATH_SEPARATOR . './application/models/'
    . PATH_SEPARATOR . get_include_path());
    include 'Zend.php';

    // Load classes used on all requests.
    Zend::loadClass('Zend_Controller_Front');

    // Setup controller.
    $frontController = Zend_Controller_Front::getInstance();
    $frontController->setControllerDirectory('./application/controllers');

    // Turn on display of exceptions.
    $frontController->throwExceptions(true);

    // Dispatch and run.
    $frontController->dispatch();
    Leaving out the closing ?> is intentional since including it in pure PHP files can lead to unwanted output and hard-to-detect bugs.
  6. Index Controller and Standard Routing
    To set up a simple index page, create a new file /webroot/application/controllers/IndexController.php and add the following.
    <?php

    class IndexController extends Zend_Controller_Action
    {
    function indexAction()
    {
    echo '<p>in IndexController::indexAction()</p>';
    }

    function fooAction()
    {
    echo '<p>in IndexController::fooAction()</p>';
    }
    }

    Now, visiting http://hostname/ should display a page with "in IndexController::indexAction()", and visiting http://hostname/index/foo/ should display a page with "in IndexController::fooAction()". Of course, due to the way the default router works, all the following URLs display the same thing.
    http://hostname/
    http://hostname/index
    http://hostname/index/
    http://hostname/index/index
    http://hostname/index/index/
    The default router looks at a URI and matches against the pattern /controller/action/param1/value1/param2/value2. http://hostname/ defaults to the index action of the index controller, which will be identical to http://hostname/index/index. This is also identical to http://hostname/index/index/ because the default router trims leading and trailing slashes. (Curiously, http://hostname/index returns a 400 Bad Request error on my shared hosting site; I'm not sure why. I never noticed this before.)

    If you're having the problem of navigating to http://hostname/index/index failing (returning a 404) even when http://hostname/ works — a problem I ran in to working on a shared host — you must make a dummy /webroot/index/ directory with a .htaccess file telling Apache to use the rewrite router. In other words, create the directory /webroot/index/, and copy /webroot/.htaccess into it. This must be done for each subdirectory (equivalently, each controller) that you wish to have. So if you want /downloads/ to be a valid URI, you must make a /webroot/downloads/ directory. If you want /archives/ to be a valid URI, you must make a /webroot/archives/ directory, and so on. Each with the .htaccess file specifying the RewriteRule. The cause for this has to do with Apache's configuration. However, I haven't been able to determine the exact reason since the shared host I'm using does not allow access to the config file.

Setting Up a Local Server

I've always found it to be helpful to have a local server, duplicating the production server as close as possible, while developing. This saves you from having to upload changes every two seconds.

I've set up phpMyAdmin/PHP/MySQL/Apache on Windows a few times. And I'm just now getting to the point where I don't get stuck somewhere along the line.

I know what you're thinking — not another tutorial on installing PHP/MySQL/Apache. But I bet none of the ones out there were geared towards setting up a server for the Zend Framework.
  1. Apache
    I'm using version 2.0.58. Make sure to test that this works alone before moving on.
  2. MySQL
    I'm using version 5.0.21.
  3. PHP
    I'm using version 5.2.0. Use the zip package, not the installation program. The installer doesn't include certain extensions. Unzip it to C:\php\. You could choose another directory, but I have avoided the Program Files directory since spaces in paths have been known to cause problems with some programs.

    Add PHP to your Apache config file. In Apache 2 using PHP as a module, it's as follows, assuming you unzipped PHP into C:\php\.
    LoadModule php5_module "C:/php/php5apache2.dll"

    If you're using the Apache module for PHP, you can set the following in the Apache config file to specify the directory where the php.ini file is. This is better than having to place it in the Windows directory.
    PHPIniDir "C:/php"

    Using the framework, you also want to load the rewrite module so that all requests can be sent to a bootstrap file. So make sure to uncomment or include this.
    LoadModule rewrite_module modules/mod_rewrite.so

    Tell Apache to look for index.php, and not just index.html, when directories are requested. The index.html.var is only needed here if you're using content-negotiated documents. If you don't know what they are, you're probably not using them.
    DirectoryIndex index.html index.php index.html.var

    Set the MIME type for php files.
    AddType application/x-httpd-php .php

    Edit the php.ini file and make sure the extension directory is set to where your PHP extensions are located, usually C:\php\ext\.
    extension_dir = ".\ext"

    Uncomment the MySQL extension.
    extension=php_mysql.dll

    Also uncomment the multibyte string extension if you plan on using exotic character sets in your database. phpMyAdmin will cry if you don't enable this.
    extension=php_mbstring.dll

    For the Zend Framework, uncomment or add the following lines if not present. This enables the PHP Data Objects extension which Zend uses for accessing the database — in this case, MySQL.
    extension=php_pdo.dll
    extension=php_pdo_mysql.dll


    While you're editing php.ini, turn off register_globals and magic_quotes. Turning off register globals is for safety, and turning off magic quotes reduces complication later when inserting data into the database. I have seen a ZF tutorial that states that turning off magic_quotes is required, but I haven't tested this.
    register_globals = Off
    magic_quotes_gpc = Off


    Remember to restart Apache to make configuration changes take effect. If you're using PHP as an Apache module, php.ini is only loaded once on startup. So changes to php.ini will not take effect until Apache restarts.
  4. phpMyAdmin
    I'm using version 2.8.0.3. phpMyAdmin won't work out of the box. There's a documentation.html file that explains (under the Quick Install section) creating a config file.

...More details on all this later. (I know what you're thinking... yeah, right.)

Scope

Here are the things I've been working on:
  • MySQL DB for persistence
  • Registration/Login
  • User-Uploaded Content
  • Permissions

Things I'm specifically not working on at the moment:
  • Performance Optimization
  • Internationalization

About

I've been using the Zend Framework since version 0.2.0. I had never used a web framework prior to that. The reason I chose Zend as opposed to other frameworks was its simplicity and intuitiveness. I admit, this conclusion may have only been drawn because of a well-written tutorial that clicked with me.

Nevertheless, I have been using the Zend framework to build a website and have been following the core classes as they've developed. As I ran in to problems, I looked for help on the web and found it lacking. So I decided to create what I wished to see.

Let's see how long this will last.