Zend Framework: Router for subdirectory-based site

Posted by Dmytro Shteflyuk on under PHP · Русский (48,585 views)

I started discovering of Zend Framework and was confronted with a problem. When I’ve placed my test sample into site’s subdirectory (http://localhost/test/), default router tried to find TestController which is not exists of course and routed me to IndexController/noRoute. It’s not good for me. I decided to create my own router.

There some inconsistence in Zend Framework (maybe because this is first release). To create my own router I need to develop class which implements Zend_Controller_Router_Interface interface:

1
2
3
4
5
6
7
8
9
10
11
interface Zend_Controller_Router_Interface
{
    /**
     * Processes an HTTP request and routes to a Zend_Controller_Dispatcher_Action object.  If
     * no route was possible, an exception is thrown.
     *
     * @throws Zend_Controller_Router_Exception
     * @return Zend_Controller_Dispatcher_Action|boolean
     */

    public function route();
}

Zend_Controller_Front uses this interface in following way:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Zend_Controller_Front
{
  // ...
  /**
   * Dispatch an HTTP request to a controller/action.
   */

  public function dispatch()
  {
    // ...
    $action = $this->getRouter()->route($this->getDispatcher());
    // ...
  }
}

Fine joke from Zend Framework’s developers. Front controller passes dispatcher to router, but router interface does not receive it. :-) Simplest way to avoid PHP error message is to define parameter which has default value null. Here my router class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/** Zend_Controller_Router_Interface */
require_once 'Zend/Controller/Router/Interface.php';

/** Zend_Controller_Dispatcher_Interface */
require_once 'Zend/Controller/Dispatcher/Interface.php';

/** Zend_Controller_Router_Exception */
require_once 'Zend/Controller/Router/Exception.php';

/** Zend_Controller_Dispatcher_Action */
require_once 'Zend/Controller/Dispatcher/Action.php';

class SubDirectoryRouter implements Zend_Controller_Router_Interface
{
  public function route(Zend_Controller_Dispatcher_Interface $dispatcher = null)
  {
    // SubDirectoryRouter: what's the path to where we are?
    $pathIndex = dirname($_SERVER['SCRIPT_NAME']);

    // SubDirectoryRouter: remove $pathIndex from $_SERVER['REQUEST_URI']
    $path = str_replace($pathIndex, '', $_SERVER['REQUEST_URI']);
    if (strstr($path, '?')) {
      $path = substr($path, 0, strpos($path, '?'));
    }
    $path = explode('/', trim($path, '/'));

    /**
     * The controller is always the first piece of the URI, and
     * the action is always the second:
     *
     * http://zend.com/controller-name/action-name/
     */

    $controller = $path[0];
    $action     = isset($path[1]) ? $path[1] : null;

    /**
     * If no controller has been set, IndexController::index()
     * will be used.
     */

    if (!strlen($controller)) {
      $controller = 'index';
      $action = 'index';
    }

    /**
     * Any optional parameters after the action are stored in
     * an array of key/value pairs:
     *
     * http://www.zend.com/controller-name/action-name/param-1/3/param-2/7
     *
     * $params = array(2) {
     *              ["param-1"]=> string(1) "3"
     *              ["param-2"]=> string(1) "7"
     * }
     */

    $params = array();
    for ($i=2; $i<sizeof($path); $i=$i+2) {
      $params[$path[$i]] = isset($path[$i+1]) ? $path[$i+1] : null;
    }

    $actionObj = new Zend_Controller_Dispatcher_Action($controller, $action, $params);

    if (!$dispatcher->isDispatchable($actionObj)) {
      /**
       * @todo error handling for 404's
       */

      throw new Zend_Controller_Router_Exception('Request could not be mapped to a route.');
    } else {
      return $actionObj;
    }
  }
}

Sample usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require('Zend.php');

Zend::loadClass('Zend_Controller_Action');
Zend::loadClass('Zend_Controller_Front');
Zend::loadClass('Zend_Filter');
Zend::loadClass('Zend_InputFilter');
Zend::loadClass('Zend_View');

Zend::loadClass('SubDirectoryRouter');

$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory('controllers');

$router = new SubDirectoryRouter();
$controller->setRouter($router);

$controller->dispatch();

To make it work you need to place SubDirectoryRouter to the SubDirectoryRouter.php file and put it under include path.

12 Responses to this entry

Subscribe to comments with RSS

said on March 8, 2006 at 5:24 pm · Permalink

You will be pleased to see that the interface has been fixed in Preview Release 0.1.2, which is now available for download from the Zend Framework website (http://framework.zend.com).

said on March 9, 2006 at 10:26 am · Permalink

Thanks, Mike. I will look at new version. I think it’s too early to use it in real projects, but idea is great. This framework can become one of the most popular thing in PHP world.

Rob, no comments :-) My code looks too similar with your one. Maybe it’s really great problem for people who started working with this framework.

said on March 9, 2006 at 5:42 pm · Permalink

As I understood from mailing list, default router allready patched and will be available in next release (0.1.3).

Dinh:

I patched Router.php in ZF with your code:

1
2
3
4
5
6
7
8
9
10
/**
 * @todo Replace with Zend_Request object
 */

//$path = $_SERVER['REQUEST_URI'];

// add

// what's the path to where we are?
$appRoutePath = dirname($_SERVER['SCRIPT_NAME']);        
$path = str_replace($appRoutePath, '', $_SERVER['REQUEST_URI']);

It works like a charm.

Михаил @
said on March 17, 2006 at 1:30 pm · Permalink

Приветствую, Дмитро!

Спасибо за весьма интересные исследования!
Меня, вот, интересует, как в ZF реализовать ЧПУ вроде: http://host/news/2006/01/01/this-is-a-cool-article-title/ ?

