PHOCOA is an object-oriented, event-driven, componentized, MVC (model-view-controller) web application framework based on Apple's Cocoa architecture.

PHOCOA's primary intent is to make developing web applications in PHP easier, faster, and with fewer bugs. The framework handles most of the "dirty work" of programming by removing the need to write much glue code for data binding (moving data between the model layer and view layer of your application), validation, error handling, request processing, etc. Most of your time will be spent designing your GUI and writing application-specific logic rather than dealing with form data, database calls, etc.

PHOCOA relies on several technologies to work its magic:

PHOCOA itself provides the controller layer of the MVC architecture.

PHOCOA itself contributes several technologies to the framework.

Developing web applications presents a variety of challenges. Listed below are a number of classic web development architectural problems, and how PHOCOA provides solutions.

PHOCOA has a very large set of technologies. Because it is based on Cocoa (Apple's development infrastructure), if you are a Cocoa programmer things will make a lot of sense to you. If you are not familar with Cocoa, there is a bit of a learning curve. But trust us, it's worth it. The power of PHOCOA will allow you to deliver robust web applications with minimal coding in record time.

Instead of starting off by explaining all of the concepts and technologies, we will first walk you through a simple application to show you how easy the finished product is. You will be much more motivated to learn the concepts when you realize how much time PHOCOA can save you.

This chapter will walk you through the development of a simple "Hello, World" application that shows off all of the basic concepts. We will explain each concept as simlpy as possible for the example.

The PHOCOA framework is contained in its own directory. Your PHOCOA-based web application will live in its own directory, separate from the framework code. This makes it easy to keep the two separated for purposes of backup, upgrading, etc.

PHOCOA Directory Structure

First let's install the PHOCOA framework. Unpack the PHOCOA tarball. The directory structure looks like:

$ ls -l phocoa/ total 0 drwxr-xr-x 4 alanpins staff 136 Aug 2 10:34 classes drwxr-xr-x 5 alanpins staff 170 Oct 15 16:53 conf drwxr-xr-x 7 alanpins staff 238 Oct 17 14:57 docs drwxr-xr-x 34 alanpins staff 1156 Oct 17 13:57 framework drwxr-xr-x 9 alanpins staff 306 Oct 16 12:04 modules drwxr-xr-x 6 alanpins staff 204 Oct 15 16:50 phing drwxr-xr-x 4 alanpins staff 136 Aug 2 10:33 skins drwxr-xr-x 5 alanpins staff 170 Aug 2 10:33 smarty drwxr-xr-x 5 alanpins staff 170 Aug 2 10:34 wwwroot

The classes directory contains a skeleton of the basic application infrastructure needed to have a functional application. This will be copied to your application's modules directory during install.

The conf directory contains default versions of all configuraiton files. These will be copied to your application's conf directory during install.

The docs directory contains a complete PHPDoc API reference for the framework. Once you have PHOCOA installed, this can be reached from http://your-server.com/docs.

The docs directory shipped with PHOCOA does not include built documentation. Use PHPDOC to build the docs with the following command. Eventually, this will be set up as a Phing task. The docs are also available online at http://phocoa.com/docs/.

$ cd phocoa $ phpdoc -dn framework-base -t docs/phpdocs -ti "PHOCOA Documentation" -o HTML:frames:default \ --ignore test/ -d framework -f "smarty/plugins/*" -f "conf/webapp.conf"

The framework directory contains most of the framework's code.

The modules directory contains modules that are used by the core framework, or are bundled with the framework. These will be copied to your application's modules directory during install.

The phing directory contains the phing buildfiles for PHOCOA.

The skins directory contains bundled skins. This is just a single skin, so that your application has some skin when it starts. These will be copied to your application's skins directory during install.

The smarty directory is where templates and plugins used by the framework go.

The wwwroot directory is the public wwwroot. This wwwroot contains the bootstrapping code for a PHOCOA application and a directory for all public www documents. This will be copied to your application's wwwroot directory on install.

Using Phing to Start a New Project

PHOCOA uses Phing to help with managing your web application. Let's create a new project.

phing -f phocoa/phing/build.xml Buildfile: /Users/alanpinstein/dev/sandbox/phocoa/phocoa/phing/build.xml phocoa > prepare: [echo] PHOCOA framework base dir at: /Users/alanpinstein/dev/sandbox/phocoa/phocoa/phing/.. phocoa > newproject: Enter the name of the new project: helloworld Enter the path to create the new project in: /Users/alanpinstein/dev/sandbox Enter the name of the server (ie dns name) that will host this application: [localhost] Enter the IP of the server that will host this application: [127.0.0.1] Enter the PORT of the server that will host this application: [80] [echo] Creating a template for new project in /Users/alanpinstein/dev/sandbox/helloworld [echo] Creating project directories and setting up permissions... [mkdir] Created dir: /Users/alanpinstein/dev/sandbox/helloworld [mkdir] Created dir: /Users/alanpinstein/dev/sandbox/helloworld/helloworld [mkdir] Created dir: /Users/alanpinstein/dev/sandbox/helloworld/log [chmod] Changed file mode on '/Users/alanpinstein/dev/sandbox/helloworld/log' to 777 [mkdir] Created dir: /Users/alanpinstein/dev/sandbox/helloworld/runtime [chmod] Changed file mode on '/Users/alanpinstein/dev/sandbox/helloworld/runtime' to 777 [mkdir] Created dir: /Users/alanpinstein/dev/sandbox/helloworld/runtime/smarty/templates_c [chmod] Changed file mode on '/Users/alanpinstein/dev/sandbox/helloworld/runtime/smarty/templates_c' to 777 [echo] Copying PHOCOA templates... [copy] Copying 905 files to /Users/alanpinstein/dev/sandbox/helloworld/helloworld [echo] Setting up configuration files... [copy] Copying 2 files to /Users/alanpinstein/dev/sandbox/helloworld/helloworld [filter:ReplaceTokens] Replaced "##SERVER_IP##" with "127.0.0.1" [filter:ReplaceTokens] Replaced "##SERVER_PORT##" with "80" [filter:ReplaceTokens] Replaced "##SERVER_NAME##" with "localhost" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_BASE_DIR##" with "/Users/alanpinstein/dev/sandbox/phocoa/phocoa/phing/.." [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_CONTAINER_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_CONTAINER_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_BASE_DIR##" with "/Users/alanpinstein/dev/sandbox/phocoa/phocoa/phing/.." [filter:ReplaceTokens] Replaced "##PHOCOA_APP_CONTAINER_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_CONTAINER_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld" [filter:ReplaceTokens] Replaced "##PHOCOA_APP_DIR##" with "/Users/alanpinstein/dev/sandbox/helloworld/helloworld" [phingcall] Calling Buildfile '/Users/alanpinstein/dev/sandbox/phocoa/phocoa/phing/build.xml' with target 'httpdconf' phocoa > httpdconf: [echo] Make sure your httpd.conf file contains the line: Include /Users/alanpinstein/dev/sandbox/helloworld/helloworld/conf/httpd.conf BUILD FINISHED Total time: 2 minutes 51.07 seconds

You should now have a directory helloworld containing your new PHOCOA application.

$ ls -l helloworld alanpinstein@g5:~/dev/sandbox total 0 drwxr-xr-x 7 alanpins staff 238 Oct 17 15:03 helloworld drwxrwxrwx 2 alanpins staff 68 Oct 17 15:02 log drwxrwxrwx 3 alanpins staff 102 Oct 17 15:02 runtime

This directory contains your PHOCOA deployment structure. Typically, log and runtime (tmp / cache) files are not part of your source code, so PHOCOA sets up a wrapper directory containing these items.

The helloworld directory inside of this deployment structure is your PHOCOA applications directory.

This "inner" directory is the root directory of your PHOCOA application, and is what you should check in to your version control system. The log/ and runtime/ directories are typically not versioned resources.

Application Directory Structure

Now let's have a look at the directory structure.

$ ls -l helloworld/helloworld total 20 drwxrwxr-x 3 apinstein ttimobile 4096 May 31 07:35 classes drwxrwxr-x 2 apinstein ttimobile 4096 May 31 11:12 conf drwxrwsr-x 9 apache ttimobile 4096 May 30 15:53 modules drwxrwxr-x 5 apinstein ttimobile 4096 May 30 18:14 skins drwxrwxr-x 3 apinstein ttimobile 4096 May 31 09:00 wwwroot

The classes directory is where all of your classes go. These are classes specific to your application.

The conf directory contains all configuraiton files.

The modules directory is where all components go. These components are the building blocks of your application and include both entire pages and sub-components.

The skins directory is where all skins go.

The wwwroot directory is a public wwwroot that contains the front controller for the PHOCOA project. All public documents (i.e. the tradiditional public www root) go in wwwroot/www/.

Initial Configuration

First thing to do is configure the framework.

$ ls -l helloworld/helloworld/conf alanpinstein@g5:~/dev/sandbox total 24 -rw-r--r-- 1 alanpins staff 1272 May 18 14:24 httpd.conf -rw-r--r-- 1 alanpins staff 1561 May 18 12:58 webapp.conf

The httpd.conf file is the Apache configuration for your application. You should customize it to match your domain and path to where you put PHOCOA. Most of this was done automatically by the Phing newproject build. Examine the file to be sure it matches what you want. You can of course make any corrections to mistakes you may have made earlier regarding hostname, port, etc. Also, examine the WebDAV setup (optional - feel free to comment this out if you don't have or want WebDAV access). We assume that you are familiar with apache setup.

WebDAV can be useful when working on an application remotely. Since PHOCOA includes a Mac OS X application to graphically manage some configuration files, using WebDAV enables you to use the PHOCOA Builder Application on your computer, but working on a PHOCOA application directory on a remote computer. For this reason, you need only share the "modules" directory of your webapp.

Then, include your application's httpd configuration file in your main httpd.conf like so:

Include /path/to/helloworld/helloworld/conf/httpd.conf

Now restart Apache.

Next up is the PHOCOA conf file. Open up webapp.conf. Read through it, setting up each directive as explained by the comments. It should also be set up correctly because of the Phing task. You may need to tweak the PHP include_path setting to ensure that PHOCOA can see your smarty install.

At this point, you should be able to access the site via the web. You should be able to access the application via http://servername/.

Now, we can move on to the tutorial!

Modules

The basic unit of work in a PHOCOA application is the module. A module is a collection of views and code related to a single function. It is conceptually the same as a single PHP file in a traditional PHP application. There may be multiple actions, web pages, etc, but it's all stored in one file.

Each PHOCOA module is a single PHP file that contains a single WFModule subclass. There is a naming convention to the subclass names. The subclass should be named "module_<dirName>" where <dirName> is the name of the directory that the module sits in. This helps prevent name collisions with other classes. This subclass will have specially named functions that you create to set up pages and respond to user actions.

Because each module has multiple pages, actions, etc., each module in PHOCOA is essentially a reusable web component. The components can be used one-at-a-time to build complete pages, or you can composite modules together to create complex layouts and behaviors.

Modules are invoked via an invocationPath which looks like path/to/module/pageName/param1/param2. Obviously this looks a lot like a URL. When you go to a URL of a PHOCOA application, the request controller parses out the invocationPath from the URL and executes the module. Modules that include other modules simply supply the invocationPath directly.

Pages

Each module can contain an arbitrary number of pages. A page is simply a single web view that a user can see. You can think of it as a web page, a view, a screen, whatever works for you. For instance, you may have one view that is an input form, and another view that is used for telling the user the form's action succeeded.

Actions

Each page has a number of actions. Actions are trigger by the user submitting a form. When the user submits a form, PHOCOA will hand off control to an action handler in your module where you can respond to the action.

It should be noted that not all requests have actions. It is possible to just load a page in a module WITHOUT an action. In this case, the module can display the default page, or could look in the parameters passed to it to find information used to load default data into the page.

Let's put PHOCOA to work with a simple example. We start out by creating a new module, "helloworld".

Your new project alread has a completed helloworld module, so we'll just explain how it was built.

To create a new module, we have a Phing task to help us:

$ php5 /Users/alanpinstein/dev/sandbox/phocoa/phocoa/framework/createModule.php helloworld helloworld

This script isn't yet wrapped by Phing yet so we just run the script directly.

The createModule task automatically creates a new module with a single page.

class module_helloworld extends WFModule { /** * Tell system which page to show if none specified in the URL. */ function defaultPage() { return 'helloWorld'; } }

WFModule is an abstract class, requiring only one method, "defautPage()". This method simply tells the system what page to load if none is specified.

Next, let's examine the helloWorld page.

Each page in PHOCOA has three parts. The .tpl file (the HTML Smarty template), the .instances file (contains a list of all WFView instances used in the template), and the .config file (contains the configuration of the WFView instances declared in the .instances file). PHOCOA needs a list of all UI instances used in the view so that it can work its magic in maintaining state, etc. The .config file is used to configure each widget, for instance by specifying labels, sizes, formatters, and other attributes. The .config file is also used to set up Bindings, which is how PHOCOA links UI widget data to your model. Don't worry about these details for now, though; we'll explain further as we go along.

In the following examples, the contents of .instances and .config files will be shown. These files are PHP files and need to have the PHP tags: <?php ... ?> surrounding any PHP code you see, but for brevity they have been left out.

PHOCOA includes a Phing task to add a page to an existing module as well. This script should be run from inside the module directory that you want to add the page to.

$ php5 /Users/alanpinstein/dev/sandbox/phocoa/phocoa/framework/createPage.php helloworld

This script isn't yet wrapped by Phing yet so we just run the script directly.

The createModule script uses the createPage script to create the default page. The helloworld module already has a helloworld page, so you don't need to run it again.

Now, edit the helloWorld.tpl file with a classic message:

Hello, World!

That's all that's needed to make a simple web page in PHOCOA. Simply load up the URL for this page: http://localhost/webapp/helloworld and you should see the helloworld page!

One of PHOCOA's biggest benefits is that it automatically maintains state of all GUI widgets across requests. Without any code at all, you can have an arbitraily complex form which automatically remembers all values perfectly from one request to another. PHOCOA also has integrated data validation and error handling that keep track of errors on a field-by-field basis. Thus, PHOCOA greatly simplifies the creation of user-friendly forms that automatically remember their state and show errors on a per-field basis.

Our example also includes a form, which you should have seen when you loaded the URL. Let's examine the helloWorld.tpl file to see how the form is set up:

{WFForm id="form"} Email: {WFTextField id="email"} {WFSubmit id="submit"} {/WFForm}

Now we need to declare these three widgets in the .instances file.

If you're using Mac OS X Tiger, you can use the PHOCOA Builder application to manage the .instances and .config files graphically.PHOCOA Builder

The end result should be that the helloWorld.instances file looks like:

$__instances = array( 'form' => array('class' => 'WFForm', 'children' => array( 'submit' => array('class' => 'WFSubmit'), 'email' => array('class' => 'WFTextField'), ) ) );

The format of the .instances file is a PHP associative array, with the first level of keys being the instance ID. The instance ID is the unique ID for this page for the given instance. Then, for each instance, you must declare the class, and optionally children. As you can see above, WFForm is a container widget that contains other widgets. You can tell this because the {WFForm} tag in the .tpl file has a closing tag.

When PHOCOA processes the .instances file, it will create an instance of the given class for the given ID. The rest of the system then can interact with the widgets by ID. In code, you can access a widget with:

$myTextField = $page->outlet('MyTextFieldInstanceID');

So, what action gets run when the user submits this form? The action methods are simply specially named methods of the WFModule subclass. Here's the prototype:

<pageName>_<actionID>_Action($requestPage)()

So, we now need to define a function in our WFModule subclass to run when the submit button is clicked. Add this to the helloworld.php file:

function helloWorld_submit_Action($page) {}

For now, our action method won't do anything. However, we have to declare it or the framework will throw an exception.

So, now load the page again and you should see a form. Enter in some data and press the submit button. You will notice that the form will be re-displayed with the data you entered still there. This is a simplified example of PHOCOA's state-maintenance capabilities. Any form elements that are added will automatically have their state maintained. All HTML form elements are available.

While it's somewhat interesting to see the form maintain its state, it's not particularly useful on its own. Typically a form submission will need to do something, and typically that will involve moving the data from the form to an object, validating the data, and eventually performing an action. Sometimes, you'll also need to format the data in a more human-readable way than it is stored, for instance, timestamps.

The glue layer between the GUI and the model is another huge benefit of PHOCOA. Based on Apple's Controller Layer, PHOCOA automates the process of moving data between the GUI elements and the data layer. Instead of writing code to move the data between the layers, the PHOCOA controller layer does this automatically. All you have to do is configure the GUI elements to "bind" them to your model objects. This is done without any coding, and once it's done, the controller layer automatically keeps your GUI in sync with your data as well as automatically providing data validation and error handling.

To extend our example and really explore PHOCOA, we're going to build a classic web application, a form-to-email page.

First, let's define our new class, ExampleEmail. Normally each class is stored in its own file, but for simplicity, we'll just put our ExampleEmail class in helloworld.php. Add the following:

class ExampleEmail extends WFObject { protected $toEmail; protected $subject; protected $message; function send() { $sent = mail( $this->toEmail, $this->subject, $this->message ); return $sent; }}

Now, of course, we also need a bigger form, so edit helloWorld.tpl:

{WFForm id="form"} Email: {WFTextField id="email"}<br /> Subject: {WFTextField id="subject"}<br /> Message: {WFTextArea id="message"}<br /> {WFSubmit id="submit"} {/WFForm}

Since we've added components to the page, we need to declare them in the helloWorld.instances file, which should now look like:

$__instances = array( 'form' => array('class' => 'WFForm', 'children' => array( 'subject' => array('class' => 'WFTextField'), 'message' => array('class' => 'WFTextArea'), 'submit' => array('class' => 'WFSubmit'), 'email' => array('class' => 'WFTextField'), ) ), );

Now, of course we need to link the form elements to our new ExampleEmail class. This is where PHOCOA starts to get really interesting. You won't have to write ANY code to make this happen!

First off, we will need an instance of the ExampleEmail class, right? Well, just like there is the helloWorld.instances file, there is also a mechanism for instantiating objects for the module. These are called Shared Instances, and they are conceptually similar to instantiating objects in a nib file in Cocoa. You can create a shared.instances file, and configure those instances with a shared.config file. All instances declared in shared.instances will be created automatically when your module is initialized.

So, let's set up an ExampleEmail instance! Create a file called shared.instances, and enter the following:

$__instances = array( 'email' => 'ExampleEmail',);

Of course, once again if you're on Mac OS X Tiger, you can use PHOCOA Builder to create a shared instance.

Now we are guaranteed to have an instance of ExampleEmail set up as an instance variable of our helloworld class.

Next up, we need to link the form elements to the members of our ExampleEmail class. For this, we need to introduce a concept called Bindings. Bindings are a way to link the property of one class to the property of another class.

If you're familiar with COCOA, these work very similarly to bindings in Cocoa, with the small difference that in PHOCOA the bindings are not implemented with a real-time observer pattern. Since PHP is stateless, we decided to simply push values from bindings once at the beginning of the request cycle, and pull values from bindings once at the end of the request cycle, just before rendering.

So, to set up the bindings we're going to edit the helloWorld.config file, to the following:

$__config = array( 'subject' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'subject', ), ), ), 'message' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'message', ), ), ), 'email' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'toEmail', ), ), ), );

