Wednesday, November 24, 2010

Try Zend Framework vol.13 "Dispatch Like Mojavi"

Do you remember "Mojavi" framework? :)
Once upon a time, there was a fantastic framework which is called "Mojavi".
If you were a PHP programmer, once you have probably heard that name.
But, it's not seems to be developed no longer.

So, tonight we look back old days and revive "Mojavi" as Zend dispacher! :)

Before talking about Mojavi, Let's talk about Ruby on Rails and frameworks around it.
After Ruby on Rails (RoR) was shown in the world, it became very famous and common as rapid web development tool.
It gives you cool architecture and the speed until making a product up.
Then many frameworks imitate to RoR, and they became similar to RoR.
And I think Zend Framework is also the one of them. :)

RoR style dispach is very simple and easy to understand, then keeps class and file less. When I saw that architechture first time, I was so impressed of its simpleness.

RoR style is very nice, but it is too hard to throw away Mojavi style.
I think Mojavi style is also simple and easy to identify the area of influence.

To know the diffrences between two styles, let's see both architechtures.
Comparing two of them, let's see Zend Frameworks' default structure.

There are "Model", "Controller" and "Action".

In Zend Framework, the standard dispatcher interprets "Module", "Controller" and "Action" from the executed url.
Actually it depends on the router, but for now , let's think about "Standard" dispatcher. ;)

OK, see the url below.
http://zf.f-pig.net/foo/bar/buz

There are three paths foo, bar and buz, it would be interpreted by dispatcher like this.
1.foo is "Module" and it is "Direcotry".
2.bar is "Controller" and it is "Class file".
3.buz is "Action" and it is "Method" of bar (controller).

