PHP on Rails

December 8, 2008

php-med-trans-light

Adding conventions to DRY our code

This article will provide a few snippets of code that I have recently plugged into the custom PHP framework that I use. However, those of you whom use and are more familiar with popular frameworks may be able to use this as well. I decided a few days ago to alter this custom framework in the following ways:

  • an “application controller” which has access to the base and current page controller
  • filter capabilities in page controllers

First, I will outline some basics about my framework so that you can understand my implementations.

The custom framework that I use is very similar to rails’ conventions. It is an MVC framework and has almost a 1:1 folder/file structure to rails. One glaring difference is that it uses camelCase throughout the framework in opposition to the rails convetion. Anyway, let me start with the app folder (since this is the center of our attention).

You can see above that my app folder is almost exactly like rails except for the camelCase and the fact that my views have .tpls (go smarty!). You’ll also notice that I have 3 page controllers plus the application controller. We are going to start with how I have implemented the application controller.

The Application Controller

You should already know that my page controllers will extend the “base” controller (which is simply Controller for me). Thus, my page controllers have access to all of the essentials such as redirections, session access, IoC, flash messaging, etc. So, we want to make another controller which will allow those 3 page controllers (and any future page controller) to access shared functions so that we can maintain DRY code. Keep in mind that this application controller should also have access to the parent controller.

The problem: How do we easily give our application controller shared communication between the parent controllers and child controller, but also disallowing it from being a page controller itself?

First let’s take a look at my page controller:

 

requireUser();
  }
};
?>

You may have guessed that this "action" will respond to http://somewhere/home/index/ and render app/view/home/index.tpl (and you are correct!). Inside the function we are trying to access the application controller's "requireUser" method. Here's what the application controller looks like:

 

session()->hasRole(array('User')))
     {
        $this->flash()->error = 'You must be logged in to do that.';
	$this->redirect(array('controller' => 'home', 'action' => 'index'));
     }
   }
};
?>

 

As you can see my application controller doesn't extend "Controller", but I'm still calling upon its methods. By having it extend from another class I get the easy benefit of not allowing the controller to be a "page" controller which means I cannot place action functions inside of it and render pages. Instead, it is simply there to DRY up some code and allow my other page controllers to access its methods. So how do we accomplish this and what does AbstractApplicationController look like? We'll get to that in a second. First, let me show you the two parts of the parent controller which we will need. Basically, directly before our method actionIndex in HomeController is invoked, we will create a new ApplicationController object in our parent controller so we can start communication.

 

$this->applicationController = new ApplicationController($this);

 

We pass $this to its constructor because the AbstractionApplicationController is basically a placeholder (or proxy) for the controller object.

 

controller =& $controller;
      else
         throw new Exception(get_class($controller) .' must be a subclass of Controller');
   }

   public function getController()
   {
     return $this->controller;
   }

   public function __call($meth, $args)
   {
     return call_user_func_array(array($this->controller, $meth), $args);
   }
};
?>

 

The beauty is in the magic method __call()
As you can see any method not found in your ApplicationController will find the magic __call() method and attempt to execute the method on the cached controller. This means when we redirect or add messages to the flash, we can use the same syntax as we would in our page controllers. This gives us one-way communication. What about accessing the "requireUser" method from the page controller? Again, there is beauty in __call(). We place the same thing in the base controller so that any method invoked from our page controller that isn't part of the base/page controller should be redirected to the ApplicationController object. Let's take a look.

 

applicationController->$meth();
}
?>

 

This allows us to call $this->requireUser() in our HomeController' actionIndex() method and it will invoke through base controller's __call() on the ApplicationController object. Now we have two-way communication between our page controllers and our application controller. Keep in mind that since the ApplicationController is not a child of Controller, we can only call upon public methods inside Controller and our page controllers.

The Filter

Rails has a great option for allowing the developer to invoke certain methods before or after the current page’s action is invoked. I wanted this bonus also.

 

 array('except' => array('myExceptionAction'))
   );

   public function actionIndex()
   {
      //before filter method will be invoked before this method is invoked
   }

   public function actionMyExceptionAction()
   {
      //before filter method will not be invoked for this
   }
};
?>

 

If you aren't familiar with rails, don't fret, the logic is simple. I'm using the protected $BEFORE_FILTER to tell my parent controller to execute the requireUser() method before it executes any of the action methods except actionMyExceptionAction(). You can change the "except" key to "only" to change from a black-list to a white-list. Where is the requireUser() method? Well, If you haven't fallen asleep yet, you should know that requireUser() resides in our ApplicationController. How does this code work?

The beauty is in PHP’s reflection class.

 

applicationController = new ApplicationController($this);
//run before filter
$this->filter('before');

private function filter($temporality)
{
  $filter = strtoupper($temporality.'_FILTER');
  $controllerName = $this->getControllerName().'Controller';
  $controller = new ReflectionClass($controllerName);

  foreach ($controller->getProperties() as $property)
  {
    if ($property->name === $filter)
    {
      $filter = $this->{$property->name};
      if (!is_array($filter))
      {
 	settype($filter, 'array');
 	$filter[$filter[0]] = $filter[0];
 	unset($filter[0]);
      }

      foreach ($filter as $method => $options)
      {
 	$action = $this->currentAction();
 	if(!is_array($options) || (!isset($options['only'])
          && !isset($options['except'])))
	      $this->$method();
	elseif (isset($options['only'])
          && in_array($action, $options['only']))
	      $this->$method();
	elseif (isset($options['except'])
          && !in_array($action, $options['except']))
	      $this->$method();
      }
    }
  }
}
?>

 

Basically, we use reflection on our GamesController and then find any properties that match our filter name (BEFORE_FILTER). We make sure to turn it into an array if it isn't (which means we can do BEFORE_FILTER = 'requireUser';), and then invoke the method based on whether or not it fits the description. If "only" and "except" do not exist then it should be invoked. Otherwise, it should not be invoked if the current page's action is not in the "only" array or if it is in the "except" array. As you can see, you simply need to add protected $AFTER_FILTER = 'someMethodHere'; to have an after filter invoke some method. There you have it!

This concludes our crazy talk of making your framework more like rails (or at least adding bits and pieces to make your life easier).

 

Leave a comment