Once again, if you're using PHOCOA Builder, you can do this graphically.

To explain what's happening here, let's look at subject first. We create a 'bindings' key for 'subject', and declare that we want to bind the "value" property of the "subject" instance (which is a WFTextField). We want to bind the "value" property to the "email" instance variable of our helloworld class, and we want to link it to the "subject" property of the "email" instance. For now, don't worry about the controllerKey.

What is a modelKeyPath? Let's introduce another concept that we call Key-Value Coding. This again will be familiar to Cocoa developers. Basically, the idea is that classes have a number of properties (their members / instance variables). Key-Value Coding provides a generic mechanism for accessing or mutating these members. The following generic functions:

function valueForKey($key); function setValueForKey($value, $key);

provide access to get/set any properties of classes derived from WFObject. If you'll remember, our ExampleEmail class extends WFObject, and this is why.

So, we can call:

$email->setValueForKey('Test Message', 'subject')

and this will update the subject of our exampleEmail class. The binding that we've just configured will now do this for you automatically. It will also take its value from the email instance, if there is one. The same is also true for the message and email fields.

The valueForKey() / setValueForKey() functions try a variety of approaches to access the property's data. First, they will use accessors with a particular naming convention: <key>()() and set<Key>()() where <key> is the property name. For instance, emailTo()() would be used as the accessor for the emailTo member, and setEmailTo($value)() for the mutator. If these specially named accessors are not available, the system will try to access the member directly.

