Setting up a Zend Framework project on Mac OS X Leopard

I’m about to do my first start to finish project on a Mac OS X Leopard. Although I’ve previously moved a project over from an XP, it’s never quite as nice as working on a fresh project with a brand new laptop, so I figured I’d document the procedure as I go through it.

Unfortunately, I didn’t have time to document the process of getting PHP/MySQL/Zend Framework /PHP Unit/Eclipse (as well as Python, Django and Mod WSGI for the CMS) et. al. on the Mac. It wasn’t all that easy, but I will say that in the end I opted for using Zend Server CE, and that part of it was really quite smooth.

It wasn’t my first choice because if there are bugs in a stack, you wind up debugging the stack rather than the components. The experience is not quite as useful. And the more layers between you and the tools you’re using, the less you tend to understand. But ZCE solved too many problems I had that it was well worth using as far as the Mac is concerned.

The scope of this post is to get the browser to display the index view without error and set up a PHPUnit test that looks for the client’s logo in the header, so you may want to get your logo ready.

NOTE: I uses {braces} throughout where you need to replace your own values for mine.

OK let’s get started… First you need a project home. I like to use this:

/Users/{yourname}/Workspace/{clientsname}/

This can be created manually and “used” by Eclipse when creating a new PHP Project, or you can create the directory by just starting a new PHP project from Eclipse and naming it.

Next you need the location of your media files. Essentially this is your public_html (otherwise known as www) folder. I like to use this location:

/Users/{yourname}/Sites/{clientsname}/

You will need to add this location to your PHP include path in Eclipse. (Right click on “PHP Include Path”, go to “Include Path->Configure Include Path”). Add it to the “libraries” tab, click on “Add external source folder”.

Good. Now we’ll use a symbolic link to map your public folder to your media (i.e. public_html) folder. Type the following, replacing your username and client name where appropriate of course:

ln -s /Users/{yourname}/Sites/{clientsname}  /Users/{yourname}/Workspace/{clientsname}/public

Next step I like to keep my own code in the library folder, and have the controllers extend from my base controller. Even if I don’t put anything in my base controller at first, this will save a lot of time later in case I find that something gets called over and over again from many controllers, I can just add it to my base and not worry that the controllers won’t have it available. So create a directory in Library with your name, I use Joed, you pick something else…

/Users/{yourname}/Workspace/{clientsname}/library/{Joed}

You need to add the library directory to your php include path. I did this through php.ini at first. But multiple projects clashed of course and I had to modify this later. If you plan to have more than one project on this machine, skip the next step.

~php.ini step~
If you are also set up with Zend Server CE, the location is /usr/local/Zend/etc/php.ini. Look for the line that says “include_path” and add a colon to the existing path along w/ your library location. E.g.

.:/some/existing/path:/Users/{yourname}/Workspace/{clientsname}/library/

~END php.ini step~

The Zend Framework code will also live in the Library. The best way to handle upgrading and downgrading Zend Framework is to pick a directory, put all versions of ZF there without overwriting each other (e.g. /path/to/ZF/zf1.50, /path/to/ZF/zf1.75 etc…) and then symlink the version you want to run, to your library.

Since I’m using Zend Server CE, I’ll just go with whatever version is already installed. So we need to find the Zend directory, which is located here: “/usr/local/Zend/share/ZendFramework/library/Zend” and create a symlink as follows:

ln -s /usr/local/Zend/share/ZendFramework/library/Zend  /Users/{yourname}/Workspace/{clientsname}/library/Zend

Note that on my installation, ZCE already included the Zend Framework library in the php.ini. So your dev box won’t have the most performant php.ini setup. But no matter, you just need to avoid extra includes where possible on the server.

Now to set up your directory structure, you can save a couple steps by using the new Zend Tool. Although we have a couple of the directories already set up, I just tried creating a project and it installed the files correctly in the directories I wanted…so without further ado, type the following, making sure the path to your installation is correct:

/usr/local/Zend/share/ZendFramework/bin/zf.sh create project {clientname}

Great! Now let’s start to configure things a bit to our liking. In the “/application/Bootstrap.php”, I don’t really like how the modules work by default, so I don’t use it. I just put these lines inside the Zend_Application_Bootstrap_Bootstrap class:

protected function _initAutoload()
{
	$autoloader = new Zend_Application_Module_Autoloader(array(
		'namespace' => '',
		'basePath'  => dirname(__FILE__),
	));
	return $autoloader;
}
protected function _initDoctype()
{
	$this->bootstrap('view');
	$view = $this->getResource('view');
	$view->doctype('XHTML1_STRICT');
}
protected function _initActionHelpers()
{
	Zend_Controller_Action_HelperBroker::addPath(
		APPLICATION_PATH."/controllers/helpers"
	);
}

This way I can autoload the models without having to add prefixes to them. There are other approaches, do what works for you. I will link to a couple other ideas for you to consider:

Zend Framework Modules

Another #ZF Module config

Now open “config/application.ini”. IF AND ONLY IF you’ve added the library to the php.ini, you can remove the includes line from here:

;;includePaths.library = APPLICATION_PATH "/../library"

Now let’s register our custom code directory “/Users/{yourname}/Workspace/{clientsname}/library/{Joed}” into the namespace with the following:

;; custom library
autoloadernamespaces.{joed} = "{Joed}_" ;remove braces and use your custom library directory on this line

Add the layout and view:

resources.layout.layout = "layout"
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.view[] =

And now the database info. Default adapter is if you only have one, it will save you a bit of code when calling your dbAdapter:

;; dB configs
resources.db.adapter = {PDO_MYSQL}
resources.db.params.host = {localhost}
resources.db.params.username = {username}
resources.db.params.password = {password}
resources.db.params.dbname = {clientdb}
resources.db.params.driver_options.PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = true
resources.db.isDefaultTableAdapter = true
resources.db.params.profiler = true

Read my previous post on why I have have MYSQL_ATTR_USE_BUFFERED_QUERY on. And only turn on the profiler if you’re using it.

The rest of the settings in “application.ini” will do for now…

Open your “HOSTS” file, and point 127.0.0.1 to whatever hostname you want to use (e.g. {yourname.clientname.com} ).

Open your “httpd.conf” and add directory specific info if needed…e.g.:

<Directory "/Users/{yourname}/Sites/{clientsname}">
    #
    # Possible values for the Options directive are "None", "All",
    # or any combination of:
    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
    #
    # Note that "MultiViews" must be named *explicitly* --- "Options All"
    # doesn't give it to you.
    #
    # The Options directive is both complicated and important.  Please see
    # http://httpd.apache.org/docs/2.2/mod/core.html#options
    # for more information.
    #
    Options Indexes FollowSymLinks Includes ExecCGI
    #Options Indexes FollowSymLinks MultiViews
    
    #
    # AllowOverride controls what directives may be placed in .htaccess files.
    # It can be "All", "None", or any combination of the keywords:
    #   Options FileInfo AuthConfig Limit
    #
    AllowOverride All

    #
    # Controls who can get stuff from this server.
    #
    Order allow,deny
    Allow from all

</Directory>

Then in the extras folder, open httpd-vhosts.conf and add your VirtualHost info, which should look something like this:

<VirtualHost *:80>
    ServerAdmin {yourname}@{yrmailserver}.com
    ServerName {nameYouChoseInHostsFile}
    DocumentRoot "/Users/{yourname}/Sites/{clientsname}"
    ErrorLog "/usr/local/Zend/apache2/{client}-error_log"
    CustomLog "/usr/local/Zend/apache2/{client}-access_log" common
</VirtualHost>

Restart apache.

BTW, as a tip, in Eclipse, .htaccess files are hidden by default. Assuming you have a PHP project, you should have your directories and files in an explorer-like tree under a tab called “PHP Project”. There is an arrow in that box, select it and on the dropdown click on “Filters”. Then uncheck *.resources. This will enable viewing .htaccess files, although you may see a few Eclipse related system files…which I’d advise ignoring.

It’s up to you where you want to set the APPLICATION_ENV constant. I like to put it in “/public/index.php”, so in .htaccess, I comment it out of “public/.htaccess”. Then in “public/index.php” I change the following line to match the aforementioned setup on the Mac:

|| define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));