And it looks like this.
application/
|~modules/
| |~foo/(module)
| | |~controllers/
| | | `~BarController.php (has "buz" method as action method.)

In BarController.php
class Foo_BarController extends Zend_Controller_Action
{
public function buzAction()
{
// this is buz action.
}
}

OK, that is Zend Framework style.

Then now, let's look back old days!!

From here I'm going to talk about Mojavi Framework style.
Mojavi interprets that url like this.
http://zf.f-pig.net/foo/bar/buz

1. foo is "Moduel" and it is "Directory".
2. bar is "Controller" it is also "Direcotry".
3. buz is "Action" and it is "Class file". It has a method to execute it self.

And it looks like this.
application/
|~modules/
| |~foo/(module)
| | |~controllers/
| | | `~Bar/(controller)
| | |   `-BuzAction.php(action having "execute" method.)

In BuzAction.php
class Foo_Bar_BuzAction extends Zend_Controller_Action
{
// this is action.

public function execute()
{
// this is an executive method of this action "class".
}
}


You may feel Mojavi style is too detailed style, and makes many class files and feel it messy.

But, this Mojavi style sometimes helps you.
There are some situations that Mojavi style can solve.

Case 1.
Let's think, you want to write some method which is for buzAction.
You probably write that method on BarController.
On the one hand, it's pretty normal, because there is no place to write that method,
on the other hand, you need to name that method to specify the aim that the method is for buzAction.

Case 2.
Zend_Controller_Action gives some useful methods like init, preDispatch and postDispatch.
But, these methods are on Controller, so every action method was called it would be called automatically.
Even if you don't want to call preDispatch when another action methods called, it would be called automatically.
Or you may need to write a "if statement" in these methods.

Case 3.
Imagine you are working with many coworkers whose skills are not same level as you.
They may touch a same class at the same project.
And you need to share the class file with them.
A coworker may use generic name even it only work for buzAction.
Once that code is released, it's pretty hard to refactor and release, isn't it?

Oops, I'm talking too much. :)
It's time to write Mojavi dispacher!

To make Mojavi style architecture, we need to make a new dispatcher!
Let's call it "Hoge_Controller_Dispatcher_MojaviLike".
Hoge_Controller_Dispatcher_MojaviLike
<?php
/**
* Hoge_Controller_Dispatcher_MojaviLike
* Dispatch like mojavi.
*/
class Hoge_Controller_Dispatcher_MojaviLike extends Zend_Controller_Dispatcher_Standard
{
/**
* Execute method name.
* @var string
*/
protected $_executeMethod = 'execute';

/**
* Set executable method name.
* @return void
*/
public function setExecuteMethod($name)
{
$this->_executeMethod = $name;
}

/**
* Get executable method name.
* @return executetable method name.
*/
public function getExecuteMethod()
{
return $this->_executeMethod;
}

/**
* Formats a string into a controller name.
* @param string $unformatted
* @return string
*/
public function formatControllerName($unformatted)
{
return ucfirst($this->_formatName($unformatted));
}

/**
* Formats a string into an action name.
* @param string $unformatted
* @return string
*/
public function formatActionName($unformatted)
{
return ucfirst(parent::formatActionName($unformatted));
}

/**
* Get action class name
*
* @param Zend_Controller_Request_Abstract $request
* @return string|false Returns class name on success
*/
public function getActionClass(Zend_Controller_Request_Abstract $request)
{
// controller info.
$controllerName = $request->getControllerName();
if (empty($controllerName)) {
if (!$this->getParam('useDefaultControllerAlways')) {
return false;
}
$controllerName = $this->getDefaultControllerName();
$request->setControllerName($controllerName);
}
$controllerName = $this->formatControllerName($controllerName);


// action info.
$actionName = $request->getActionName();
if (empty($actionName)) {
if (!$this->getParam('useDefaultActionAlways')) {
return false;
}
$actionName = $this->getDefaultAction();
$request->setActionName($actionName);
}
$actionName = $this->formatActionName($actionName);

// build class name.
$className = $controllerName . $this->getPathDelimiter() . $actionName;

$controllerDirs = $this->getControllerDirectory();
$module = $request->getModuleName();
if ($this->isValidModule($module)) {
$this->_curModule    = $module;
$this->_curDirectory = $controllerDirs[$module];
} elseif ($this->isValidModule($this->_defaultModule)) {
$request->setModuleName($this->_defaultModule);
$this->_curModule    = $this->_defaultModule;
$this->_curDirectory = $controllerDirs[$this->_defaultModule];
} else {
require_once 'Zend/Controller/Exception.php';
throw new Zend_Controller_Exception('No default module defined for this application');
}

return $className;
}

/**
* Returns TRUE if the Zend_Controller_Request_Abstract object can be
* @param Zend_Controller_Request_Abstract $action
* @return boolean
*/
public function isDispatchable(Zend_Controller_Request_Abstract $request)
{
$className = $this->getActionClass($request);
if (!$className) {
return false;
}
$finalClass  = $className;
if (($this->_defaultModule != $this->_curModule)
|| $this->getParam('prefixDefaultModule'))
{
$finalClass = $this->formatClassName($this->_curModule, $className);
}
if (class_exists($finalClass, false)) {
return true;
}

$fileSpec    = $this->classToFilename($className);
$dispatchDir = $this->getDispatchDirectory();
$test        = $dispatchDir . DIRECTORY_SEPARATOR . $fileSpec;
return Zend_Loader::isReadable($test);
}

/**
* Dispatch to a controller/action
* @param Zend_Controller_Request_Abstract $request
* @param Zend_Controller_Response_Abstract $response
* @return void
* @throws Zend_Controller_Dispatcher_Exception
*/
public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
{
$this->setResponse($response);

/**
* Get action class
*/
if (!$this->isDispatchable($request)) {
$action = $request->getControllerName();
if (!$this->getParam('useDefaultActionAlways') && !empty($action)) {
require_once 'Zend/Controller/Dispatcher/Exception.php';
throw new Zend_Controller_Dispatcher_Exception(
'Invalid controller/action specified (' .
$request->getControllerName() . '/' . $request->getActionName() . ')');
}
$className = $this->getDefaultActionClass($request);
} else {
$className = $this->getActionClass($request);
if (!$className) {
$className = $this->getDefaultActionClass($request);
}
}

/**
* Load the action class file
*/
$className = $this->loadClass($className);

$action = new $className($request, $this->getResponse(), $this->getParams());
if (!($action instanceof Zend_Controller_Action_Interface) &&
!($action instanceof Zend_Controller_Action)) {
require_once 'Zend/Controller/Dispatcher/Exception.php';
throw new Zend_Controller_Dispatcher_Exception(
'Action "' . $className . '" is not an instance of
Zend_Controller_Action_Interface'
);
}

/**
* Retrieve the execution name
*/
$execute = $this->getExecuteMethod();

$request->setDispatched(true);

// by default, buffer output
$disableOb = $this->getParam('disableOutputBuffering');
$obLevel   = ob_get_level();
if (empty($disableOb)) {
ob_start();
}

try {
$action->dispatch($execute);
} catch (Exception $e) {
// Clean output buffer on error
$curObLevel = ob_get_level();
if ($curObLevel > $obLevel) {
do {
ob_get_clean();
$curObLevel = ob_get_level();
} while ($curObLevel > $obLevel);
}
throw $e;
}

if (empty($disableOb)) {
$content = ob_get_clean();
$response->appendBody($content);
}

// Destroy the page controller instance and reflection objects
$action = null;
}

/**
* Retrieve default action class
*
* @param Zend_Controller_Request_Abstract $request
* @return string
*/
public function getDefaultActionClass(Zend_Controller_Request_Abstract $request)
{
$controller = $this->getDefaultControllerName();
$defaultController = $this->formatControllerName($controller);

$action = $this->getDefaultAction();
$defaultAction = $this->formatControllerName($action);

$default = $defaultController . $this->getPathDelimiter() . $defaultAction;

$request->setControllerName($controller)
->setActionName($action);

$module              = $request->getModuleName();
$controllerDirs      = $this->getControllerDirectory();
$this->_curModule    = $this->_defaultModule;
$this->_curDirectory = $controllerDirs[$this->_defaultModule];

if ($this->isValidModule($module)) {
$found = false;
if (class_exists($default, false)) {
$found = true;
} else {
$moduleDir = $controllerDirs[$module];
$fileSpec  = $moduleDir . DIRECTORY_SEPARATOR . $this->classToFilename($default);
if (Zend_Loader::isReadable($fileSpec)) {
$found = true;
$this->_curDirectory = $moduleDir;
}
}
if ($found) {
$request->setModuleName($module);
$this->_curModule = $this->formatModuleName($module);
}
} else {
$request->setModuleName($this->_defaultModule);
}

return $default;
}


To switch dispatcher.
$frontController = Zend_Controller_Front::getInstance();
$frontController->setDispatcher(new Hoge_Controller_Dispatcher_MojaviLike())


With this Mojavi dispacher, Controller class will be Action class.
It looks like this.

application/modules/foo/controllers/Bar/BuzAction.php
<?php
class Foo_Bar_BuzAction extends Zend_Controller_Action
{
/** 
* init
*/
public function init()
{   
$this->view->class = '"' . __CLASS__ . '" class';
$this->view->init = '"' . __FUNCTION__ . '" is called';
}   

/** 
* preDispatch
*/
public function preDispatch()
{   
$this->view->preDispatch = '"' . __FUNCTION__ . '" is called';
}   

/** 
* postDispatch
*/
public function postDispatch()
{   
$this->view->postDispatch = '"' . __FUNCTION__ . '" is called';
}   

/** 
* execute this class. 
*/
public function execute()
{   
$this->view->execute = '"' . __FUNCTION__ . '" is called';
}   
}


How about templates?
There is no need to change anything!

To show registered view values above template looks like this.

application/modules/foo/views/scripts/bar/buz.phtml
<b>Mojavi Like</b>
<? foreach ($this as $key =>  $prop): ?>
<?= $prop ?><br />
<? endforeach ?>
But, I also think Zend style is pretty nice and enough. :)
The Controller and Action method should be as small as they can.
The logic should be written in Model class?! Actually I'm not sure if it should be. :p

But, please don't forget, in the real world,
it's pretty hard that all coworkers write source code beautifully and same style. : <

In these case, "Mojavi" style might help you.

At last, thank Mojavi to appeared in this world. And good bye!! :)

(weight 89.6kg BMI 32%)

Labels:

2 Comments:

At 5:45 AM, Blogger komagata said...

OMG! Fantasy Pig was updated.

 
At 7:37 AM, Blogger kawadu said...

OMG!
I'm still alive desiring to migrate this blog to lokka!
http://lokka.org/?locale=en

 

Post a Comment

<< Home