One last thing before we try it out. Of course, we need to have our action handler call the send() method on our email instance when the form is submitted, right? So, modify the action handler to the following:

function helloWorld_submit_Action($page) { $this->email->send(); }

As mentioned previously, because of the setup we did in shared.instances, we're guaranteed to have an instance variable in our helloworld class populated with an ExampleEmail instance. So we need only to tell it to send!

Your mailer should work now. Try it out by sending an email to yourself. Assuming your mailer setup for PHP is correct, you should get an email!

At the end of the last section, we had a working mailer. However, the UI didn't change when the user submitted the form. Probably we'll want to display a "success" message when the mail is sent. Let's walk through the steps to display a different page as a response to an action.

First, let's create the success page. Use the handy createPage script again to create a page named "emailSuccess", then edit the template to look like this:

<p>Successfully sent email!</p> To: {WFLabel id="email"}<br /> Subject: {WFLabel id="subject"}<br /> Message: {WFLabel id="message"}

Now, set up the emailSuccess.instances:

$__instances = array( 'message' => array('class' => 'WFLabel'), 'subject' => array('class' => 'WFLabel'), 'email' => array('class' => 'WFLabel'), );

and emailSuccess.config:

$__config = array( 'message' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'message', ), ), ), 'subject' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'subject', ), ), ), 'email' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'toEmail', ), ), ), );

