Thursday, June 17, 2010

Create Excel Data from PHP

Hey All,

It's been a little while since I've written; but everything has been smooth sailing for a while. Tonight, I had a small issue with Excel XML file creation that I thought I'd write about.

The problem stems from word processing programs using left and right oriented apostrophe's instead of single quotes. Notice that the character ' is different from ’ and even different from ‘.

They all have different html codes associated with them. To display the single quotation, we escape the character with a backslash; however, with left apostrophe and right apostrophe, we have to use &lsquo and &rsquo respectively.

Now, php's htmlspecialchars() function does not look after these special html translations, so we have to add in a function like
function myhtmlspecialchars($string) {
    $transTable = get_html_translation_table(HTML_ENTITIES);

    $transTable[chr(145)] = '\'';  // looks after &lsquo
    $transTable[chr(146)] = '\'';  // looks after &rsquo
    $transTable[chr(147)] = '&quot'; // looks after &ldquo
    $transTable[chr(148)] = '&quot'; // looks after &rdquo

    return strtr($string, $transTable);
}

The result of not fixing this on a website is that you get diamonds with question marks in them. The result of not fixing this on a xml file is that it breaks.

The function that I'm using the generate the excel data is as follows

function excelXML($bigArray) {
    $colCount = sizeof($bigArray[0]);
    $colCount2 = $colCount -1;
    $rowCount = sizeof($bigArray)+1;
    $beginOutput = "
         
         
         
             dougler
             dougler
             2010-05-04T04:19:22Z
             12.00
         
         
             14880
             28755
             0
             135
             False
             False
         
         
         
        
        
        \n";  foreach ($bigArray[0] as $key => $value) {       $beginOutput .= "$key\n"; } $beginOutput .= "\n";   $middleOutput = "";  for ($i = 0; $i < sizeof($bigArray); $i++) {
    $middleOutput .= "\n";     foreach ($bigArray[$i] as $key => $value) {         $middleOutput .= "" . myhtmlspecialchars($value) . "\n";     }     $middleOutput .= "\n"; }  $endOutput = "     
300 300 3 55 False False
"; return $beginOutput . $middleOutput . $endOutput; }

Where $bigArray looks something like

Array
(
    [0] => Array
        (
            [id] => 1
            [company] => test
            [address] => test
            [city] => test
            [province] => test
            [postal] => test
            [website] => test
            [contact] => test
            [telephone] => test
            [email] => test@test.ca
            [password] => test
            [Number of Projects] => test
        )

    [1] => Array
        (
            [id] => 2
            [company] => test
            [address] => test
            [city] => test
            [province] => test
            [postal] => test
            [website] => test
            [contact] => test
            [telephone] => test
            [email] => test@test.ca
            [password] => test
            [Number of Projects] => 
        )

    [2] => Array
        (
            [id] => 3
            [company] => test
            [address] => test
            [city] => test
            [province] => test
            [postal] => test
            [website] => test
            [contact] => test
            [telephone] => test
            [email] => test@test.ca
            [password] => test
            [Number of Projects] => test
        )
)

Monday, May 31, 2010

Openvpn 2.1.1 Not Connecting

One of the services that I offer for a company is a VPN. I use the OpenVPN distribution that comes in the FreeBSD ports collection.

I have mine configured as a tunnel device which authenticates over RSA keys. Once the user is connected, I allow them access to port 139 which gives SMB shares mapped from a linksys NAS RAID device. It's a pretty slick service and very stable.

Until today!

Today, none of my clients could connect, and I was curious about why this had happened. After running a port upgrade, I had not manually restarted all of my services, and it wasn't until this weekend, when the server was rebooted that openvpn 2.1.1 was initialized (previously 2.0.9 was installed).

There was no immediate reason as to why openvpn wouldn't work except for a line which said that --script-security 2 was supposed to be in the openvpn command line if it were to execute client scripts. I added the line in rc.conf as openvpn_flags="--script-security 2", restarted the service, and now it works perfectly.

Sunday, May 23, 2010

Zend Gdata Calendar Tutorial DataMapper

I first started using the Zend library when I found out that the Google Apps API for PHP was distributed in the Zend_Gdata library. It's been a while since then, and I've become more and more fond of the Zend Framework since. But this post isn't about my swoon for Zend, it's actually meant to explain how to use Zend_Gdata_Calendar to list events and to change, read and update extended properties to the events.

