Organize directories for applications with front-end and back-end using WebApplicationEnd behavior

Previously there was described a way to build an application with front-end and back-end. I would like to continue this theme and suggest another way to organize directories using WebApplicationEnd behavior.

If you are not familiar with events & behaviors yet, then please, read this tutorial to get basic knowledge about using them.

First, create a new web application with yiic tool. I used an application generated by Yii 1.1.1, but everything described below should work with other versions of Yii Framework (probably with minor changes).

Now let's modify some directories' structure to separate front-end and back-end related files.

Since application's ends usually use the same models, but different controllers and views, we will separate them by creating two subdirectories under protected/controllers and protected/views directories:

webroot/
    protected/
        components/
            Controller.php
            UserIdentity.php
        controllers/
            /front
                SiteController.php
            /back
                SiteController.php
        views/
            /front
                /layouts
                    column1.php
                    column2.php
                    main.php
                /site
                   /pages
                       about.php
                   contact.php
                   error.php
                   index.php
                   login.php
            /back
                /layouts
                    main.php
                /site
                    error.php
                    index.php
                    login.php

Front-end SiteController and all front-end views are files generated by yiic tool. You have to create back-end SiteController and back-end views by yourself (or just copy and modify front-end ones).

Note: Since a new full path to front-end layouts is "application.views.front.layouts", you have to edit "column1.php" and "column2.php" files to render "application.views.front.layouts.main" as a parent layout. Also set Controller's (protected/components/Controller.php) layout property to 'column1'.

Now let's create different config files for both ends. Since these files usually have much in common, we will "inherit" them from the main.php config:

webroot/protected/config/front.php:

return CMap::mergeArray(
    require(dirname(__FILE__).'/main.php'),
    array(
        // Put front-end settings there
        // (for example, url rules).
    )
);

webroot/protected/config/back.php:

return CMap::mergeArray(
    require(dirname(__FILE__).'/main.php'),
    array(
        // Put back-end settings there.
    )
);

By default, Yii will try to find controllers and views in protected/controllers and protected/views directories respectively. We have to change this behavior and force Yii to search controllers and views in the "back" or "front" subdirectories depending on the currently running end.

Actually we can do it in the -end's config file by setting "viewPath" and "controllerPath" properties, but what if we are going to have some modules like News, Articles, etc.? We'll need to set these properties for them too. We can also have some back-end modules which don't need such separation.

Here comes the Yii magic. In protected/components directory create a file "WebApplicationEndBehavior.php" with the following contents:

class WebApplicationEndBehavior extends CBehavior
{
    // Web application end's name.
    private $_endName;
 
    // Getter.
    // Allows to get the current -end's name
    // this way: Yii::app()->endName;
    public function getEndName()
    {
        return $this->_endName;
    }
 
    // Run application's end.
    public function runEnd($name)
    {
        $this->_endName = $name;
 
        // Attach the changeModulePaths event handler
        // and raise it.
        $this->onModuleCreate = array($this, 'changeModulePaths');
        $this->onModuleCreate(new CEvent($this->owner));
 
        $this->owner->run(); // Run application.
    }
 
    // This event should be raised when CWebApplication
    // or CWebModule instances are being initialized.
    public function onModuleCreate($event)
    {
        $this->raiseEvent('onModuleCreate', $event);
    }
 
    // onModuleCreate event handler.
    // A sender must have controllerPath and viewPath properties.
    protected function changeModulePaths($event)
    {
        $event->sender->controllerPath .= DIRECTORY_SEPARATOR.$this->_endName;
        $event->sender->viewPath .= DIRECTORY_SEPARATOR.$this->_endName;
    }
}

Now add some lines to the main config file:

'behaviors'=>array(
    'runEnd'=>array(
        'class'=>'application.components.WebApplicationEndBehavior',
    ),
),