becomes:

|| define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../../Workspace/{clientname}/application'));

If you’ve done everything right and try to load up your browser to your client’s location, you will get an error about a missing layout file. So let’s create a layout now. Underneath your “applications” directory, create a “scripts” directory and then a “layout.phtml” file with the following contents:

<?php 

echo $this->doctype() ?>
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head>  
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <title>Hello World!</title>
  <?php echo $this->headLink()->appendStylesheet('/css/global.css') ?>
</head> 
<body>
<div id="header" style="background-color: #EEEEEE; height: 30px;">
    <div id="header-logo" style="float: left">
        <b>Yay it works!</b>
    </div>
    <div id="header-navigation" style="float: right">
		howdy.
    </div>
</div>

<?php echo $this->layout()->content ?>

</body>
</html>

and the error should be gone.

Now let’s create a PHPUnit Test. This post does not cover installing PHPUnit. So before you do the next step, make sure it’s installed by typing “phpunit” from the terminal. If that fails, you need to get it working…

Now inside your tests directory (created by Zend Tool earlier), there should be an empty file called “phpunit.xml”. Open it. Put this there with your changes for {clientname} of course:

<phpunit bootstrap="./application/bootstrap.php" colors="true">
	<testsuite name="{clientname}">
		<directory>./</directory>
	</testsuite>
	
	<filter>
		<whitelist>
			<directory suffix=".php">../application/</directory>
			<exclude>
				<directory suffix=".phtml">../application/</directory>
				<file>../application/Bootstrap.php</file>
				<file>../application/controllers/ErrorController.php</file>
			</exclude>
		</whitelist>
	</filter>
	
	<logging>
		<log type="coverage-html" target="./log/report" charset="UTF-8"
		yui="true" highlight="true" lowUpperBound="50" highLowerBound="80" />
		<log type="testdox" target="./log/textdox.html" />
	</logging>	
</phpunit>

Now we need to create a bootstrap for PHPUnit to use. It should exist already under ‘tests/application/bootstrap.php’, but empty. Add this:

<?php

// Define path to application directory
define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../../application'));

// Testing env has the mock database
define('APPLICATION_ENV', 'testing');

/** Zend_Application */
require_once 'Zend/Application.php';
require_once 'ControllerTestCase.php';

Note that the APPLICATION_ENV = ‘testing’. This lets you send different configuration directives when testing, so be sure and keep that in mind for the “application.ini” as you build your app.

Now create ‘ControllerTestCase.php’ in the ‘tests/application’ directory and put this in it, changing the clientname of course:

<?php
require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';
abstract class ControllerTestCase
	extends Zend_Test_PHPUnit_ControllerTestCase
{
	/**
	 * @var Zend_Application
	 */
	protected $application;

	public function setUp()
	{
		$this->bootstrap = array($this, 'appBootstrap');
		// setup the host var
		$_SERVER['HTTP_HOST'] = '{clientname}';//this can probably be done on phpunit command line
		parent::setUp();
	}
	public function appBootstrap()
	{
		$this->application = new Zend_Application(APPLICATION_ENV,
											  APPLICATION_PATH . '/configs/application.ini');
		$this->application->bootstrap();
	}
}

I’ve set the $_SERVER[‘HTTP_HOST’] here because in my previous project it helped me test certain things that were expecting the HTTP_HOST that I would get when visiting the page through the browser but wasn’t appearing through PHPUnit. I later found out that there’s a switch in the command line to provide this functionality, so if you feel like digging through the manual and using that instead, you can remove that line.

Now under ‘/tests/application’ create a directory called ‘controllers’ and create a file in there called ‘IndexControllerTest.php’ with these contents:

<?php
/**
 * @group controllers
 * @group indexcontroller
 * @author {Joe Devon}
 *
 */
class IndexControllerTest extends ControllerTestCase
{

	public function setUp()
	{
		/* Setup Routine */
		parent::setUp();
	}

	public function tearDown()
	{
		/* Tear Down Routine */
		parent::tearDown();
	}

	public function testCanDoUnitTestInIndexController()
	{
		$this->dispatch('/');
		$this->assertTrue(true);
	}