Заранее спасибо!
С уважением,
Михаил

said on March 17, 2006 at 3:11 pm · Permalink

Здравствуйте, Михаил

Во-первых, Вам нужно создать собственный роутер, поскольку стандартный предназначен для передачи параметров в виде:
http://host/controller/action/param1/value1/param2/value2/

Вы можете скопировать код стандартного роутера, и на его основе создать свой, изменив следующие строки:

Обработка запроса:

1
2
3
4
5
6
7
8
9
//...
    $controller = $path[0];
    $action     = isset($path[1]) ? $path[1] : null;
 
    if (!strlen($controller)) {
      $controller = 'index';
      $action = 'index';
    }
//...

на

1
2
3
4
//...
    $controller = $path[0];
    $action = 'index';
//...

Обработка параметров:

1
2
3
4
5
6
//...
    $params = array();
    for ($i=2; $i<sizeof($path); $i=$i+2) {
      $params[$path[$i]] = isset($path[$i+1]) ? $path[$i+1] : null;
    }
//...

на что-то вроде

1
2
3
4
5
6
//...
    $params = array();
    for ($i=1; $i<sizeof($path); $i++) {
      $params[] = $path[$i];
    }
//...

Теперь осталось просто определить NewsController с методом index().

На следующей неделе я собираюсь написать пример использования Zend Framework, где будет использоваться подобная техника.

Михаил @
said on March 17, 2006 at 4:05 pm · Permalink

Спасибо, Дмитро!
Я просто сам только “въезжаю” во фремворки.. Пытался осилить Symfony — не получилось, CakePHP — попроще, тем не менее, все говорят, что время на их изучение для создания рабочего приложения (а не просто blabla.php) нужно отсчитывать в месяцах. Почитал обзор ZF и заинтересовался очень, поскольку спустя 2 часа могу что-то изобразить тогда как с Ruby-oriented type of frameworks приходится делать слишком много операций, что только осложняет работу. Хотя скорее всего все от того, что у меня просто нет много времени для их изучения. Порой даже кажется, что надо сперва изучить структуру ROR, чтоб потом что-то написать с помощью Symfony/Cake :)))))
Чего вот очень хотелось бы, тем не менее, в ZF, так это поддержку ORM, т.е. чего-то вроде Propel, но желательно еще проще. :))

Случайно совсем наткнулся на ваш сайт, так что теперича буду следить.. ;)

С уважением,
Михаил

said on March 17, 2006 at 4:21 pm · Permalink

К сожалению, никаких средств ORM в ZF не будет (по заявлениям разработчиков). Фреймворк преследует несколько целей, включая отсутствие необходимости в файлах конфигурации (configuration-less), простота, эффективность. Propel же не только требует настроить, что весьма и весьма нетривиально, но и вводит еще один этап – генерацию классов (хотя, надо признать, довольно удобно получается :-) ). В общем именно такая тема поднималась в списках рассылки, и разработчики сказали свое веское “нет”. Посмотрим, если будет желание, попробую их как-нибудь красивенько увязать :-) (но обещать ничего не буду за неимением достаточного количества свободного времени).

Kevin Campion @
said on April 19, 2006 at 1:27 am · Permalink

I have just tested the Zend Framework, and I was confronted with the same problem (subdomain).
I found your explanation after some research. But with version 0.1.2 of Zend Framework, it doesn’t work.

There are the changes which I carried out for work :

At the beginning I used this tutorial to have a little application for testing : http://phparch.com/zftut/index.php?p=0

My structure look like this:

1
2
3
4
DOCUMENT ROOT >         /www
My First Application >      /www/project1
Zend Framework Library >    /www/zend/ZendFramework-0.1.2/library
(php.ini contains include_path = ".:/www/zend/ZendFramework-0.1.2/library")

I wished to check my application by http://example.org/project1/

1
2
3
4
5
6
7
/www/project1/.htaccess :
    RewriteEngine on
    RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php

/www/project1/app/controllers/IndexController.php

/www/project1/app/views/index.php

/www/project1/index.php :

1
2
3
4
5
6
7
8
9
10
setScriptPath('./app/views');
Zend::register('view', $view);

$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory('./app/controllers');
 
$router = new SubDirectoryRouter();
$controller->setRouter($router);
 
$controller->dispatch();

When i check “http://localhost/project1/” I’ve got this error :

1
Fatal error: Class 'Zend_Controller_Dispatcher_Action' not found in  /www/zend/ZendFramework-0.1.2/library/SubDirectoryRouter.php on line 73

It is cause of: $actionObj = new Zend_Controller_Dispatcher_Action($controller, $action, $params);
Replace it by $actionObj = new Zend_Controller_Dispatcher_Token($controller, $action, $params);
and in top of the file SubDirectoryRouter.php move require_once 'Zend/Controller/Dispatcher/Interface.php';
to require_once 'Zend/Controller/Dispatcher/Token.php';

In Zend Framework 0.1.2 it’s the right way (cf /www/zend/ZendFramework-0.1.2/library/Zend/Controller/Router.php)

When i recheck “http://localhost/project1/” it work.

D’ont forget to change in all views :
by
or
escape($entry['id']); ?>”> by escape($entry['id']); ?>”>

said on October 10, 2007 at 7:08 pm · Permalink

[...] Zend Framework: Router for subdirectory-based site || Dmytro Shteflyuk’s Home – I’ve had problems with Zend Framework: Router for subdirectory-based site || Dmytro Shteflyuk’s Home [...]

Comments are closed

Comments for this entry are closed for a while. If you have anything to say – use a contact form. Thank you for your patience.