Now our application has a new method runEnd (to run one of the application's ends) and a new event onModuleCreate. By raising this event from a web module we can change modules' properties. Controllers and views paths are changed in the attached handler "changeModulePaths".

If you have a module, which should use different controllers and views for different ends, then just modify it's init() method:

protected function init()
{
    // ...    
 
    // We can configure our module depending on the value
    // of Yii::app()->endName.
    $this->foo = (Yii::app()->endName == 'front') ? 'bar1' : 'bar2';
 
    // Raise onModuleCreate event.
    Yii::app()->onModuleCreate(new CEvent($this));
}

Note that in this case the module's controllers and views paths must be organized as shown before.

If a module doesn't need a separation to back-end and front-end controllers and views, then just omit the onModuleCreate event's raising.

Finally, let's protect back-end by creating a parent controller for all back-end controllers:

webroot/protected/components/BackEndController.php:

class BackEndController extends CController
{
    public $layout='layout_name';
    public $menu=array();
    public $breadcrumbs=array();
 
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
 
    public function accessRules()
    {
        return array(
            array('allow',
                'users'=>array('*'),
                'actions'=>array('login'),
            ),
            array('allow',
                'users'=>array('@'),
            ),
            array('deny',
                'users'=>array('*'),
            ),
        );
    }
}

webroot/protected/controllers/back/SiteController.php must extend this controller to perform access checking.

Everything's done. New index.php and backend.php files are:

webroot/index.php:

$yii = dirname(__FILE__).'/../yii/framework/yii.php';
$config = dirname(__FILE__).'/protected/config/front.php';
 
// Remove the following lines when in production mode
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL', 3);
 
require_once($yii);
Yii::createWebApplication($config)->runEnd('front');

webroot/backend.php:

$yii = dirname(__FILE__).'/../yii/framework/yii.php';
$config = dirname(__FILE__).'/protected/config/back.php';
 
// Remove the following lines when in production mode
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL', 3);
 
require_once($yii);
Yii::createWebApplication($config)->runEnd('back');

Summary

The created behavior delivers us from specifying controllers and views paths for the application and all it's modules by using runEnd() method and invoking the onModuleCreate event in necessary places.

Also modules became more self-sufficient and can be easily integrated with the existing front-end and back-end layouts.

Appendix A. How to use yiic tool.

You can easily generate models and views for the front-end the old way without any changes. Just go to the application's webroot directory and execute the next command:

protected/yiic shell
or
path/to/php.exe protected/yiic shell

All generated models will be saved under protected/models directory, and controllers & views will be saved under protected/(controllers|views)/front directory.

Generating controllers and views for the back-end is a little different. You have to run yiic the next way:

protected/yiic shell backend.php
or
path/to/php.exe protected/yiic shell backend.php

Now all generated controllers and views will go to protected/(controllers|views)/back directory.

Appendix B. Back-end url rules.

Usually you don't need to setup url rules for the back-end, but if you want to do this, then you'll have to modify .htaccess as follows:

AddDefaultCharset utf-8

Options +FollowSymLinks
IndexIgnore */*
RewriteEngine on

# Make the backend accessible via url: http://site/backend.
RewriteRule ^backend backend.php

# If a directory or a file exists, use it directly.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Otherwise forward it to index.php.
RewriteRule . index.php

Then, in the back.php config file setup url manager component:

'urlManager'=>array(
    'urlFormat'=>'path',
    'showScriptName'=>false,
    'rules'=>array(
        'backend'=>'site/index',
        'backend/<_c>'=>'<_c>',
        'backend/<_c>/<_a>'=>'<_c>/<_a>',
    ),
),

Now you can generate beautiful urls using CHtml::link() method.

If you have a module (e.g. news), then you'll need to add 3 more rules before existing ones:

'backend/news'=>'news',
'backend/news/<_c>'=>'news/<_c>',
'backend/news/<_c>/<_a>'=>'news/<_c>/<_a>',

Also, you still can add own rules:

'backend/news/<id:\d+>'=>'news/newsReport/update',

But make sure that you insert these rules before news rules containing <_c> and <_a>. Otherwise the latter rules will be used instead.

Total 17 comments:

#1065
Doude about rewrite url
by Shankar at 2:55pm on February 3, 2010.

Hey good way organize the directorys, very good. But i have the doude about how i can to use the mod_rewrite with the backend and frontend.

The first is the file .htaccess, and next how i will to create the respectives URL in the backend and frontend.

Once again I thank you for attention and solution for this organization.

#1067
htaccess & url manager
by andy_s at 2:49pm on February 3, 2010.

.htaccess file may look like:

Options +FollowSymLinks
IndexIgnore */*
RewriteEngine on

# Make backend accessible via url: http://site/backend.
RewriteRule ^backend backend.php

# If a directory or a file exists, use it directly.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Otherwise forward it to index.php.
RewriteRule . index.php

Usually you don't need to rewrite urls in the backend. But if you want to, then add urlManager component to the backend config file (see frontend config). It can look like:

'urlManager'=>array(
    'urlFormat'=>'path',
    'showScriptName'=>false,
    'urlSuffix'=>'',
    'rules'=>array(
        'backend'=>'site/index',
        // News.
        'backend/news'=>'News/newsReport/list',
        'backend/news/'=>'News/newsReport/update',
    ),
),

To generate appropriate links use CHtml::link method. For example:

CHtml::link('News', array('News/newsReport/list'));
#1072
Thank you
by Shankar at 2:00am on February 4, 2010.

Thank you man.

#1073
Thanks for sharing this
by atrandafir at 2:47am on February 5, 2010.

Thank you, I think i'll use this for a project to have a better structure.

#1087
Not working :(
by emix at 2:49am on February 9, 2010.

Everything looks fine - but - I have rewritten my application to look like above and now I cannot login :(

#1088
to emix
by andy_s at 2:37am on February 9, 2010.

The login part is not described here particularly. Please, reference the guide: http://www.yiiframework.com/doc/guide/topics.auth

(you'll need create UserIdentity class)

#1089
to andy_s
by emix at 2:26pm on February 9, 2010.

I had a fully working application and have rewritten it to allow more front ends. After followed above steps I cannot log in. The script does not work and I don't know what's going on :(

#1090
to emix #2
by andy_s at 2:58pm on February 9, 2010.

Maybe you forgot to add a user component to a front-end config (if you want to allow users log in in the front-end)?

#1247
doubt
by isreal at 3:41am on March 9, 2010.

what I dont understand is why and where should I put this code:

protected function init() { // ...

// Depending on the value of Yii::app()->endName we can configure our module in different ways.
$this->newsPerPage = (Yii::app()->endName == 'front') ? 5 : 15;

// Raise onModuleCreate event.
Yii::app()->onModuleCreate(new CEvent($this));

}

besides this, this is an excellent idea, I like it...

#1264
to isreal
by andy_s at 3:20am on March 12, 2010.

If you have a module with a front-end and back-end views, then we should use front-end views if the front-end is running, and back-end views otherwise.

Yii::app()->onModuleCreate(new CEvent($this));

Raises onModuleCreate event and says to the application to change this module's views path to moduleId/views/front and controllers path to moduleId/controllers/front.

This line:

$this->newsPerPage = (Yii::app()->endName == 'front') ? 5 : 15;

is just to show that you can use endName property to change some logic of the application depending on what "end" (front or back) is currently running now.

#1276
No need for another script in your root!
by Nique at 3:54am on March 15, 2010.

Hi there,

There is no need for another script in your root. If you have privileges to setup subdomains try this!

  • Setup a sub-domain (like http://backend.domain.com) and let it point to the same root.
  • Put the following code in the index.php bootstrap

    define('YII_DEBUG', true);
    require_once '../../lib/yii/yii.php';
    
    if(($sd = explode('.',$_SERVER['HTTP_HOST'])) && $sd[0] == 'backend')
        Yii::createWebApplication('./path/to/config/backend.php')->runEnd('back');
    else 
        Yii::createWebApplication('./path/to/config/frontend.php')->runEnd('front');
    
#1375
nice but...
by mech7 at 4:57am on April 10, 2010.

The default oulmn1 view does not render anymore.. main works..

I think it has something to do with..

<?php $this->beginContent('application.views.layouts.main'); ?>

but changing it to:

<?php $this->beginContent('main'); ?>

Does not work too..

#1376
url
by mech7 at 4:07am on April 10, 2010.

Also if put the url rules in back or front.php.. I get :

Property "CWebApplication.urlManager" is read only.

#1377
ah nevermind.. i forget
by mech7 at 4:27am on April 10, 2010.

I forgot to put it in a component array.. but still have some problems though.. in backend url's look like:

backend/site/page?view=about

if go to /page/view/about it does work.. but GHtml::link probably not work good

#1461
layouts not working
by Yarrgh at 5:10pm on May 5, 2010.

Just responding to mech7. You said that this didn't work:

<?php $this->beginContent('application.views.layouts.main'); ?>

The reason this doesn't work is because the views are now seperated into front and back. The layouts column1 and column2 don't know you changed that. You need to change them respectively. Example

for the frontend layout <?php $this->beginContent('application.views.front.layouts.main'); ?>

for the backend layout <?php $this->beginContent('application.views.back.layouts.main'); ?>

#1558
How can i automatically generate code with the new generator?
by nielsilmer at 6:39pm on June 4, 2010.

Does anybody already generate code with the new generator with this structure?

#1784
How can I create url from backend to frontend ?
by r0n9.GOL at 8:11am on August 12, 2010.

How can I create url from backend to frontend ?

Your Comment:

You may enter comment using Markdown syntax.

Please login with your forum account.
Note: you must have at least ONE forum post with your account.