	/**
	 * @group index
	 * @return void
	 */
	public function testIndexAction()
	{
		$this->resetRequest()
			 ->resetResponse();
		$this->dispatch('/');
		$this->assertNotRedirect();
		$this->assertController('index');
		$this->assertAction('index');
		$this->assertResponseCode(200);
	}

	public function testErrorURL()
	{
		$this->dispatch('/index/foo');
		$this->assertController('error');
		$this->assertAction('error');
		$this->assertResponseCode(404);
	}

	public function testIndexLoadsDefaultHeaderSuffix()
	{
		$this->dispatch('/index');
		$this->assertQueryContentContains('div#navigation', 'logo.jpg');
	}
}

Now from a terminal window, run:

$ phpunit --verbose

You should see 4 tests, 9 assertions and 1 failure. The failure is caused by the lack of a logo in the view. If you aren’t clear on why, make sure to read the code above. We will need to create a view for index and a div id=”navigation” inside the view, and put ‘logo.jpg’ inside the div.

If you don’t know why we created a test before the code to make the test pass, it’s because we’re doing what’s called Test Driven Development. TDD is beyond the scope of this post, but look it up if you aren’t familiar with it. You will definitely learn something interesting…

We’re almost done. We just need to clean up a couple of things and we’ll get this last test to pass.

Remember we created a “/Users/{yourname}/Workspace/{clientsname}/library/{Joed}” directory? In there, create a file called ‘Controller.php’ with these contents:

<?php
/*
 * Class: {Joed}_Controller
 *
 * Base controller
 */
class {Joed}_Controller extends Zend_Controller_Action
{

	/*
     * Pre-dispatch routines
     *
     * Called before action method. If using class with
     * {@link Zend_Controller_Front}, it may modify the
     * {@link $_request Request object} and reset its dispatched flag in order
     * to skip processing the current action.
     *
     * @return void
     */
	public function preDispatch()
	{
		parent::preDispatch();
	}

    /**
     * Post-dispatch routines
     *
     * Called after action method execution. If using class with
     * {@link Zend_Controller_Front}, it may modify the
     * {@link $_request Request object} and reset its dispatched flag in order
     * to process an additional action.
     *
     * Common usages for postDispatch() include rendering content in a sitewide
     * template, link url correction, setting headers, etc.
     *
     * @return void
     */
	public function postDispatch()
	{
		parent::postDispatch();
	}
} // end class

I’ve got a couple of methods I used for my last project in here that I should publish, but I got to make it more generalized and reusable first. Needless to say, you need your own name in the code and you will use this file to put in custom code that you will need your controllers to extend from.

Now open /Users/{yourname}/Workspace/{clientsname}/application/controllers/IndexController.php and change ‘class IndexController extends Zend_Controller_Action’ to read ‘class IndexController extends {Joed}_Controller’.

Finally, open /Users/{yourname}/Workspace/{clientsname}/application/views/scripts/index.phtml and point to your logo with a minimum of this sprinkled somewhere in your view:

<div id="navigation">
<img src="logo.jpg">
</div>

Now you should get no failures on your tests and you are ready to begin!

If you enjoyed this post and want to follow my posts in the world of Zend Framework, MySQL & the web, consider “following” me on twitter @joedevon.

3 responses to “Setting up a Zend Framework project on Mac OS X Leopard

  1. Great post Joe! Especially including the TDD bit.

  2. I’ve been wanting to try out Zend for a while, thanks for writing this up

  3. uff, it must take you a couple of hours every time you have to set up a new client!🙂

    I keep this in my httpd and now i only need to create a folder in my sites folder with a public_html inside and then i can create new projects with the zf.sh tool🙂

    AllowOverride All

    ServerName home.art-box.dk
    ServerAlias *.home.art-box.dk *.fugl.local
    ServerAdmin root@localhost
    VirtualDocumentRoot /Users/ranza/Sites/%1/public_html/
    UseCanonicalName Off

    Alias /sf /Applications/MAMP/bin/php5/lib/php/data/symfony/web/sf

    LogLevel debug
    ErrorLog /Applications/MAMP/logs/error.log
    CustomLog /Applications/MAMP/logs/access.log common

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s