Now, in the our action handler, we want to show the success page if the email was sent, so we add a line to set up the reponse page to use:

function helloWorld_submit_Action($page) { $this->email->send(); $this->setupResponsePage('emailSuccess'); }

Try submitting the email form again. This time, you should see the success page you've just created.

Let's add some validation to our email class. For instance, let's say that we want to require a properly-formatted email address, and the subject cannot be blank. Of course, we'll also want to show the errors to the user. How to accomplish this with PHOCOA?

It's time once again to introduce another concept: Key-Value Validation. Similar to Key-Value coding, the Key-Value Validation mechanism looks for specially-named functions of your class, with the following prototype:

boolean validate<Key>(&$value, &$edited, &$errors)

It is very important to notice the pass-by-reference used on all three parameters.

So, we can implement our two validators by adding the following code to ExampleEmail:

function validateToEmail(&$value, &$edited, &$errors) { $value = trim($value); $edited = true; if (preg_match("/[A-z0-9._-]+@[A-z0-9-]+\.[A-z0-9-\.]*[A-z]+$/", $value) == 1) return true; $errors[] = new WFError("The email you entered is not a properly formatted email address."); return false; } function validateSubject(&$value, &$edited, &$errors) { $value = trim($value); $edited = true; if ($value != '') return true; $errors[] = new WFError("The subject cannot be blank."); return false; }