First, I'll explain my basic setup.
The whole app is based on a schedule for a radio station with some logging features. I have an object Application_Model_Show which gets its data from a google calendar. The actual object which is responsible for populating the show object with the right information is Application_Model_Mapper_ShowMapper (located in the appropriate directory vis-a-vis the Zend system). I'll explain how to set up this in Zend in a bit, for now... let's stay a little simpler.

The Zend_Gdata_Calendar object is the first one that we'll be concerned with. I started off playing with this library in a non-Zend environment to start. This just gave me ability to see the nuts and bolts of this library. Let's start off by just creating a connection to the service.

Because we don't have the luxury of the Zend Autoload feature, we're going to have to load our classes   manually.
<?php
require_once('Zend/Loader.php');
$classes = array('Zend_Gdata','Zend_Gdata_Query','Zend_Gdata_ClientLogin','Zend_Gdata_Calendar');
foreach($classes as $class) {
    Zend_Loader::loadClass($class);
}
Great, now we can set our credentials and create the authorized connection to the google. There are three connection methods which google implements. AuthSub is used so that your clients are sent to a google page first, authenticated, and then returned back to your page with an authentication slip. MagicCookie is a read only method, and the method we'll be using is ClientLogin. This one is best suited for this situation, because there is only one calendar that I'm going to need to access. Following is the setup for clientLogin, if you're following along, concatenate it to the above code.
$user = 'someemail@gmail.com';
$pass = 'supersecret';
$service = Zend_Gdata_Calendar::AUTH_SERVICE_NAME;
$client = Zend_Gdata_ClientLogin::getHttpClient($user,$pass,$service);
$service = $Zend_Gdata_Calendar($client);
Cool, now we have a connection that we can process our requests through. If we were going to be making a lot of calls to the google server, it would be wise to maintain this one connection rather than recreating it every time. We'll keep this in mind for when we discuss the Application_Model_Mapper_ShowMapper object in the MVC.