Validators in PHOCOA are a bit different than some validation mechanisms. Validators are called BEFORE the actual value is set, by the Bindings system, as a pre-flight mechanism. Doing it this way prevents invalid data from ever being in the class.

The validators can also do normalization of the data. Since the value is passed by reference, you can normalize the data in any appropriate way. If you do alter the data, be sure to set edited to true.

Notice also that the errors parameter is an array. This allows you to specify multiple errors during validation.

Now that we can detect errors, we'll want to display the errors to the user as well, so let's edit the helloWorld.tpl file:

{WFShowErrors} {WFForm id="form"} Email: {WFTextField id="email"}<br /> {WFShowErrors id="email"} Subject: {WFTextField id="subject"}<br /> {WFShowErrors id="subject"} Message: {WFTextArea id="message"}<br /> {WFSubmit id="submit"} {/WFForm}

You'll notice the addition of several {WFShowErrors} tags. This is a special function that looks up the errors generated by the specified widget ID and dislpays them. If no id is specified, all errors will be displayed.

Now try the form again, but this time enter invalid data for both the Email and Subject fields and then submit. You'll see that all of the errors are listed above the form, and the specific errors are again repeated next to the relevant form elements.

PHOCOA performs all validation as it is moving the data to your objects via bindings. If the validation fails at all, then the "action" method will not be called. Sometimes of course, there will be no during the bindings phase, but errors may still occur when processing the action. For instance, saving data to a database might return an error. In this case, you can easily add additional errors using the addError() function of WFPage.

There's one more major component to the PHOCOA framework that you should know about, and that's Formatters. Many data types are not human-readable by default, or are not in the desired format. Good examples are timestamps and number formats.

We'll now extend our example to show off formatters.

Let's add a timestamp to our ExampleEmail class:

protected $sendTimestamp;

and in the send() method:

$this->sendTimestamp = time();

Now, let's add the date sent to the success page:

Date Sent: {WFLabel id="timestamp"}<br />

And now of course we need to add it to the emailSuccess.instances file:

'timestamp' => array('class' => 'WFLabel', 'children' => array()),

and set up the binding in emailSuccess.config:

'timestamp' => array( 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'sendTimestamp', ), ), ),

If you try sending another email now, you'll see that the Date Sent is an ugly integer, or UNIX time. To format it, we can use a PHOCOA formatter. Formatters are WFFormatter subclasses, and include WFNumberFormatter, WFSQLDateFormatter, and WFUNIXDateFormatter. We want to use a WFUNIXDateFormatter.

To add a formatter, you could instantiate one manually in the helloworld.php file, and then link it to the timestamp label. However, why bother coding if you don't have to? Let's instantiate a WFUNIXDateFormatter by adding some code in shared.instances:

'dateSentFormatter' => 'WFUNIXDateFormatter',