So, what good is this gateway to google without some need for information. The first thing your'e going to want to do is get a list of the Calendars accessible to this user. Let's throw this request into a try block just in case anything goes wrong
try{
    $listFeed = $service->getCalendarListFeed();
}
catch (Zend_Gdata_App_Exception $e){
    echo "Error: " . $e->getMessage();
}
// echo it back so you can see the id
echo "<ul>
foreach($listFeed as $listEntry) {     echo "
<li> . $listEntry->title . "(Event Feed: " . $listEntry->id . ")"</li>
} echo "</ul>
";
This will give us all of the calendars that you normally see in the google calendar user interface. Go ahead, create a new one and see how it shows up. Isn't that cool? Note that your default calendar has an id that looks like
someemail@gmail.com (Event Feed: http://www.google.com/calendar/feeds/default/someemail%40gmail.com)

But any of the non-default calendars look more like
OtherCalendarName (Event Feed: http://www.google.com/calendar/feeds/default/[SOME26CHARACHTERSTRING]%40group.calendar.google.com)
This is important to note if you want to access the non-default calendar.

Now, Let's do something useful.... like finding the events that are scheduled that day on either calendar.

To get this information, at the Zend level, we create a query object and send this query object to a get method. A eventFeed object is returned, and we iterate through the eventFeed to see the eventEntries.

Sounds simple right? Well, if you're comfortable with OO, I'm sure it'll be simple... but if you're not, then it's comforting to know, that the only thing that's happening here is that we're making an authenticated http request for an XML response, in turn, the Zend Library turns the XML response into an object which plays nicely with us.

we start by building a query.
$timezone = "00:00:00-08:00"; //My application is in PST (Vancouver, BC)
$today = date('Y-m-d') . $timezone;
$tonight = date('Y-m-d',strtotime($today + 24*60*60)) . $timezone;

$queryDefault = service->getEventQuery();
              ->setUser('default'); // for this query we'll look at the default calendar
              ->setVisibility('public');  // since we aren't planning on editing the calendar at this point, we can just set this to public; otherwise, it'd have to be private.
              ->setOrderBy('starttime');
              ->setStartMin($today);
              ->setStartMax($today);

$queryOtherCalendar = $queryDefault->setUser('[THAT26CHARACHTERSTRING]%40group.calendar.google.com');

As I said above, we're only building a URL here that we're going to request an XML response from. So, the $queryDefault url is actually http://www.google.com/calendar/feeds/default/full/public. The response back from google is an XML file which is turned into a nice object

Let's process these requests.
try{
   $eventFeedDefault = $service->getCalendarEventFeed($queryDefault);
   $eventFeedOtherCalendar = $service->getCalendarEventFeed($queryOtherCalendar);
}
catch (Zend_Gdata_App_Exception $e){
    echo "Error :" . $e->getMessage();
}
and display the results
echo "<h1>On the Default Calendar, the events of the day are</h1>";
echo "<ul>"; 
foreach ($eventFeedDefault as $event) {     
    echo "<li>" . $event->title . "(Event Id: " . $event->id . ")</li>
"; } 
echo "</ul>";

echo "<h1>On the other calendar, the events of the day are</h1>";
echo "<ul>"; 
foreach ($eventFeedOtherCalendar as $event) {     
    echo "<li>" . $event->title . "(Event Id: " . $event->id . ")</li>
"; } 
echo "</ul>";
Well, there we have it! We've now listed all of the events of the day on both calendars. This is a pretty big step for now. Let's start including this into the Application_Model_Mapper_ShowMapper class (located in application/models/Mapper/ShowMapper.php). Here's some more information.

The ShowMapper has the following skeleton
<?php 
class Application_Model_Mapper_ShowsMapper extends Application_Model_Mapper_AbstractMapper
{
    private static $_instance = null;
    private $_service = '';

    public function __construct() {
        // create the connection in here and return it to $this->_service
    }

    private function getExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name){
        // more on this later
    }

    private function addExtendedProperty(Zend_Gdata_Calendar_EventEntry $name, $value){
        // more on this later
    }

    private function getEventById($id){
        // returns a Zend_Gdata_Calendar_EventEntry
    }

    public function getShows(){
        // returns an array of Application_Model_Show
    }

    public function getShowById(){
        // returns a single Application_Model_Show
    }

    public function save(Application_Model_Show $show){
        // calls addExtendedProperty, returns boolean
    }

    public static function getInstance() {
        //return the singleton in $this->_instance
    }
}
So, for public methods, we have getShows(), getShowById(), save(), and getInstance(). Just a note, show is an instance of Application_Model_Show, shows refers to an array of show, and Event is an instance of Zend_Gdata_Calendar_EventEntry.

we call the creation of the object through
$showMapper = Application_Model_ShowMapper::getInstance();
so that only one showMapper object is being created no matter how many requests are made in the session. To make sure that only one connection is created no matter how many times it's called, we populate $_service in the construct with our code from above (with some exception handling built in).
public function __construct() {
    $user = 'someemail@gmail.com';
    $pass = 'superSecret';
    $service = Zend_Gdata_Calendar::AUTH_SERVICE_NAME;
    try {
        $client = Zend_Gdata_clientLogin::getHttpClient($user,$pass, $service);
    } catch (Zend_Gdata_App_CaptchaRequiredException $e) {
        echo 'URL of Captcha image: ' . $e->getCaptchaUrl() . "\n";
        echo 'Token ID: ' . $e->getCaptchaToken() . "\n";
    } catch (Zend_Gdata_App_AuthException $e) {
        echo 'Problem Authenticating: ' . $e->exception() . "\n";
    }
    $this->_service = new Zend_Gdata_Calendar($client);
}
The constructor creates the service variable which each request on all instances of ShowMapper will marshal through. We look for a couple exceptions here to make sure that we're connecting properly. If you catch CaptchaRequiredException, you should goto the google calendar UI and follow their captcha instructions on their site to confirm you're note a hackbot.

Google Calendar extended properties are only available through the API, so far, there is no way to view them through the google UI. They're quite useful for storing different attributes for your event. For instance, in this application, shows are either spoken word shows or not. This comes into play for calculating the amount of spoken word we have on the station (a CRTC requirement). Additionally, Show has a boolean of 'syndicated' as well. Here's an example of how to set and get extended properties.
private function getExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name)
{
    $extProps = $event->extendedProperty;
    foreach($extProps as $extProp){
        if ($name == $extProp->name){
            return $extProp->value;
        }
    }
}

private function addExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name, $value)
{
    $extProp = $this->_service->newExtendedProperty($name,$value);
    $extProps = array_merge($event->extendedProperty, array($extProp));
    $event->extendedProperty = $extProps;
    $eventNew = $event->save();
    return $eventNew;
}
These are fairly self-explanatory; I'll move on.
private function getEventById($id)
{
    // this method is giving me some issues right now, I'll report back on this one when I have it working
    // currently, an event is loading, but without $event->when being populated. :(
    $query = $this->_service->newEventQuery()
           ->setUser('default')
           ->setVisibility('private')
           ->setProjection('full')
           ->setEvent($id);
    try {
        $event = $this->_service->getCalendarEventEntry($query);
    }
    catch (Zend_Gdata_App_Exception $e) {
        "Error $id: " . $e->getMessage();
    }

    return $event;
}
Currently, I'm tempted to just request the whole 2 week span once a day and to store that as a site-cached variable while allowing the admin user to refresh the cache when they visit the site. This could get around the $event->when not populating properly. I can't find any reason why this wouldn't come back. The next step is to dive into the XML and figure out what the hell is going on.

Anyways </rant>.

the getShows() method is very similar to some code way up there - just that we populate the Application_Model_Show object with our returned data.

public function getShows()
{
    $timezone = "T00:00:00-08:00";  // this works for PST
    $startTime = date("Y-m-d", time() - 60*60*24*14) . $timezone;
    $stopTime = date("Y-m-d", time() + 60*60*24*14) . $timezone;
    // this looks for all the events within 2 weeks either size of today
    $query = $this->_service->newEventQuery()
           -> setUser('default')
           -> setVisibility('private')
           -> setStartMin($startTime)
           -> setStartMax($stopTime)
           -> setMaxResults(336) // the default is 25
    try {
        $eventFeed = $this->_service->getCalendarEventFeed($query);
    }
    catch (Zend_Gdata_App_Exception $e) {
        echo "Error: " . $e->getMessage();
    }
    $shows = array();
    foreach ($eventFeed as $event)
    {
        $show = new Application_Model_Show(array(
            'id'          => substr($event->id,strrpos($showData->id,'/')+1,26), //(the id is 26 charachters long, sometimes followed by a time variable)
            'name'        => $event->title,
            'description' => $event->content,
            'when'        => $event->when(),
            'spokenWord' => $this->getExtendedProperty($event, 'spokenWord'),
            'syndicated' => $this->getExtendedProperty($event, 'syndicated')
        ));
        $shows[] = $show;
    }
    return $shows;
}

You can populate the data into your show variable however you want, i use the construct to shove the array entries in.

Two more and we're done.

public function getShowById($id){
    $event = $this->getEventById($id);
    $show = new Application_Model_Show(array(
            'id'          => substr($event->id,strrpos($showData->id,'/')+1,26), //(the id is 26 charachters long, sometimes followed by a time variable)
            'name'        => $event->title,
            'description' => $event->content,
            'when'        => $event->when(),
            'spokenWord' => $this->getExtendedProperty($event, 'spokenWord'),
            'syndicated' => $this->getExtendedProperty($event, 'syndicated')
        ));
    return $show;
}
and finally a method we can call to save our extended properties in one call
public function save(Application_Model_Show $show)
{
    $event = $this->getEventById($show->getId());
    $this->addExtendedProperty($event, 'spokenWord', $show->getSpokenWord());
    $this->addExtendedProperty($event, 'syndicated', $show->getSyndicated());
}


Well, there we have it!

Gripes with Vpro and Intel i5-661 with Asus P7Q57M-DO

I was so fucking excited when I heard about the new Intel Vpro technology and how RealVNC is integrating it so that you get bios level KVM access because I'm currently administering 2 FreeBSD boxes at the radio station in BC and a production box at home in Calgary.

Finally, I'd be able to stop depending on my sysadmin friends to step in when something horrible happened!

Well, you might be wondering where the gripe is then?  Well, it's the lack of documentation from intel as to what kind of machinery does this 'vpro' stuff, and what it means to be full 'vpro'.

I have the Asus P5E-VM DO which claims to have "Intel® vPro Technology support"; but, the board doesn't have any KVM options in it, and the RealVNC client returns an inappropriate permissions error.  It allows me to have access via SOL and I can do IDE redirection (which is pretty damn cool), but it's not going to help me diagnosing why my RAID didn't come back online. 

Eventually, I decide that my board with the Q35 chipset just wasn't new enough, so I wrote off my chances of buying an IPad and bought a new server for myself with the  Asus P7Q57-M DO board and an Intel i5 661 processor.

The system is awesomely fast; but, the KVM option is STILL not available.  WHY!?!?

Eventually enough searching found this article; note on page 37, it says the ingredients for vPro
What ingredients are required for Intel vPro technology branded platforms? The latest 2010 Intel vPro processor platforms with the Intel vPro brand include the following key ingredients:
• The all new Intel Core vPro processors
• Intel Q57, QS57, or QM57 chipset
• TPM 1.2
• Intel VT capable BIOS
• Intel TXT capable BIOS
• TPM 1.2 capable BIOS
• Intel AMT capable BIOS
• Intel ME firmware 6.0 with Intel AMT 6.0 and Intel AT 2.0
• Intel 82578DM GbE LAN, Intel 82577LM GbE LAN, Intel Centrino® Advanced-N+WiMAX 6250, or Intel Centrino Ultimate-N/Advanced-N 6000 Series
• Intel Management and Security Status Icon (recommended)
Note: Earlier generations of Intel vPro branded platforms have different platform ingredients. Consult your Intel sales representative for the platform ingredients in earlier generations of Intel vPro technology.
Now, the first line says Intel Core vPro Processors, and if we take a look at all of the processors available at most stores, nothing is specified about vPro; but it turns out that only the i5-650, i5-660, and i5-670 processors have the vPro technology enabled.   So, I had to special order one in for Monday - I'll keep you posted as to whether it works or not.

An Easy SyntaxHighlighter with Blogger

So, in the last blog post, I wanted to throw down some nice code box like I've seen on other blogger pages. I found a couple tutorials out there which had you pasting information into the html layout found in layout->edit HTML in three different places.

It was a bit of a pain that I had to put this piece here and this other piece there and so on, but, I found one that doesn't require all that.

Without further adieu,
  1. Download and find a place to host the library files for Syntax HighLighter.
  2. Login to blogger; go layout -> edit HTML and add the following at the end of the file.
</div></div> <!-- end outer-wrapper -->
<link href='http://[YOUR HOST]/SyntaxHighlighter.css' rel='stylesheet' type='text/css'/>
<script src='http://[YOUR HOST]/shCore.js' type='text/javascript'/>

<script src='http://[YOUR HOST]/shBrushCpp.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushCSharp.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushCss.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushJava.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushJScript.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushSql.js' type='text/javascript'/>
<script src='http://[YOUR HOST]/shBrushXml.js' type='text/javascript'/>

<script class='javascript'>
//<![CDATA[
  function FindTagsByName(container, name, Tag)
  {
      var elements = document.getElementsByTagName(Tag);
      for (var i = 0; i < elements.length; i++)
      {
          if (elements[i].getAttribute("name") == name)
          {
              container.push(elements[i]);
          }
      }
  }
  var elements = [];
  FindTagsByName(elements, "code", "pre");
  FindTagsByName(elements, "code", "textarea");

for(var i=0; i < elements.length; i++) {
if(elements[i].nodeName.toUpperCase() == "TEXTAREA") {
 var childNode = elements[i].childNodes[0];
 var newNode = document.createTextNode(childNode.nodeValue.replace(/<br\s*\/?>/gi,'\n'));
 elements[i].replaceChild(newNode, childNode);

}
else if(elements[i].nodeName.toUpperCase() == "PRE") {
 brs = elements[i].getElementsByTagName("br");
 for(var j = 0, brLength = brs.length; j < brLength; j++) {
  var newNode = document.createTextNode("\n");
  elements[i].replaceChild(newNode, brs[0]);
 }
}
}
//clipboard does not work well, no line breaks
// dp.SyntaxHighlighter.ClipboardSwf =
//"http://[YOUR HOST]/clipboard.swf";
dp.SyntaxHighlighter.HighlightAll("code");
//]]>
</script>

</body>
</html>
There are a few other language highlighting rules found in the SyntaxHighlighter library; you can add them in above as you need them, and you can take the ones out that you don't use also. Here's a list of what's in the current Syntax Highlighter 1.5.1
  • Cpp
  • C#
  • Css
  • Delphi
  • Java
  • Jscript
  • Php
  • Python
  • Ruby
  • Sql
  • Vb
  • Xml
I found this solution on Tips for software engineer. Thank you very much.

Changing from Procedural PHP to Zend MVC - Hello World

Having worked with procedural PHP (non OO), I decided this spring to learn the Zend Framework MVC system.  Through making various PHP sites, I always found it unclear how to tie the structure together so that it's clear what is going on.  Certainly for small sites, this is not a big issue, but for a larger site, a redirector was important.

I would typically code the following into index.php

<php
if(isset($_POST['form']){
 // deal with $_POST['form']
if(isset($_GET['page']){
  include $_GET['page'];
}
else {
 // send out the default stuff
}
?>

Of course, this is insecure as hell because you're allowing someone to grab any file from your system, it's wise to define an array of allowable pages here... I'm not going to get into that here - you get the point. This allows for a basic redirector model for selecting different pages; however, it doesn't provide a smart interface for a controller / model difference.

The Zend Framework MVC system is much more sophisticated.  I work primarily in a FreeBSD environment; so we start by installing with a command like:

root@aztec~# cd /usr/ports/www/zend-framework && make install

We can now setup a skeleton directory structure for our Zend project

gate@aztec~# zf create project zendPlay

This creates a directory structure

gate@aztec~# tree zendPlay/
zendPlay/
|-- application
|   |-- Bootstrap.php
|   |-- configs
|   |   `-- application.ini
|   |-- controllers
|   |   |-- ErrorController.php
|   |   `-- IndexController.php
|   |-- models
|   `-- views
|       |-- helpers
|       `-- scripts
|           |-- error
|           |   `-- error.phtml
|           `-- index
|               `-- index.phtml
|-- library
|-- public
|   `-- index.php
`-- tests
    |-- application
    |   `-- bootstrap.php
    |-- library
    |   `-- bootstrap.php
    `-- phpunit.xml

14 directories, 10 files


The part that's a little unclear with Zend right away is that the controller automagically calls the view script.  Notice above how we have controllers IndexController and ErrorController, and we have error and index directories in the application/views/scripts/ directory.  This is no coincidence.  The controller calls the view script and together they form the page.  It's important that the names are associated the way they are so that the router works properly.

In Zend, a url re-writer is used to determine which page to load.  In apache, we include something like

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]

into zendPlay/public/.htaccess  so that a request like http://zendhost/ is sent to the controller IndexController (a class found in application/controllers/IndexController.php) with method indexAction (in the IndexController class).  Similarly, the request http://zendhost/foo/bar would be sent to the controller FooController with method barAction which in turn uses the view application/views/scripts/foo/bar.phtml.

Now create a vhost with apache and a free domain from dyndns (or wherever) with the following in /usr/local/etc/apache22/extras/httpd-vhosts.conf

<virtualhost zend.thruhere.net:80="">
  DocumentRoot "/home/gate/zendPlay"
  ServerName zendhost
  <directory gate="" home="" zendplay="">
    DirectoryIndex index.php
    AllowOverride All
    Order allow,deny
    Allow from all
  </directory>
</virtualhost>

and restart apache.

root@aztec~# /usr/local/etc/rc.d/apache22 restart

So the following code would give us a hello world script right off the bat.

edit application/controllers/IndexController.php
<?php

class IndexController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
    }

    public function hiAction()
    {
      $this->view->myVar = "Hellow World";
    }
}

so that it has the additional action hiAction (notice how there is no ?> bracket). Try loading http://zendhost/ now. We get an error message!

Message: script 'index/hi.phtml' not found in path (/usr/home/gate/zendPlay/application/views/scripts/)

Fortunately the solution is quite clear: create the file application/views/scripts/index/hi.phtml and add the following

<?php

echo $this->myvar;

Wonderful! it's quite an advanced 'hello world' script. But it at least explains how Zend's router works. There's a lot more to cover here; but I'll finish this howto off with some justification for this much more code.

The purpose of separating the .phtml files from the action is that our controller can act as a traffic marshall between the model and the view.  The controller has no `echo` or `print` functions, and the view only has output functions (with some loops).   The value in this is that our .phtml files are very simple and the application logic is kept in the rest of the code.