Now, we want to make the timestamp label use the formatter. There's another feature of the .config files besides bindings; you can use them to set up properties of the instances. Supply a property name and value, and PHOCOA will configure that property with a setValueForKey()() call.

So, in emailSuccess.config, we can set the "formatter" property to the formatter we instantiated in shared.instances by editing the 'timestamp' entry:

'timestamp' => array( 'properties' => array( 'formatter' => '#module#dateSentFormatter', ), 'bindings' => array( 'value' => array( 'instanceID' => 'email', 'controllerKey' => '', 'modelKeyPath' => 'sendTimestamp', ), ), ),

Notice the #module# preceding the shared instance ID; since you can use the config mechanism to set up values such as strings, integers, booleans, and doubles, we needed a special flag to indicate that you want to link it to the value of an instance variable of the module, which is what shared instances are.

Now, submit the form again and you'll see the new, nicely formatted date. You can also set up your own format string, by using shared.config to set up the formatString property of the WFUNIXDateFormatter instance. WFUNIXDateFormatter simply wraps PHP's date()() function, using "r" by default as the format string. Try your own format string by editing shared.config:

'dateSentFormatter' => array( 'properties' => array( 'formatString' => 'F j, Y, g:i a' ), ),

Send another email and you'll see the new format.

Another note, formatters are reversible. So, if you have a date field that you want to be editable, feel free to add a formatter to a WFTextField instance. The formatter will attempt to convert the edited string value using PHP's strtotime()() function. In this case, you might want to add a validator to your date property to make sure it's not NULL, which is what you'll get if the string was not parseable by strtotime().()

It is a best-practice to set up the Title, Meta Keywords, and Meta Description of each page to keep the information relevant and help with search engine optimization. PHOCOA provides a callback mechanism on a page-by-page basis to all you to edit this information.

The prototype of the callback is:

function <pageName>_SkinSetup($skin)

The function should be implemented in your WFModule subclass. Here is an example:

function helloWorld_SetupSkin($skin) { $skin->setTitle("Hello, World, from PHOCOA!"); $skin->addMetaKeywords(array('keyword 1', 'keyword 2')); $skin->setMetaDescription('Hello world example module in PHOCOA.'); }

The complete PHOCOA documentation is available online at http://phocoa.com/docs/phpdocs/.

PHOCOA Key-Value Coding and Bindings Primer

PHOCOA bindings make it easy to show information from your Key-Value Coding Compliant objects, which includes Propel objects if you've implemented the Propel changes noted in the appendix.

You can use WFObject's valueForKeyPath() function to access properties of your objects. For instance, if you have a Book object, and want to access the birthdate of the author, you could use:

$authorBirthDate = $book->valueForKeyPath("author.birthDate");

Which is equivalent to:

$authorBirthDate = $book->getAuthor()->getBirthDate();

While it doesn't seem that advantageous from just looking at the code, it becomes much more powerful when coupled with PHOCOA bindings.

Let's say you want to have a web page that shows the details of a book. Normally, you'd have to assign the $book object to your template engine and then put something like {$book->getAuthor()->getBirthDate()} in your template code.

With PHOCOA, instead of assigning the $book object to your template, you instead make the $book object one of the instance variables of your module. Then, you set up a WFLabel object and bind the "value" of the WFLabel to the $book variable, and set a keyPath of "author.birthDate" as the modelKeyPath.

Now, even this doesn't seem that different. However, as you might be starting to notice, the keyPath for the WFLabel object isn't programming, it's configuration. So, if you want to change the value of the label, you just edit the configuration of the keyPath and the new value will be used.

Furthermore, because you're using PHOCOA GUI widgets, you get access to a host of additional functionality without having to add any code. Want to truncate the string after 30 characters? Just configure the "ellipsisAfterChars" property of the label. Want to hide the label in certain circumstances? Just bind the "hidden" property of the WFLabel to a function returning a boolean value and PHOCOA does the rest. Want the birthdate to show up as a properly formatted date? Just provide a formatter to the WFLabel. Want to use a formatString to combine multiple values? Use the ValuePattern binding! This is all done via the configuration file, not via coding.

PHOCOA's Skin System

PHOCOA's skin system provides a simple yet flexible way to control the template being used to render each page. The skin system has three layers:

By default, a new PHOCOA application has a single Skin Type (simple) and two Skins (simple1, simple2). Simple1 has only one theme, while simple2 has two themes set up.

The bundled "SkinInfo" module provides an interactive browser for the installed skins on any PHOCOA application to make it easy to understand and browse the skin infrastructure.

Visit http://phocoa.com for the latest news, downloads, etc. There are also a mailing lists that you can join at http://lists.phocoa.com.

Hopefully this brief tutorial has showed to you the benefits of PHOCOA development, and taught you enough to experiment on your own. Thanks for reading!

PHOCOA is designed to use Propel as an analog for Core Data. By making a 2-line change to the Propel code, your Propel objects will automatically be Key-Value Coding compliant, making it very simple to make your PHOCOA application interact with Propel.

To update your Propel to work with Phocoa, simply edit the propel/om/BaseObject.php file in 2 places:

  1. Include the PHOCOA WFObject.php base object file:

    require_once FRAMEWORK_DIR . '/framework/WFObject.php';
  2. Make the Propel BaseObject a subclass of the PHOCOA WFObject base class:

    abstract class BaseObjectextends WFObject{

That's it! Your Propel install is now fully compatible with PHOCOA to act as the data store for your objects.

Documentation generated on Thu, 07 Aug 2008 10:36:32 -0400 by phpDocumentor 1.4.1