PHP 247
September 09, 2010, 01:34:31 PM *
Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length
 

Pages: [1]
Print
Author Topic: Writing a CMS/Community with Smarty and the Zend Framework  (Read 553 times)
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« on: February 27, 2010, 02:41:11 AM »

(from phpnubie - phpvn.org)

I just found this great tutorials and wanna bring it hither for all, who's interested in. Here we go  Grin:

Source:http://www.prodevtips.com
Background
I’ll make this short so we can get on with the tutorial: The reason I’m making this system is that we use Joomla 1.5 for our CMS related stuff here and Community Builder (A.K.A Joomlapolis) just don’t work very well with 1.5. Alas, for the current project I’m doing we need some basic blog and gallery functionality for each member and as far as I know there is no real replacement for Community Builder. Checking out typo3 alternatives didn’t work out well either. At a quick glance CWT Community didn’t seem to have the features I was looking for, neither did HOI Community. Any Community looked more promising but the alpha status didn’t really excite me.

Tutorial
So the requirements for the system we are going to build are some kind of CMS capability combined with a community. In this first phase each member will have her own simple blog and gallery. The result will not be near as advanced as Joomla, especially when it comes to the admin area. This system will require people with some programming and designing skill to setup in order to get a new site going. Massive amounts of time and effort has been expended to make Joomla a simple thing to install and administer, time I don’t have.

Let’s start with more specific requirements:
1. We need to be able to insert components everywhere, just like in Joomla with the jdoc tag.
2. The designer has to be able to do as much as possible in the Smarty code. The reason for this is that we don’t want this to become some archaic thing that will require a lot of programmer effort to setup for each new project. We want to empower the designer! That this could mean less boring work for us programmers is another matter…

If you know how to instantiate controllers in the Zend Framework you know that #1 can be taken care of by a combination of instantiating controllers and the Smarty insert function. The way this will work is basically identical to the procedure described in the Smarty widget tutorial. The plan for the overall “flow” will look like this:
1. A call is made to the IndexController which is responsible for rendering the main page, for instance with a call to Smarty->display(). It will however assign various variables to the Smarty template which will be used to render other elements.
2. In the Smarty template code we now have Smarty insert functions which will load controllers based on the parameters they get and display the output from these controllers.
3. We also have a main content area that is passed parameters assigned in the IndexController which in turn got these parameters as a result of user interaction.

Before we begin I want to stress that this is not a newbie tutorial. If the current level is too much for you I recommend these tutorials:
For the Zend Framework
For Smarty

Let’s begin with menu generation:
In my index.tpl (which is based on YAML) the following will render a menu:

{insert name=”item” controller=”Menu” id=”topmenu”}

Ok so it’s a Smarty insert function, lets take a look at the function:

Code:
function insert_item($params){
    $function = empty($params['function']) ? 'render' : $params['function'];
    $controller = empty($params['controller']) ? 'index' : $params['controller'];
    return Common::loadController($controller)->$function($params);
}

Since we didn’t pass a function in the parameter list it will default to “render” in the above example. The controller is apparently called “Menu”. Let’s look at the current code for MenuController:

Code:
class MenuController extends ExtController{
 
    function init(){
        parent::init();
        $this->name = 'menu';
        $this->obj = Common::loadModel($this->name);
        parent::finishInit();
    }
 
    function render($params){
        $menu = $this->obj->fetchRowToArr($params['id'], false, 'menu_slug');
        $menu_items = $menu->findDependentRowset('menuitem', 'db_menu')->toArray();
        Common::sort2DAsc($menu_items, 'position');
        $this->smarty->assign('menu', $menu->toArray());
        $this->smarty->assign('menuitems', $menu_items);
        return $this->fetch();
    }
 
}

Common is a class with only static methods used to kind of extend the basic functions of PHP, that is also the place where I have all my factory functions. ExtController is a custom class I use to extend Zend_Controller_Action with. All my controllers extend this class. And the top of it looks like this:

Code:
class ExtController extends Zend_Controller_Action{
 
    function __construct($request = false, $response = false, $invokeArgs = false){
        if(!$request)
            $request = new Zend_Controller_Request_Http();
 
        if(!$response)
            $response = new Zend_Controller_Response_Http();
 
        if(!$invokeArgs)
            $invokeArgs = array();
 
        parent::__construct($request, $response, $invokeArgs);
    }
 
    function init(){
        $registry = Zend_Registry::getInstance();
        $this->smarty = new Smarty_Zend();
        $this->date_format = $registry['date_format'];
        $this->db = $registry['db'];
        $this->mysql_date_format = $registry['mysql_date_format'];
        $this->base_url = $this->_request->getBaseUrl();
        $this->page_limit = 3;
        $this->globalsess = new Zend_Session_Namespace('globalsess');
        $this->globalsess->language = "eng";
        $this->acl = new Zend_Session_Namespace('acl');
        $this->smarty->assign('baseUrl', $this->base_url);
        $this->smarty->assign('date_format', $this->date_format);
    }
 
    function finishInit(){
        $this->smarty->assign('controller', $this->name);
        $this->template = $this->name.".tpl";
        $this->session = new Zend_Session_Namespace($this->name);
        $this->smarty->assign('config_file', “{$this->globalsess->language}_{$this->name}.conf");
    }
 
    function display($template = false){
        if($template == false){
            $page = utf8_encode($this->smarty->fetch($this->template));
        }else
            $page = utf8_encode($this->smarty->fetch($template));
        echo $page;
        exit;
 
    }
 
    function fetch($template = false){
        if($template == false)
            return utf8_encode($this->smarty->fetch($this->template));
        else
            return utf8_encode($this->smarty->fetch($template));
    }

Apparently we create an instance of some model in the MenuController, let’s first look at the Common::loadModel() factory function:

Code:
class Common{
    static function loadModel($table){
        $class_name = strtolower($table);
        include_once($class_name.".php");
        return new $class_name;
    }

And this is the contents of models/menu.php:

Code:
class Menu extends ExtModel{
    protected $_name = 'db_menus';
    protected $_primary = 'id';
}

In the MenuController you might have noticed that we use the ZF function findDependentRowset with ‘menuitem’ so lets take a look at models/menuitem.php too:

Code:
class Menuitem extends ExtModel{
    protected $_name = 'db_menuitems';
    protected $_primary = 'id';
    protected $_referenceMap = array(
        'db_menu' => array(
            'columns' => 'db_menu_id',
            'refTableClass' => 'Menu',
            'refColumns' => 'id',
        )
    );
 
}
All my models extend a class called ExtModel, let’s look at that and the fetchRowToArr function that is called in the MenuController:

Code:
class ExtModel extends Zend_Db_Table_Abstract{
 
    function fetchRowToArr($id_value = false, $as_array = true, $id_field = false, $where = false){
        if($where != false){
            $where_sql = array();
            foreach($where as $field => $value)
                $where_sql[] = $this->getAdapter()->quoteInto("$field = ?", $value);
            $row = $this->fetchRow($where_sql);
        }else if($id_field == false){
            $row = $this->find($id_value)->current();
        }else{
            $where = $this->getAdapter()->quoteInto("$id_field = ?", $id_value);
            $row = $this->fetchRow($where);
        }
 
        if($as_array == true && $row != false)
            return $row->toArray();
        else
            return $row;
    }

The name of the fetchRowToArr function is not representative, It should really be renamed since it obviously can return row objects too. Now that we have all this information let’s recap what happens in the MenuController:

1. The init() function is always run with some general stuff through parent::init() and parent::finishInit(). We will use the variable name consistently to emulate some of the convention over configuration mentality of Ruby on Rails.
2. The render() function is called with the id ‘topmenu’ which we first use to retrieve all the information for that menu through fetchRowToArr().
3. Next we use that information to retrieve all menuitems with the help of the $_referenceMap in the menuitem model.
4. The two dimensional $menu_items array gets sorted by position. I couldn’t find any way to do this through findDependentRowset() or the reference map in the menuitem model. If you know of some way of getting the ORDER BY clause into the underlying SQL (without changing the source of course), please comment on this post!
5. We assign the menu and the items it contains to the template.
6. Finally we return the result which will replace the smarty insert code above.

The menu.tpl template which we use in the above code looks like this if a menu is set as “flowing” for instance:

Code:
{elseif $menu.menu_type eq “flowing"}
    <table class="{$menu.menu_class}">
        <tr>
            <td>
            {foreach from=$menuitems item=menu_item}
                {if empty($menu_item.item_class)}
                    <span class="{$menu.menu_itemclass}">
                {else}
                    <span class="{$menu_item.item_class}">
                {/if}
                 
                <a href="{$baseUrl}/{$menu_item.item_link}">
                    {if empty($menu_item.item_image) == false}
                        <img src="{$baseUrl}/images/{$menu_item.item_image}" alt="{$menu_item.image_alt}" />
                    {/if}
                     
                    {if empty($menu_item.link_text) == false}
                        {$menu_item.link_text}
                    {/if}
                </a>
                </span>
            {/foreach}
            </td>
        </tr>
    </table>
{/if}

And lastly the SQL for the menus and menuitems:

Code:
CREATE TABLE `db_menus` (
`id` bigint(12) NOT NULL auto_increment,
`menu_class` varchar(250) NOT NULL,
`menu_itemclass` varchar(250) NOT NULL,
`menu_type` varchar(250) NOT NULL,
`menu_slug` varchar(250) NOT NULL,
PRIMARY KEY  (`id`),
UNIQUE KEY `menu_slug` (`menu_slug`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
CREATE TABLE `db_menuitems` (
`id` bigint(12) NOT NULL auto_increment,
`db_menu_id` bigint(12) NOT NULL,
`item_class` varchar(250) NOT NULL,
`item_link` varchar(250) NOT NULL,
`item_image` varchar(250) NOT NULL,
`image_alt` varchar(250) NOT NULL,
`link_text` varchar(250) NOT NULL,
`position` bigint(12) NOT NULL,
`item_imageover` varchar(250) NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

In the next part of this series we will take a look at the main content area where we display articles etc.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #1 on: February 27, 2010, 02:41:30 AM »

In this part we will take a look at how the main content area can change based on user actions. This is basically done in the same way we display menus with the difference that we have to give insert_item() dynamic data.
The insert block for the main area looks like this:

Code:
{insert name=”item” controller=”$controller” function=”$function” id=”$id”}

$controller, $function and $id have been assigned in the indexController. The function in question looks like this:
Code:
function dAction(){
        $params = $this->_request->getParams();
        $this->smarty->assign('id', $params['id']);
        $this->smarty->assign('controller', $params['c']);
        $this->smarty->assign('function', $params['f']);
        $this->display();
    }
So if we enter this in the address field:

http://localhost/index/d/c/article/id/dogs

We will call the above function with the parameter article as controller and nothing for function and dogs as id. Since we pass null as f the function to call will default to “render” as shown in part 2.

Let’s take a look at the article controller:
Code:
class ArticleController extends ExtController{
 
    function init(){
        parent::init();
        $this->name            = 'article';
        $this->obj             = Common::loadModel($this->name);
        parent::finishInit();
    }
 
    function render($params){
        if(empty($params['id']))
            return 'No parameters when trying to render article.';
        $article = $this->obj->fetchRowToArr($params['id'], false, 'article_slug');
        if($article->acl_level &gt; $this->getAclLevel()){
            if(empty($article->noacc_target))
                return Common::loadController('index')->frontPage();
            else
                return $this->aclNoacc($article->noacc_target);
        }
        $this->smarty->assign('article', $article->toArray());
        return $this->fetch();
    }
}

So we fetch the article with the help of the ‘article_slug’ field which in the above case would be “dogs”. We countinue by checking the access control list for this article to see if the current user is allowed to view it. Lets take a look at getAclLevel():
Code:
function getAclLevel(){
        if(empty($this->acl->acl_level))
            return 0;
        return $this->acl->acl_level;
    }

Simple enough, if we are not logged in which is the same as an empty acl namespace, then we return 0. We can conclude from this that an article which has an acl_level of 0 is viewable by everyone.

Let’s pretend that the article has an acl_level of 1 for instance. In that case we would not be able to see it when not logged in (the check above fails). We then proceed by checking if a field called ‘noacc_target’ is set. If it is not set then we call frontPage in IndexController:
Code:
function frontPage(){
        $where = array("front_page" =&gt; 1, "acl_level" =&gt; 0);
        $articles = Common::loadModel('article')->fetchToArr(false, false, true, $where);
        $this->smarty->assign('articles', $articles);
        return $this->fetch('front_page.tpl');
    }
The above function will simply try and fetch all articles with an access level of 0 and display them instead of the content we were not allowed to see. Pretty crude at this stage but it’s easy to see how we could make this logic more sophisticated. Lets take a look at fetchToArr in ExtModel:
Code:
function fetchToArr($id_value = false, $id_field = false, $as_array = true, $where = false){
        if($where != false){
            $where_sql = array();
            foreach($where as $field =&gt; $value)
                $where_sql[] = $this->getAdapter()->quoteInto("$field = ?", $value);
 
            $rowset = $this->fetchAll($where_sql);
        }else if($id_field == false || $id_value == false){
            $rowset = $this->fetchAll();
        }else{
            $sql = $this->getAdapter()->quoteInto("$id_field = ?", $id_value);
            $rowset = $this->fetchAll($sql);
        }
 
        if($as_array == true &amp;&amp; $rowset != false)
            return $rowset->toArray();
        else
            return $rowset;
    }
As is the case with fetchRowToArr() it’s pretty stupidly named as it obviously can return rowset objects too, not just arrays.

Now let’s retrace our steps, pretend we have a noacc_target value in the dogs article. In that case aclNoacc() executes as you can see above:
Code:
function aclNoacc($noacc){
        list($controller, $function) = explode("/", $noacc);
        return Common::loadController($controller)->$function();
    }
Simple enough. If we pretend that the value of noacc_target in the dogs article is “user/register” it’s easy to see that that will call the UserController with the function register(). That is what part 4 will be all about. User registration and all the headaches it brings with it in the form of for instance validating input and so on.

SQL for the article controller:

Code:
CREATE TABLE `db_articles` (
  `id` bigint(12) NOT NULL auto_increment,
  `article_slug` varchar(250) NOT NULL,
  `article_text` text NOT NULL,
  `article_class` varchar(250) NOT NULL,
  `acl_level` int(4) NOT NULL default '0',
  `noacc_target` varchar(250) NOT NULL,
  `front_page` tinyint(1) NOT NULL default '0',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `article_slug` (`article_slug`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=4 ;
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #2 on: February 27, 2010, 02:41:54 AM »

As I promised in part 3 of this series, this piece will be about user registration and login with all that it brings with it in the form of validation and so on.

The requirements for this part are:
1.) We have to put as much power into the hands of designers, project managers and others to enable them to do as much work by themselves as possible. Having them come to us for small inconsequential changes all the time is going to drive us insane. The main workhorse here will be Smarty.
2.) The registration has to be validated through user interaction, in effect clicking a link which we will send to her email address.
3.) Since the users will be paying through their phones we also need to validate their phone numbers. We will do this by sending them a four digit code with the help of our SMS gateway.
4.) Extra features like sending forgotten passwords and resending validation codes are also needed.

Unfortunately I’ve run in to the dreaded 404 wordpress mega bug. Therefore I will publish the code separately and refer to sections in it instead. It sucks but is better than nothing.

cms_part4_code.zip

Let’s begin by taking a look at the function responsible for rendering the registration in UserController, it’s section 1 in the source code. GetCityOptions() and getPhoneOptions() will return arrays with cities and prefix numbers respectively. We have to draw the line for maximum membership age somewhere and a hundred years should be enough. Smiley

Section 2 is the user_register_form template. I’ve left less interesting parts of it out for brevity. The holes have been marked with three vertical dots. The insert function calls at the top is a way of making requirement #1 happen. I fervently hope that a lot of non technical people will be able to manage the control of validation without a lot of explaining on my part. I got some inspiration from how it works in Flex here. However, in Flex the validators are set as attributes of each input field which is a hell of a lot cleaner. I suppose some kind of Smarty plugin could have been created to accomplish the same thing but I’m on a schedule here so that won’t happen. Besides, having all of them in one place is a good thing in itself.

insert_val() is section 3.

Common::checkMethod is section 4.

UserController->insertVal(): Section 5.

The way we work here is highly chaotic. New requirements can come any time and that is why I’ve intentionally made the fields array two dimensional even though at the moment the assignment might just as well have looked like this: $this->session->fields[$params[’field’]] = $validation. So there is a lot of unused “space” here that we could use for other things if we wanted.

Let’s check validationToArr() in section 6.

Here we basically sort more complicated stuff like “int, atleast-5″. You might also notice that we do not have a terms and conditions page yet. At the moment we just link to the frontpage.

So let’s pretend we have filled the form with some data and hit submit. Apparently that will call a method called save() in UserController in section 7.

That section is filled with a lot of method calls, lets start with validatePost() and validateField() in section 8.

Note that I call validatePost() with “insert” as the last parameter. This parameter has currently no function. Yet again I’m trying to think ahead of various erratic decision makers. At the moment the only validation in the login form is to check if a user with the entered information actually exists, I have no other specs. However, at some point in the future someone might decide that we have to validate this input too before we query the database. If that happens I could pass “login” or something instead and have the validation script validate in a different way based on that parameter, if need be.

Another noteworthy thing is that we call $smarty->get_template_vars() to load various validation errors from a language file. As you might have noticed already, this project is multilingual. It makes sense to separate copy from view in any case as it enables total cave men - that don’t even understand what HTML is - to be productive in the project.

Upon successful validation we generate a 4 digit code and call sendPhoneCode(), we also generate a unique id and call sendMail() in section 9.

Note the use of config loading with the help of Zend_Config_Ini().

I’m not going to show the login template, there is only one interesting thing there which will be explained soon, let’s jump to the login functions instead in section 10.

We make use of a row object in valEmail() which enables us to keep hassle to a minimum since all the database connection information is stored in the object itself. We simply change member variables and call save() to update, awesome.

In passwordSend() for instance we assign this: $this->smarty->assign(’email_error’, ‘yes’). In the template this information is used like this:

{insert name=”getText” yesno=$email_error text=#email_resend_error#}

And the insert function can be found in section 11.

The whole point of this is to avoid something like this inside the template itself:
{if $email_error eq ‘yes’}
{#email_resend_error#}
{/if}

Designers are notoriously allergic to code, it is my belief that the insert way is easier to handle by their own instead of having them do conditionals.

And that my friends wraps it up, the code solves all our requirements and the extra Smarty candy we have created will hopefully be helpful to a whole slew of people that cringe at the sight of the slightest bit of the underpinnings.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #3 on: February 27, 2010, 02:42:21 AM »

In this part we will take a look at what happens after a user logs on. We will also revisit quite a lot of code that has been changed.

First of all, part 4 has been updated with new information in a reply from me to a request. You might want to take a look at that.

Let’s begin by looking at the login function in UserController again:
Code:
function login($params = false){
    if($params['do'] == 'login'){
        $usr_info = $this->obj->fetchRowToArr(false, false, false, $params);
        if($usr_info != false){
            $this->globalsess->usr_row = $usr_info;
            $this->globalsess->usr_info = $usr_info->toArray();
            $result = $this->extraLoginCheck();
            if($result !== true)
                return $result;
            else{
                $this->setAclLevel($usr_info);
                $this->rdrParams["f"] = "desktop";
                $this->refresh($this->rdrParams);
            }
        }else
            $this->smarty->assign('val_err', 'yes');
    }
     
    return $this->getLoginForm();
}
So, upon successful login we set some member variable called $this->rdrParams[”f”] to “desktop” and then we call a new function called refresh. As you might suspect, the point of the refresh function is to reload the page, but why? Well, since we call all logic with the help of Smarty insert functions and return the results we have no way of affecting the rendering of other objects. In case of a successful login for instance, the menus above the main content area are already rendered, they can not magically change as a result of what happens in the login function. For instance, adding a Logout item to one of the menus is impossible if we do not refresh the whole page.

If we revisit the init function of the UserController:
Code:
function init(){
    parent::init();
    $this->name            = 'user';
    $this->obj             = Common::loadModel($this->name);
    $this->rdrParams    = array("index" => "d", "c" => $this->name);
    if(!empty($this->globalsess->usr_row))
        $this->globalsess->usr_row->setTable($this->obj);
    parent::finishInit();
}
There you have the rest of the rdrParams variable, apparently we are storing a default path to use in case we need to redraw the whole page from within the UserController.
Code:
function refresh($params = false){
    $location = "Location: {$this->base_url}";
    if(is_array($params)){
        foreach($params as $key => $value)
            $location .= "/$key/$value";
    }
    header($location);
    exit;
}
So with the information above we see that the URL we use to redraw the page with, will end with index/d/c/user/f/desktop:
Code:
function desktop($params = false){
    $this->chkSessRdr($params);
    return $this->fetch('desktop.tpl');
}
 
function chkSessRdr($params = false){
    if(!$this->chkSess())
        $this->refresh($params);
}
 
function chkSess($function = '', $params = ''){
    if(empty($this->globalsess->usr_info)){
        if(!empty($function))
            return $this->$function( $params );
        return false;
    }
    return true;
}
So when we call the desktop function after the login we will also call chkSessRdr() which will use chkSess() with the default values. In this case globalsess->usr_info will not be empty because it was set during the login. ChkSess() will therefore return true and no redirection will take place, we will simply return the contents of desktop.tpl:

<table>
    <tr>
        <td>
            {insert name="item" controller="Menu" id="usermenu"}
        </td>
        <td>
            {insert name="item" controller="Article" id="sitenews"}
        </td>
    </tr>
</table>

The menu above will now change based on settings set in the future admin module. At the moment we make do with phpMyAdmin. When the user is logged in we will now display Logout and Desktop links. If the user is logged out we display Register and Login links. From the MenuController:
Code:
function filterItems(&$menu_items, $have_session){
    $rarr = array();
    foreach($menu_items as &$item){
        if( ($have_session && $item['sess_show']) || (!$have_session && $item['nosess_show']) )
            $rarr[] = $item;
    }
    return $rarr;
}
 
function render($params){
    $menu = $this->obj->fetchRowToArr($params['id'], false, 'menu_slug');
 
    $have_session = empty($this->globalsess->usr_info) ? false : true;
    $menu_items = $menu->findDependentRowset('menuitem', 'db_menu')->toArray();
    $menu_items = $this->filterItems($menu_items, $have_session);
    Common::sort2DAsc($menu_items, 'position');
     
    $this->smarty->assign_by_ref('menu', $menu->toArray());
    $this->smarty->assign_by_ref('menuitems', $menu_items);
    return $this->fetch();
}

The desktop will have a usermenu and display some article with the slug “sitenews”. In my case the usermenu will only have two alternatives so far; edit profile, and view profile. Let’s take a look at what happens if our user chooses to edit her profile:
Code:
function update(){
    $this->smarty->assign_by_ref('prepop', $this->globalsess->usr_info);
    return $this->smarty->fetch('user_update_form.tpl');
}

We simply prepopulate a form with the current values and display it:

{config_load file="$config_file"}
{assign var="input_class" value=""}
{assign var="label_class" value=""}
{insert name="val" ctrl="user" field="password"     val="atleast-6"}
{insert name="val" ctrl="user" field="password2"     val="pass_again-password"}
{insert name="val" ctrl="user" field="avatar"         max_width="100" max_height="100" folder="upload"}
<form action="{$baseUrl}/index/d/c/user/f/saveUpdate" method="post" enctype="multipart/form-data">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
    <td class="{$label_class}">
        {#description#}:
    </td>
    <td colspan="2" class="{$input_class}">
        <textarea name="description">{$prepop.description}</textarea>
    </td>
</tr>
<tr>
    <td class="{$label_class}">
        {#avatar#}:
    </td>
    <td class="{$input_class}">
        <input type="file" name="avatar" class="{$input_class}"/>
    </td>
    <td>
        {$val_error.avatar}
    </td>
</tr>
.
.
.

Currently the form has four fields, the about me description, small avatar upload and password one and two.

New stuff:
{insert name=”val” ctrl=”user” field=”avatar” max_width=”100″ max_height=”100″ folder=”upload”}

Let’s revisit insertVal() in UserController:
Code:
function insertVal($params){
    if(!empty($params['val'])){
        $validation = $this->validationToArr($params['val']);
        $this->session->fields[$params['field']]['validation'] = $validation;
    }
     
    if(!empty($params['folder'])){
         
        if(empty($params['max_width']) || empty($params['max_height'])){
            echo "Error: An image needs 'max_width' and 'max_height' in pixels.";
            exit;
        }else if(empty($params['folder'])){
            echo "Error: An image needs a 'folder' to upload to.";
            exit;
        }
             
        $this->session->images[$params['field']]['max_width']     = $params['max_width'];
        $this->session->images[$params['field']]['max_height']     = $params['max_height'];
        $this->session->images[$params['field']]['folder']         = $params['folder'];
    }
}
So the new information gets saved in a session variable called “images”. Note the pluralism here, I’m already preparing for the future gallery extension with multiple image uploads. Onwards to saveUpdate() and Co:
Code:
function saveUpdate($params = false){
    $post = $this->_request->getParams();
    $result = $this->validatePost($post, $this->session->fields, "insert");
    if($result === true){
        $images = $this->upload($this->session->images);
        if(!empty($images['avatar'])){
            if(!empty($this->globalsess->usr_row->avatar))
                unlink($this->globalsess->usr_row->avatar);
            $this->globalsess->usr_row->avatar = $images['avatar'];
        }
        $this->globalsess->usr_row->setFromArray( $this->intersectPost($post) );
        $this->globalsess->usr_info = $this->globalsess->usr_row->toArray();
        $this->globalsess->usr_row->save();
        return $this->view();
    }else{
        return $this->validationError('update', $post, $result);
    }
}
 
function intersectPost(&$post){
    return array_intersect_key($post, $this->globalsess->usr_info);
}
 
function viewProfile(){
    $this->smarty->assign_by_ref('user', $this->globalsess->usr_info);
    return $this->fetch('user_profile.tpl');
}
To get the above logic to work We’ve had to add the following In UserController init() and In the index.php boostrap file:
Code:
if(!empty($this->globalsess->usr_row))
    $this->globalsess->usr_row->setTable($this->obj);
 
Zend_Loader::loadClass('Zend_Db_Table_Row');
Zend_Loader::loadClass('Zend_Db_Table_Row_Abstract');
Zend_Loader::loadClass('Zend_Db_Table_Rowset');
Zend_Loader::loadClass('Zend_Db_Table_Rowset_Abstract');
The above lets us load the user information into a row object with the help of session data, no database fetching in order to re-activate the object is required. This functionality is covered thoroughly in the ZF manual. The reason we keep the user information in a row object in addition to an array is convenience, the row object has a few nifty functions in addition to the save function that could come in handy. Having the information duplicated in an array will let us have easy access to it in other modules where we probably just need to read values. Actually activating the user row object there would be overkill. But damit, saveUpdate() could really benefit from an alias for $this->globalsess->usr_row…

Let’s upload that avatar:

Code:
function upload($image_fields){
    $insert_arr = array();
    foreach($image_fields as $key => $value){
        if($_FILES[$key]['name'] != ''){
            $insert_arr[$key] = $value['folder']."/".uniqid().".jpg";
            $move_result = move_uploaded_file($_FILES[$key]['tmp_name'], $insert_arr[$key]);
            if($move_result){
                $result = Common::reScaleImage($insert_arr[$key], $value['max_width'], $value['max_height']);
                if(!$result){
                    unlink($insert_arr[$key]);
                    $insert_arr[$key] = '';
                }else
                    chmod($insert_arr[$key], 0777);
            }else
                $insert_arr[$key] = '';
        }
    }
    return $insert_arr;
}

We try and move each image in the array and if it works we rescale them according to what was assigned above the update form in the template, by some designer or project manager probably. 100×100 if I remember correctly:
Code:
static function reScaleImage($filename, $conx, $cony){
    if(empty($conx) || empty($cony))
        return;
    list($new_width, $new_height) = self::constrainImage($filename, $conx, $cony);
    return self::resizeDiscardAspect($filename, $new_width, $new_height);
}
Resize and discard aspect, that sounds a little bit crude? Sure but we keep it through constrainImage():

Code:
static function constrainImage($filename, $conx, $cony){
    list($orig_width, $orig_height, $type) = getimagesize($filename);
     
    $new_width    =    0;
    $new_height    =    0;
     
    // for instance 0.66 = 125 / 188
    $con_ratio         = $conx / $cony;
     
    // for instance  1.33 =  300 / 225
    $orig_ratio        = $orig_width / $orig_height;
     
    //if the new picture is laying and the original is standing or laying
    //"less", the original height has to lead
    if($con_ratio > $orig_ratio){
        $new_height = $cony;
        $new_width     = round(( $cony * $orig_width) / $orig_height);
    }else if($con_ratio < $orig_ratio){
        $new_height = round(( $conx * $orig_height) / $orig_width);
        $new_width     = $conx;
    }
     
    return array($new_width, $new_height);
}

And finally the resize function which is just a copy of the GD2 section in the PHP manual:
Code:
static function resizeDiscardAspect($fileName, $new_width, $new_height){
    //we retrieve the info from the current image
    list($orig_width, $orig_height, $type) = getimagesize($fileName);
    //we create a new image template
    $image_p = imagecreatetruecolor($new_width, $new_height);
    //we create a variable that will hold the new image
    $image = null;
    //only the three first of all the possible formats are supported, the original image is loaded if it is one of them
    switch($type){
        case 1: //GIF
            $image = imagecreatefromgif($fileName);
            break;
        case 2: //JPEG
            $image = imagecreatefromjpeg($fileName);
            break;
        case 3: //PNG
            $image = imagecreatefrompng($fileName);
            break;
        default:
            return false;
            break;
    }
    //we copy the resized image from the original into the new one and save the result as a jpeg   
    imagecopyresampled($image_p, $image, 0, 0, 0, 0, $new_width, $new_height, $orig_width, $orig_height);
    imagejpeg($image_p, $fileName, 100);
    return true;
}

SaveUpdate() shares some logic with save() nowadays:
Code:
function validationError($handler, &$post, &$result){
    $this->smarty->assign('prepop', $post);
    $this->smarty->assign('val_error', $result);
    return $this->$handler();
}

That’s the end of this update. In the next part we take a look at the gallery component, there is some really exiting stuff there, multiple uploads with a Shockwave generated with the Flex SDK for instance. Some neat jQuery stuff too.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #4 on: February 27, 2010, 02:42:38 AM »

This time we will take a look at the current state of the gallery section.

To part 5.

I’ve opted to use a Flex and Javascript combination for the image uploading which might make this section of the project interesting for other people than ZF/Smarty geeks. Due to that fact most of the markup and actionscript for this piece is a separate article called: Multiple uploads with jQuery and Flex or Flash.

The gallery is currently implemented with a simple file structure that looks like this:

site_url/gallery/user_id
site_url/gallery/user_id/thumbs

We will store the big pictures in the main gallery folder and the thumbnails in the thumbs folder. The database is not involved at all. However I did opt for storing configuration data in a table that looks like this at the moment:
Code:
CREATE TABLE `com_gallery` (
  `max_w` bigint(12) NOT NULL,
  `max_h` bigint(12) NOT NULL,
  `max_s` bigint(12) NOT NULL,
  `thumb_w` int(5) NOT NULL,
  `thumb_h` int(5) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `com_gallery` (`max_w`, `max_h`, `max_s`, `thumb_w`, `thumb_h`) VALUES (500, 500, 1048576, 100, 100);
It’s pretty self explanatory, max_s will store the maximum allowed size of an image in bytes. The reason I chose a database solution instead of a config file is that I’ve decided that only the core functionality belongs in the config file and the gallery is not a core component. Yes it’s Joomla style yet again.

Most of the logic is in our new GalleryController which currently is responsible for two main actions. It will generate a page used for uploading images to the gallery and a page to display the user’s gallery. Let’s take a look at the upload logic first:
Code:
class GalleryController extends ExtController{
     
    function init(){
        parent::init();
        $this->name = "gallery";
        $this->info = Common::loadModel('gallery')->fetchAll()->current();
        $this->setDirs($this->gs->usr_info['id']);
        parent::finishInit();
    }
     
    function setDirs($id){
        $this->dir = "gallery/$id/";
        $this->thumb_dir = $this->dir."thumbs/";
    }
     
    function upload($params){
        $this->chkSessRdr();
        $this->smarty->assign('max_size', $this->info->max_s);
        $conf = $this->getZendConfSection('paths');
        $base_path = "http://{$conf->site_url}{$this->base_url}/gallery";
        $this->smarty->assign('upload_script', "$base_path/save/PHPSESSID/{$_REQUEST['PHPSESSID']}");
        $this->smarty->assign('get_thumb_url', "$base_path/newThumbs/");
        $this->commonThumbs();
        return $this->fetch('gallery_upload.tpl');
    }
    .
    .
    .

function commonThumbs(){
  $thumbs = $this->getThumbs(); 
  $this->smarty->assign_by_ref("thumbs", $thumbs);
  $this->smarty->assign("thumb_dir", $this->thumb_dir);
   
}
 
function getThumbs(){
  if(is_dir($this->thumb_dir)){
    return Common::listDir($this->thumb_dir);;
  }else{
    return array();
  }
}
 
function newThumbsAction(){
  $this->commonThumbs();
  echo $this->fetch('gallery_thumbs.tpl');
  exit;
}
We start by loading the above mentioned configuration information in init(). Then we setup our current directories with the id of the user owning the session in this case. Since the last part I have created a convenience function to reduce code in the form of getZendConfSection():
Code:
function getZendConfSection($section){
  $registry     = Zend_Registry::getInstance();
  $config_file = $registry['config_file'];
  $conf = new Zend_Config_Ini($config_file, $section);
  return $conf;
}

Anyway, the above upload function is responsible for generating the markup that is covered by the Flex/Flash article mentioned in the beginning of this tutorial. As you can see newThumbsAction() is the Ajax function called in that markup.

Let’s move on to the function that the Shockwave is calling in order to upload each image:
Code:
function saveAction(){
   
  if(!is_dir($this->dir)){
    mkdir($this->dir);
    mkdir($this->thumb_dir);
    chmod($this->dir, 0777);
    chmod($this->thumb_dir, 0777);
  }
   
  $filename_only = uniqid().".jpg";
  $filename = $this->dir.$filename_only;
  $thumbname = $this->thumb_dir.$filename_only;
   
  $move_result = move_uploaded_file($_FILES["Filedata"]["tmp_name"], $filename);
  $result = Common::reScaleImage($filename, $this->info->max_w, $this->info->max_h);
   
  copy($filename, $thumbname);
  $result2 = Common::reScaleImage($thumbname, $this->info->thumb_w, $this->info->thumb_h);
   
  if(!$result){
    unlink($filename);
    unlink($thumbname);
  }else{
    chmod($filename, 0777);
    chmod($thumbname, 0777);
  }
   
  exit;
}

The chmodding at the top should be unnecessary since mkdir can be called like this: mkdir($this->dir, 0777). However, that would not work with our current Samba configuration we are using to access the Ubuntu server here. Don’t ask me why. After setting up this function I thought I was home free but oh so wrong I was. Note that I make use of session data in the form of the current user’s unique id when using $this->dir above.

No problem you might think since I append the session id to the url used to call saveAction() with. I did too, but I was wrong. It seems the Zend Framework won’t pick up on this without a little help. Why can nothing ever be simple? I was fortunate and quickly found Rob’s Flash upload in Symfony article before I got too panicked. Empowered by the basic aha feeling I got from that piece I was able to quickly hack ZF in a similar way:
Code:
class ExtSession extends Zend_Session{
    public static function start($options = false){
    if(strpos($_SERVER['REQUEST_URI'],'PHPSESSID') !== false){
      $tok = strtok($_SERVER['REQUEST_URI'], "/");
      while($tok !== false){
        if($tok == 'PHPSESSID'){
          session_id( strtok("/") );
          break;
        }
        $tok = strtok("/");
      }
    }
    parent::start($options);
  }
}

We simply check our URL for PHPSESSID, and if we find it we start the session with that id instead. Calling my extended session instead of the original Zend Session in the bootstrap file solved the problem.

Let’s move on to the viewing logic:
Code:
function view($params){
  $this->chkSessRdr();
  if(!empty($params['id']))
    $this->setDirs($params['id']);
  $this->smarty->assign("gallery_dir", $this->dir);
  $this->commonThumbs();
  return $this->fetch('gallery_view.tpl');
}

If we explicitly pass an id to this function we will use that id instead of currently logged in user’s.

The markup:
Code:
{config_load file="$config_file"}
<script src="{$baseUrl}/js/jquery.js" language="JavaScript"></script>
<script src="{$baseUrl}/js/scrollTo.js" language="JavaScript"></script>
 
<script type="text/javascript">
      // <![CDATA[
      var galleryDir     = "{$baseUrl}"+"/{$gallery_dir}";
      {literal}
      function showMainPic(pic){
          var inner_html = '<img src="'+galleryDir+pic+'"/>';
                $("#mainPic").html(inner_html);
                $.scrollTo("#mainPic", {speed:500});
      }
      {/literal}
      // ]]>
</script>
 
<div id="thumbs">
    {if !empty($thumbs)}
        {foreach from=$thumbs item=thumb}
            <a href="javascript:showMainPic('{$thumb}')">
                <img class="gallery_thumb" src="{$baseUrl}/{$thumb_dir}{$thumb}"/>
            </a>
        {/foreach}
    {else}
        {#no_pics#}
    {/if}
</div>
<div id="mainPic"></div>

.gallery_thumb{}
 
.gallery_thumb:hover{
  filter:alpha(opacity=70);
-moz-opacity: 0.4;
}

I use the jQuery scrollTo plugin yet again. The reason being that if a user loads a vertically big image we will scroll down automatically so that the image is shown completely. Being able to automatically scroll the main window this easily is awesome because it’s one of the most helpful usability features one can implement. No one likes to scroll so when we have content that the user obviously want to access and there might be some scrolling involved it makes no sense to not implement automatic scrolling. And when we have ease in and out like we have with the scrollTo plugin we also avoid the jarring and confusing feeling that it is possible to get with instantaneous jumps.

That was it for this time, next time we will take a look at the blogging component which is the second major requirement we have for the community part of this project.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #5 on: February 27, 2010, 02:42:59 AM »

Since XOOPS didn’t do it for me when it comes to this project I have started with the blog component. As it turns out, a blog is more complicated than you would think. Therefore this part will not cover the whole component, only as far as I’ve gotten at the moment. Covering the whole process in one piece would make it a very long piece indeed.

To part 6

I’ve also separated the template stuff into it’s own piece as there might be people interested in some of the stuff there but not in the PHP back end. Check it out:

Validation and AJAX with jQuery

You could have that article in a new tab and switch tabs to get the whole picture.

Let’s move on, first the SQL:
Code:
CREATE TABLE `com_blog` (
  `id` bigint(12) NOT NULL auto_increment,
  `article` text NOT NULL,
  `post_date` bigint(15) NOT NULL,
  `headline` varchar(250) NOT NULL,
  `user_id` bigint(12) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
CREATE TABLE `com_blog_categories` (
  `id` bigint(12) NOT NULL auto_increment,
  `user_id` bigint(12) NOT NULL,
  `headline` varchar(100) NOT NULL,
  `description` varchar(250) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
CREATE TABLE `com_blog_connect` (
  `id` bigint(15) NOT NULL auto_increment,
  `article_id` bigint(12) NOT NULL,
  `category_id` bigint(12) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`,
 `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`,
  `sess_show`) VALUES (24, 4, '', 'index/d/c/blog/f/writeArticle', '', '', 'Write
   Article', 4, '', 0, 1);
 
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`,
 `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`,
  `sess_show`) VALUES (25, 4, '', 'index/d/c/blog/f/view', '', '', 'View Blog', 5,
   '', 0, 1);
The first table is the main table that will hold all the posts, the category table will hold all the categories of course. The connection table will link a certain amount of categories to a single article. If we hadn’t had the user id in the categories table I believe we would have had to call it tags instead.

The inserts at the end are two new menu items that provide the current blog functionality. Let’s examine the links, first we have a controller called blog, and a function called writeArticle, let’s go there:
Code:
class BlogController extends ExtController{
     
    function init(){
        parent::init();
        $this->name = "blog";
        $this->obj     = $this->loadModel($this->name);
        $this->usrid = $this->gs->usr_info['id'];
        parent::finishInit();
    }
     
    function writeArticle($params){
        $this->assignCategories();
        return $this->fetch('blog_write.tpl');
    }
         
    function assignCategories(){
        $categories = $this->loadModel('blogcategories')->fetchToArr($this->usrid, 'user_id');
        $this->smarty->assign('categories', $categories);
    }
.
.
.

We simply fetch this user’s categories, assign them to Smarty and return the result. The result will contain the markup where the user can write a new article. After an article is written it has to be saved:
Code:
function saveArticle(){
   
  $this->chkSessRdr();
  $post             = $this->_request->getParams();
  $post['post_date']     = time();
  $post['user_id']      = $this->usrid;
  $article_id             = $this->obj->insert($post);
  $con_obj                = $this->loadModel('blogcatcon');
  $cat_ids                 = explode(" ", trim($post['category_selections']));
   
  foreach($cat_ids as $cat_id){
    if(!empty($cat_id)){
      $insert_arr = array("article_id" => $article_id, "category_id" => $cat_id);
      $con_obj->insert($insert_arr);
    }
  }
   
  return $this->view();
}
As you can see I don’t filter the $post before I insert it, there is a little bit of convention over configuration magic going on here. I make sure I name input elements after their database field names, that way I can use an insert function that looks like this in ExtModel:
Code:
function insert(&$insert_arr){
  $clean_arr = array_intersect_key($insert_arr, array_combine($this->_cols, $this->_cols));
  return parent::insert($clean_arr);
}
It will simply filter out all the stuff that is not a field in the database table. I can’t remember if I’ve discussed this function before, if that is the case then well, a little bit of repetition never hurt anyone.

Let’s return to saveArticle(), notice how category ids is a space delimited string. We have some Javascript in the markup that updates a hidden field so we can get more than just one category for each article. We proceed with looping through them and inserting them one by one. Since we are trimming the string before we explode it I don’t think we really need to test for empty but a little bit of redundant safety never hurt anyone either, or maybe it did?

There is also an AJAX call that can be made from the interface to create new categories on the fly, it will call saveCategoryAction():
Code:
function saveCategoryAction(){
  $headline = $this->_request->getParam("headline");
  if(!empty($headline)){
    $insert_arr = array("headline" => $headline, "user_id" => $this->usrid);
    $new_id = $this->loadModel('blogcategories')->insert($insert_arr);
    $new_cat = array("headline" => $headline, "id" => $new_id);
    $this->smarty->assign('category', $new_cat);
    echo $this->fetch('blog_category.tpl');
  }
  exit;
}

What we have in the markup is a text field and a button, when the button is pressed whatever is entered in the text field will end up in our headline parameter. If we have something that is not empty we will create a new category with it and use the returned id to display this new category.

Back to saving the article, we finish off by returning view():
Code:
function view($params = array(), $articles = array()){
   
  $id = $this->getUsrId($params);
   
  if(empty($articles))
    $articles = $this->obj->fetchAll("user_id = $id", "post_date DESC", 10, 0)->toArray();
     
  $categories = $this->loadModel('blogcategories')->fetchAll("user_id = $id")->toArray();
  $this->smarty->assign(array("articles" => $articles, "categories" => $categories, "id" => $id));
  return $this->fetch('blog_view.tpl');
   
}
 
function getUsrId($params){
  $id = empty($params['user_id']) ? $this->usrid : $params['user_id'];
  return $id;
}

First we make sure we have some kind of id to use when fetching articles, someone else’s or the currently logged in user’s. Anyway, we fetch the ten newest articles plus all the categories created by the user in question and return blog_view.tpl:
Code:
{config_load file="$config_file"}
<table>
    <tr>
        <td width="300px">
            {foreach from=$articles item=article}
                <div>
                    {$article.headline}<br/>
                    {$article.post_date|date_format}
                </div>
                <br/>
                <div>
                    {$article.article}
                </div>
            {/foreach}
        </td>
        <td>
            <ul>
            {foreach from=$categories item=category}
                <li>
                    <a href="{$baseUrl}/index/d/c/blog/f/getArticlesByCat/cat_id/{$category.id}">{$category.headline}</a>
                </li>
            {/foreach}
            </ul>
        </td>
    </tr>
</table>
Not much to explain here, we use the default Smarty date format. In the future this has to be set in some configuration table that can be changed in a future admin back end. When a user clicks a category link all articles in that category will be displayed:
Code:
function getArticlesByCat($params){
  $cur_cat = $this->loadModel('blogcategories')->fetchAll("id = {$params['cat_id']}")->current();
  $articles = $cur_cat->findManyToManyRowset('blog', 'blogcatcon')->toArray();
  return $this->view($params, $articles);
}
Here we use findManyToManyRowset() which brings us to the models:

blog.php:

Code:
class Blog extends ExtModel{
    protected $_name             = 'com_blog';
    protected $_primary     = 'id';
}

blogcategories.php:
Code:
class Blogcategories extends ExtModel{
    protected $_name             = 'com_blog_categories';
    protected $_primary     = 'id';
}

blogcatcon.php:
Code:
class Blogcatcon extends ExtModel{
    protected $_name             = 'com_blog_connect';
    protected $_primary     = 'id';
     
    protected $_referenceMap    = array(
           'category' => array(
                 'columns'           => 'category_id',
                 'refTableClass'     => 'Blogcategories',
                 'refColumns'        => 'id'
           ),
       'article' => array(
        'columns'           => 'article_id',
        'refTableClass'     => 'Blog',
        'refColumns'        => 'id'
           )
    );
}

The reference map is the configuration that makes findManyToManyRowset() work. This is fairly well documented in the ZF manual so I will not elaborate. That’s it for now, the next time we will continue with implementing viewing other people’s blogs, updating articles and more.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #6 on: February 27, 2010, 02:43:24 AM »

The blog saga continues, we still don’t have any fancy Wordpress style filtering of the content. You know, creating these nice looking quotes and filtering potentially nasty html and stuff. Sure enough, TinyMCE has some function for allowing only certain tags and discarding others but I haven’t had time to look at that yet, I will though… Fancy filtering is not in the requirements for this project either however. But changing and deleting blog posts are necessary actions though, so is some kind of search for articles. Let’s begin by looking at what’s been added to the desktop menu:
Code:
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`, `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`, `sess_show`) VALUES (26, 4, '', 'index/d/c/blog/f/administrate', '', '', 'Administrate Blog', 6, '', 0, 1);
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`, `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`, `sess_show`) VALUES (27, 4, '', 'index/d/c/blog/f/getSearchForm', '', '', 'Search for articles', 7, '', 0, 1);

So we’ve got two entry points to the new stuff. Administrate() and getSearchForm(). Let’s begin with the administration part:
Code:
function administrate(){
  $articles = $this->getArticles($this->usrid)->toArray();
  foreach($articles as &$article)
    $article['categories'] = $this->getCatsByArticle($article['id'])->toArray();
  $this->assign_by_ref('articles', $articles);
  return $this->fetch('blog_administrate.tpl');
}
 
function getArticles($id = null, $count = null, $offset = null, $where = "user_id = ?", $order = "post_date DESC"){
  $where = !empty($where) ? $this->obj->getAdapter()->quoteInto($where, $id) : null;
  return $this->obj->fetchAll($where, $order, $count, $offset);
}
 
function getCatsByArticle($article_id = false){
  if($article_id)
    return $this->obj->fetchRow("id = $article_id")->findManyToManyRowset('blogcategories', 'blogcatcon');
  return array();
}

The getArticles function is basically just an alias with some probable default values set. In this case it will retrieve all articles of the logged in user. Next we append all connected categories to each article, the number of round trips to the database could become huge here which is not optimal of course. However, focus is on speed at the moment, we have to get finished sometime and we don’t want to get bogged down by creating some massive optimized SQL statement that will fetch the right stuff from the beginning. Besides, SQL is not my strong side, I can hack most stuff but I’m not lightning fast, going with this possibly temporary PHP solution will do for now and is much faster. Furthermore, when the service is released it won’t get a bazillion users straight away either, performance can always be fine tuned later if need be.

You might also notice that we have a new alias, actually two:
Code:
function assign($key, $value = false){
  if(is_array($key))
    $this->smarty->assign($key);
  else
    $this->smarty->assign($key, $value);
}
 
function assign_by_ref($key, $value = false){
  if(is_array($key))
    $this->smarty->assign_by_ref($key);
  else
    $this->smarty->assign_by_ref($key, $value);
}

K, let’s look at the template blog_administrate.tpl:
Code:
{config_load file="$config_file"}
<table>
    <tr>
        <td>{#article_headline#}</td>
        <td>{#categories#}</td>
        <td>{#post_date#}</td>
        <td>{#actions#}</td>
    </tr>
    {foreach from=$articles item=article}
    <tr>
        <td width="300px">
            {$article.headline}
        </td>
        <td>
            {foreach from=$article.categories item=category}
                {$category.headline},&nbsp;
            {/foreach}
        </td>
        <td>
            {$article.post_date|date_format:$date_format}
        </td>
        <td>
            <a href="{$baseUrl}/index/d/c/blog/f/updateArticle/id/{$article.id}">{#edit#}</a>
            <a href="{$baseUrl}/index/d/c/blog/f/viewArticle/id/{$article.id}">{#view#}</a>
            <a href="{$baseUrl}/index/d/c/blog/f/deleteArticle/id/{$article.id}">{#delete#}</a>
        </td>
    </tr>
    {/foreach}
</table>

So, we’ve got three new functions here, updateArticle, viewArticle and deleteArticle:
Code:
function updateArticle($params){
  if(!empty($params['id'])){
    $entry = $this->obj->fetchRow("id = {$params['id']}");
    $this->assign('article', $entry->toArray());
    $this->assignCategories($params['id']);
    $this->assign('update', 'yes');
    return $this->fetch('blog_write.tpl');
  }else
    return "We really need an id if we are to update something";
}
 
function viewArticle($params){
  $article                 = $this->obj->fetchRow("id = {$params['id']}")->toArray();
  $article['categories']     = $this->getCatsByArticle($article['id'])->toArray();
  $this->assign('article', $article);
  return $this->fetch('blog_view_article.tpl');
}
 
function deleteArticle($params){
  $article = $this->obj->fetchRow("id = {$params['id']}");
  $this->deleteConsByArticle($article->id);
  $article->delete();
  return $this->administrate();
}

Apparently updateArticle will launch the blog_write.tpl with a flag called ‘update’ set to ‘yes’. Of course when saving there the good old saveArticle function will be called. In that function we now do one extra thing, in addition to the user id we also keep track of the user name in each article. Why we do this will become apparent soon enough.

Let’s move on to assignCategories() and deleteConsByArticle():
Code:
function assignCategories($article_id = false){
  $categories     = $this->cat_obj->fetchAll("user_id = {$this->usrid}")->toArrayWithKey('id');
  $cur_cats         = $this->getCatsByArticle($article_id);
  foreach($cur_cats as $cur_cat)
    $categories[ $cur_cat->id ]['checked'] = 'checked = "checked"';
  $this->assign('categories', $categories);
}
 
function deleteConsByArticle($article_id){
  $this->con_obj->delete("article_id = $article_id");
}
The delete connection function is just an alias. Why don’t I use the $_referenceMap with a cascade delete like it says in the ZF manual, or InnoDB with this set in the schema?

1.) I tried my ass off with the reference map but I couldn’t get it to work, I must have missed something, beats me what I missed though because this isn’t supposed to be complicated at all. Let me know if you’ve made it work and how you did it.

2.) InnoDB has wreaked havoc with my data on several occasions by crashing miserably, I don’t know why. It might have been my fault or something but it doesn’t really matter because I’ve never had any problems with MyISAM, that’s why I’m prepared to go through the extra trouble of explicitly deleting dependencies in this way.

You might notice something strange in assignCategories, it seems that the rowset has got a new function called toArrayWithKey. It has and it is my doing, instead of making yet another static function I opted to put the functionality in an extension of the rowset class this time because it’s somewhat prettier:
Code:
class ExtRowset extends Zend_Db_Table_Rowset_Abstract{
    function toArrayWithKey($primary = false){
        if(!$primary) return false;
      $rarr = array();
        $rowArr = $this->toArray();
        foreach($rowArr as $row){
            $key = $row[ $primary ];
            $rarr[ $key ] = $row;
        }
        return $rarr;
    }
     
    function toKeyValueArray($key_field, $value_field){
        $rarr = array();
        $rowArr = $this->toArray();
        foreach($rowArr as $row)
            $rarr[ $row[$key_field] ] = $row[ $value_field ];
        return $rarr;
    }
}
ToArrayWithKey will convert the rowset to a 2d array and assign each sub array a key that matches the id of each entry. The whole point of this is to be able to easily keep track of checked / not checked categories in the template. Take a look at assignCategories again and you will understand what I mean. This functionality is only needed if we allow updating of articles, which we currently do. One more thing, we have to use the new ExtRowset class in ExtModel like this at the top: protected $_rowsetClass = ‘ExtRowset’; Don’t forget to include the new rowset script in the bootstrap file either.

Let’s retrace our steps to the beginning and the new menu item that links to getSearchForm():
Code:
function getSearchForm(){
  return $this->fetch('blog_search.tpl');
}

And the template:
Code:
{config_load file="$config_file"}
<form id="search_category_form" action="{$baseUrl}/index/d/c/blog/f/searchCategory" method="post">
    {#search_categories#}:
    <br/>
    <input type="text" style="width:250px;" id="search_field" name="search_field">
    <br/>
    <input type="submit" value="{#submit#}">
</form>
<br/>
<div>
    <a href="{$baseUrl}/index/d/c/blog/f/getNewArticles">
        {#view_new_posts#}
    </a>
</div>

Let’s do searchCategory first:
Code:
function searchCategory($params){
  $search_params = array(
    "count"     => $this->session->conf->cat_count, 
    "where"     => array("headline LIKE '%{$params['search_field']}%'", "id IN ( SELECT category_id FROM com_blog_connect )"),
    "obj"         => $this->cat_obj,
    "offset"    => $params['offset']
  );
  $categories = $this->sumCategories( $this->getSearchResults($search_params) );
   
  if(empty($params['offset'])){
    $search_params['count'] = null;
    $temp = $this->getSearchResults($search_params);
    $this->session->category->tot_count = count($temp);
  }
   
  $this->assignPaginationArray($this->session->category->tot_count, $this->session->conf->cat_count, $params['offset']);
  $this->assign('categories', $categories);
  return $this->fetch('blog_cat_search_result.tpl');
}
Yeah, I know, it’s a biggie but it has it’s reasons, in probably 90% of all cases it would be smaller and easier. So we begin by creating an array that we will use with the necessary information like “count” which is how many results we want to display per page, “where” which is yet another array with what we want to search for, “obj” is the object we want to use in the search, leaving this empty will default to $this->obj, last but not least the “offset” which will control where we are in the pagination.

First of all, let’s start with $this->session->conf->cat_count. Since the last part I’ve mustered the strength to actually start putting stuff in a configuration table which we will later be able to change in the future admin interface:
Code:
CREATE TABLE `com_blog_config` (
  `id` int(5) NOT NULL auto_increment,
  `conf_var` varchar(100) NOT NULL,
  `conf_val` varchar(100) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (1, 'cat_count', '2');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (2, 'date_format', '%b %e, %Y');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (3, 'articles_per_page', '10');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (4, 'new_articles_count', '50');
Cat_count is 2 at the moment, it will probably be 10 or 15 in the end but I’ve set it to 2 now because that makes it easier for me to test pagination logic with low amounts of data.

This information is fetched in init() which we should revisit:
Code:
function init(){
  parent::init();
  $this->name     = "blog";
  $this->obj         = $this->loadModel($this->name);
  $this->cat_obj    = $this->loadModel('blogcategories');
  $this->con_obj  = $this->loadModel('blogcatcon');
  $this->usrid     = $this->gs->usr_info['id'];
  parent::finishInit();
  $this->chkSessRdr();
  parent::setConfig('blogconfig');
  $this->assign('date_format', $this->session->conf->date_format);
}
 
In ExtController:
function setConfig($table){
  if(!isset($this->session->conf))
    $this->session->conf = (object)$this->loadModel($table)->fetchAllKeyValue();
}
 
In ExtModel:
function fetchAllKeyValue(){
  return $this->fetchAll()->toKeyValueArray($this->_key_field, $this->_value_field);
}
 
In blogconfig.php:
class Blogconfig extends ExtModel{
    protected $_name             = 'com_blog_config';
    protected $_primary         = 'id';
    protected $_key_field        = 'conf_var';
    protected $_value_field        = 'conf_val';
}

Here we use the other function we have extended rowset with to fetch the config info as an array that has the field conf_var as it’s keys and conf_val as values. Notice the explicit type conversion from array to object, it’s easier to write ->var instead of [’var’], at least in my book.

The next function is getSearchResults:
Code:
function getSearchResults($params){
  extract($params);
  $obj = empty($obj) ? $this->obj : $obj;
  return $obj->fetchAll($where, $order_by, $count, $offset)->toArray();
}

It’s basically just an alias which I’ve created to avoid a fetchAll function with an ugly list of long strings as parameters. We need the params array in more places and this is also a good way of encapsulating the dangerous but powerful extract function. I know, it’s a borderline example of trying to minimize code size at the expense of readability but we go with it for now. Let’s look at sumCategories:
Code:
function sumCategories($categories){
  $final_cats = array();
  foreach($categories as $cat)
    $final_cats[ $cat['headline'] ] += $this->con_obj->fetchAll("category_id = {$cat['id']}")->count();
  arsort($final_cats);
  return $final_cats;
}
What’s happening here is that there might exist categories with the same headline created and attached to different users. When doing the search by category we want the number of articles in each category irrespective of which user has written something in that category. In this case we basically treat the categories as tags by their headlines even though there is a connection to a unique user for each category. Yes it’s complicated and convoluted, but if we had had a tag system then the complexity would have shown it’s ugly face when retrieving “categories” in the write/update template instead. Having a twofold solution like we maybe should have, would’ve added extra code and complexity in it’s own way. It’s always easy to create complexity but sometimes it’s impossible to avoid it. Note also that we are doing a looping retrieve from the database again, not exactly optimized.
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #7 on: February 27, 2010, 02:43:53 AM »

The if(empty($params[’offset’])) conditional is necessary because we need to set $this->session->category->tot_count to a proper value when the search is executed, but only once.

AssignPaginationArray:
Code:
function assignPaginationArray($tot_count, $count, $selected){
  $selected = empty($selected) ? 0 : $selected;
  $arr = array();
  $display = 1;
  for($i = 0; $i < $tot_count; $i += $count){
    $temp = array();
    $temp['offset'] = $i;
    $temp['display'] = $display;
    if($i == $selected)
      $temp['selected'] = "selected";
    $display++;
    $arr[] = $temp;
  }
  $this->assign('pagination_arr', $arr);
}
Nothing special here, it’s just creating the array we need to draw the pagination controls. As you can see there is no grouping of pages being done at this stage. That will be added later if needed. In this case we have 2 results per page, a 1000 item result will actually draw 500 page links numbered from 1 to 500. We will see what happens, we will probably have to implement some grouping eventually but at the moment we need something to show asap and this works. Whatever the managers are calling it, “proof of concept” or a “testable prototype”, we all know what they mean, a dirty first iteration that they can play around with.

I think we are finally ready to take a look at blog_cat_search_result.tpl:
Code:
{foreach from=$categories key=cat item=article_num}
    <div>
        <a href="{$baseUrl}/index/d/c/blog/f/getArticlesByCatName/cat_name/{$cat}">{$cat} ({$article_num})</a>
    </div>
{/foreach}
<br/>
{include file="paginate.tpl" f="searchCategory" c="blog"}
And paginate.tpl:
Code:
{foreach from=$pagination_arr item=page}
    <a href="{$baseUrl}/index/d/c/{$c}/f/{$f}/offset/{$page.offset}">
        {if $page.selected eq "selected"}
            >>{$page.display}<<
        {else}
            {$page.display}
        {/if}
    </a>
    &nbsp;
{/foreach}
Nothing much to comment on here, as you can see the pagination template can be dropped as a widget anywhere where the required back end logic is in place, let’s move on to getArticlesByCatName:
Code:
function getArticlesByCatName($params){
  $categories = $this->cat_obj->fetchAll("headline = '{$params['cat_name']}'");
  $articles = array();
  if($categories){
    foreach($categories as $cat)
      $articles += $cat->findManyToManyRowset('blog', 'blogcatcon')->toArray();
  }
  $this->assign('articles', $articles);
  return $this->fetch('blog_view_articles.tpl');
}
Yet again a loop that queries the database… blog_view_articles.tpl:
Code:
{foreach from=$articles item=article}
    <div>
        <div>
            <a href="{$baseUrl}/index/d/c/blog/f/viewArticle/id/{$article.id}">
                {$article.headline}
            </a>
            &nbsp;{#by#}&nbsp;
            <a href="{$baseUrl}/index/d/c/user/f/view/id/{$article.user_id}">{$article.username}</a>
            &nbsp;|&nbsp;{$article.post_date|date_format:$date_format}
        </div>
    </div>
{/foreach}
Here the new username field in com_blog makes sense, if it wasn’t already there we would have had to do yet another table join to retrieve it. Moreover, since articles will not be able to change authors this will not prove to be a problem. For once we have a change that makes sense from both an optimization and dev-speed point of view!

There is one more change of importance, during the process leading up to the above logic I thought it would be nice to integrate the gallery into the blog by enabling the gallery thumbs in blog_write.tpl. The goal was to have the thumbs show if the user wanted and if clicked inserted into TinyMCE. Let’s take a look:
Code:
.
.
.
tinyMCE.init({
    mode : "textareas",
});
     
{/literal}
// ]]>
</script>
{if $update eq "yes"}
    <form id="save_article_form" action="{$baseUrl}/index/d/c/blog/f/saveUpdatedArticle/id/{$article.id}/" method="post">
{else}
    <form id="save_article_form" action="{$baseUrl}/index/d/c/blog/f/saveArticle" method="post">
{/if}
<table>
    <tr>
        <td>
            {insert name="item" controller="Gallery" function="view" template="blog_gallery.tpl"}
            <label>{#article_headline#}:
                <br/>
                <input type="text" style="width:250px;" value="{$article.headline}" class="validator-required" id="headline" name="headline" title="{#article_headline#} - {#headline_err#}">
            </label>
            <br/>
.
.
.

Obviously some changes to the gallery controller view() has been made:

function view($params){
  $this->chkSessRdr();
  if(!empty($params['id']))
    $this->setDirs($params['id']);
  $this->assign("gallery_dir", $this->dir);
  $this->commonThumbs();
  if(empty($params['template']))
    return $this->fetch('gallery_view.tpl');
  else
    return $this->fetch($params['template']);
}

Not much new, we can override the default template now. Onwards to blog_gallery.tpl:

{if !empty($thumbs)}
{config_load file="$config_file"}
<script language="javascript" type="text/javascript">
    var galleryDir     = "{$baseUrl}"+"/{$gallery_dir}";
    {literal}
    function toggleViz(el_id){ 
        $("#"+el_id).toggleClass("hide_me"); 
    }
     
    function insertPic(pic){
        var image_html = '<img src="'+galleryDir+pic+'"/>';
        tinyMCE.setContent( tinyMCE.getContent('article') + image_html );
    }
 
    {/literal}
</script>
<a href="javascript:toggleViz('gallery')">{#toggle_gallery#}</a>
<br/>
<div id="gallery" class="hide_me">
    {#gallery_explain#}
    <br/>
    {foreach from=$thumbs item=thumb}
        <a href="javascript:insertPic('{$thumb}')">
            <img class="gallery_thumb" src="{$baseUrl}/{$thumb_dir}{$thumb}"/>
        </a>
    {/foreach}
</div>
{/if}
And the CSS:
Code:
.hide_me{
    width:0px;
    height:0px;
    visibility:hidden;
}

As you can see we can toggle the thumbs on and off, when a thumb gets clicked it will be inserted at the end of the content currently in TinyMCE. Yes I tried like a maniac to get it to work by being inserted at the place where the marker currently is in the editor but I couldn’t make it work. Probably because it loses focus when you click somewhere outside it and then it becomes impossible to read the marker position but I’m not 100% on this one. If you’ve discovered a solution then please please let me know. Of course it might be possible to create some kind of plugin to TinyMCE but with the state of my basic Javascript skills and the time pressures I’m under… Not likely at the momen
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #8 on: February 27, 2010, 02:44:18 AM »

This piece covers creating a proper folder structure so that we can have a skin system. I’ve also created a folder for the admin section which is fully contained with it’s own controllers folder and so on. Another unrelated change that has happened since the last part is sub menu logic which we will also take a look at.

To Part 8

folder_struct.jpg

Let’s begin with the bootstrap file, somehow we have to let ZF know that we want to use two controller directories:
Code:
.
.
.
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$controllerDirectories = array('default' => './controllers', 'administrator' => './administrator/controllers');
$frontController->setControllerDirectory($controllerDirectories)
                ->setRouter(new Zend_Controller_Router_Rewrite())
                ->registerPlugin(new ControllerPlugin());
.
.
.
The only new thing here is that we now have two controller directories, our default one we’ve used all the time and /administrator/controllers. Let’s take a look at administrator/controllers/IndexController.php:
Code:
class Administrator_IndexController extends ExtControllerAdmin{
     
    function init(){
        parent::init();
        $this->name            = 'index';
        $this->module        = 'administrator';
        parent::finishInit();
    }
     
    function indexAction(){
        $this->display();
    }
     
    function dAction(){
        $params = $this->_request->getParams();
        $params['m'] = 'administrator';
        $this->assign('params', $params);
        $this->display();
    }
     
    function frontPage(){
        return $this->fetch('front_page.tpl');
    }
     
    function render($params){
        return $this->frontPage();
    }
         
    function norouteAction(){
        $this->refresh();
        exit;
    }
}

There are a couple of new things to note here. First we have to have a prefix in the class name which corresponds to the key ‘administrator’ in our $controllerDirectories variable in the bootstrap file, hence the name Administrator_IndexController. As you see we have a new extension to ExtController called ExtControllerAdmin which looks like this at the moment:
Code:
class ExtControllerAdmin extends ExtController{
     
    function init(){
        parent::init();
    }
     
    function finishInit(){
        parent::finishInit();
        $this->smarty->prependDirs("administrator");
        $this->assign('designDir', "administrator");
    }
}
It seems our Smarty class has got some new additions:
Code:
class Smarty_Zend extends Smarty{
   
   function __construct(){
        parent::Smarty();
        $this->template_dir     = 'templates';
        $this->config_dir         = 'configs';
        $this->compile_dir         = 'templates_c';
        $this->cache_dir           = 'cache';
   }
   
   function setConfig($config = false){
           if(!empty($config) && is_array($config)){
               foreach($config as $key => $value)
                   $this->$key = $value;
           }
   }
   
   function prependDirs($value = ""){
       if(!empty($value)){
           $this->template_dir   = $value . '/' . $this->template_dir;
            $this->config_dir        = $value . '/' . $this->config_dir;
            $this->compile_dir    = $value . '/' . $this->compile_dir;
            $this->cache_dir       = $value . '/' . $this->cache_dir;
       }
   }
}
There is not much to say here, we’ve got two new functions that will enable us to change the paths on the fly whenever we want. In the above case we would have a prior value of ‘templates’ in the template_dir for instance, which after passing ‘administrator’ to the prependDirs function will have a value of administrator/templates, easy stuff.

Let’s continue examining Administrator_IndexController, in the init() function we’ve got $this->module = ‘administrator’;. The new module variable is used in several places in ExtController:
Code:
function finishInit(){
  $this->assign('controller', $this->name);
  $this->template        = $this->name.".tpl";
  $this->base_url     = $this->_request->getBaseUrl();
   
  if(!empty($this->module))
    $this->base_url .= "/".$this->module;
     
  $this->compl_path = "http://{$_SERVER['HTTP_HOST']}{$this->base_url}";
  $this->assign('baseUrl', $this->base_url);
  $this->assign('complUrl', $this->compl_path);
  $this->session = new Zend_Session_Namespace($this->name);
  $this->assign('resourceDir', "resources");
}
 
function loadModel($table){
  $class_name = strtolower($table);
  if(empty($this->module))
    $file_name = $class_name.".php";
  else
    $file_name = $this->module."/models/".$class_name.".php";
  include_once($file_name);
     
  return new $class_name;
}

We have to implement our new modular system in the base_url variable, we also have to account for it when we load models. On a side note, notice the new template variable we have called resourceDir. I’ve moved some menu related images from the image directory that the template design utilizes. Since menu pics are to be uploaded by admins for menus created on the fly we have two separate the folders. Think media manager in Joomla.

Back to Administrator_IndexController again, the next new thing is $params[’m'] = ‘administrator’; in dAction. This is a requirement to let our new modularity cascade through the logic of displaying various pieces. Let’s follow this to it’s conclusion by first taking a look at a section of administrator/templates/index/index.tpl:
Code:
.
.
.
<div id="main">
  <!-- begin: #col3 static column -->
  <div id="col3">
    <div id="col3_content" class="clearfix">
      <!-- skiplink anchor: Content -->
      {insert name="item" params=$params module="administrator"}
    </div>
    <div id="ie_clearing">&nbsp;</div>
    <!-- End: IE Column Clearing -->
  </div>
  <!-- end: #col3 -->
</div>
.
.
.
As you can see we pass our insert_item function a new key value pair of module => administrator. So we have to check out widgets.php to see what has happened:
Code:
function insert_item($params){
    if(empty($params['params'])){
        $function     = empty($params['function']) ? 'render' : $params['function'];
        $controller = empty($params['controller']) ? 'index' : $params['controller'];
        $module       = empty($params['module']) ? '' : $params['module'];
        return Common::loadController($controller, false, $module)->$function($params);
    }else{
        $function     = empty($params['params']['f']) ? 'render' : $params['params']['f'];
        $controller = empty($params['params']['c']) ? 'index' : $params['params']['c'];
        $module       = empty($params['params']['m']) ? '' : $params['params']['m'];
        return Common::loadController($controller, false, $module)->$function($params['params']);
    }
}
It makes sense, we have to keep track of the current module now, in addition to the controller and function. And as you can see the Common::loadController function takes a third argument now:
Code:
static function loadController($controller, $request = false, $module = ''){
  $controller = ucfirst($controller)."Controller";
  $module = ucfirst($module);
  $class_name = empty($module) ? $controller : $module."_".$controller;
  if(!class_exists($class_name)){
    $file_name = "controllers/".$class_name.".php";
    if(!empty($module))
      $file_name = "$module/$file_name";
    include_once($file_name);
  }
  return new $class_name($request);
}

In addition, both the display and fetch function in ExtController have been changed:
Code:
function display($template = false, $folder = false){
  $page = $this->fetch($template, $folder);
  echo $page;
  exit;
}
 
function fetch($template = false, $folder = false){
  if($template == false){
    $this->smarty->template_dir .= "/".$this->name;
    return utf8_encode($this->smarty->fetch($this->template));
  }else{
    if($folder !== null){
      if($folder == false)
        $this->smarty->template_dir .= "/".$this->name;
      else
        $this->smarty->template_dir .= "/".$folder;
    }
    return utf8_encode($this->smarty->fetch($template));
  }
}
We’re changing the smarty template directory to handle skinning now. To understand this completely we have to check out ExtControllerFront which all non admin controllers are an extension of nowadays:
Code:
class ExtControllerFront extends ExtController{
     
    function init(){
        parent::init();
    }
     
    function finishInit(){
        parent::finishInit();
        $this->setGlobalConfig('mainconfig');
        $this->config_file    = "{$this->gs->gc->front_language}_{$this->name}.conf";
        $this->assign('config_file', $this->config_file);
        $templ_dir = "templates/{$this->gs->gc->design}";
        $this->smarty->setConfig(array("template_dir" => $templ_dir));
        $this->assign('designDir', "{$this->base_url}/$templ_dir");
        $this->assign('resourceDir', "{$this->base_url}/resources");
    }
}

Apparently we change the smarty template dir when we initialize, to “templates/{$this->gs->gc->design}”. $this->gs->gc->design is set when we call $this->setGlobalConfig(’mainconfig’);:
Code:
function setGlobalConfig($table){
  if(!isset($this->gs->gc))
    $this->gs->gc = (object)$this->loadModel($table)->fetchAllKeyValue();
}
We basically load the whole main config table straight into an array. Changes to the config table since last time is a name change from language to language_front for the language field. However, the most important change is the ‘design’ field which has a value of ‘default’ at the moment. This setup reflects the folder structure where we currently have all our templates, it’s of course templates/default/*/*.tpl. Remember that $this->gs is a session variable, that is why we can check if it is set or not, it stays put between requests. A final note; you might be wondering about the designDir variable, that is just a copy of the template dir in the Smarty object, we use it to load css at the moment. Yes I have thrown in the css, YAML and images folder among the templates too now. It would be a little hard to implement self contained skins otherwise.

Let’s continue with the new submenu logic like I promised in the beginning. First of all the db_menuitems table has got a new field called submenu_id. If an item has a submenu_id that is not 0 we will treat it as being a headline which will hide/unhide a submenu. Let’s begin with menu.tpl:
Code:
<script src="{$baseUrl}/js/rollOverImage.js" language="JavaScript"></script>
<SCRIPT type="text/javascript" src="{$baseUrl}/js/common.js"></SCRIPT>
.
.
.
{elseif $menu.menu_type eq "vertical"}
    <ul class="{$menu.menu_class}" id="{$submenu_id}">
    {foreach from=$menuitems item=menu_item}
        {if empty($menu_item.item_class)}
            <li class="{$menu.menu_itemclass}">
        {else}
            <li class="{$menu_item.item_class}">
        {/if}
         
        {if empty($menu_item.item_imageover) == true and $menu_item.submenu_id == 0}
            <a href="{$baseUrl}/{$menu_item.item_link}">
        {elseif $menu_item.submenu_id == 0}
            <a href="{$baseUrl}/{$menu_item.item_link}" onmouseout="MM_swapImgRestore()" onmouseover="MM_swapImage('image-{$menu_item.id}','','{$resourceDir}/{$menu_item.item_imageover}',1)">
        {else}
            <a href="javascript:toggleViz('{$menu_item.submenu_id}')">
        {/if}
         
        {if empty($menu_item.item_image) == false}
            <img id="image-{$menu_item.id}" src="{$resourceDir}/{$menu_item.item_image}" alt="{$menu_item.image_alt}" />
        {/if}
        {if empty($menu_item.link_text) == false}
            <span>{$menu_item.link_text}</span>
        {/if}
        </a>
        {if $menu_item.submenu_id != 0}
            {insert name="item" controller="Menu" function="subMenu" id=$menu_item.submenu_id}
        {/if}
        </li>
    {/foreach}
    </ul>

The new common.js only contains toggleViz at the moment:
Code:
function toggleViz(el_id){ 
    $("#"+el_id).toggleClass("hide_me"); 
}
As you can see we’ve only implemented the new sub menu logic for vertical menus so far. Also notice the new resource directory in action when we retrieve images. Anyway, the most important thing is the new insert we perform if $menu_item.submenu_id is not 0. Let’s take a look at the target function in MenuController:
Code:
function subMenu($params){
  $menu = $this->obj->fetchRow("id = {$params['id']}");
  $this->assign('submenu_id', $params['id']);
  return $this->getMenu($menu);
}
 
function getMenu($menu){
  $menu_items = $menu->findDependentRowset('menuitem', 'db_menu')->toArray();
  $menu_items = $this->filterItems($menu_items);
  Common::sort2DAsc($menu_items, 'position');
  $this->assign_by_ref('menu', $menu->toArray());
  $this->assign_by_ref('menuitems', $menu_items);
  return $this->fetch();
}

If there is a menu item in this sub menu with a submenu_id that is not 0 we will go through this again and again to draw the whole menu tree. This is essentially a recursive process which is really the best way of handling arbitrary trees. However, don’t make menus with a depth of more than some 100-200, otherwise PHP will get grumpy on you Smiley
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #9 on: February 27, 2010, 02:44:38 AM »

As some of you know, I’ve got other things to do at the moment. The cms/community project is iced. Therefore I’ve created a Google project out of it.

ZF CMS/Community source

The source for the Flex/Flash uploader is here.

Some changes since the last part:
Code:
class ControllerPlugin extends Zend_Controller_Plugin_Abstract{
    public function preDispatch( Zend_Controller_Request_Abstract $request ){
        $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();
      $controllerName = $request->getControllerName();
      $moduleName    = $request->getModuleName();
      if (empty($controllerName))
        $controllerName = $dispatcher->getDefaultController();
             
      $className = $dispatcher->formatControllerName($controllerName);
         
        if($className){
      try{
                $controllerDirectories = $dispatcher->getControllerDirectory();
                if($moduleName == 'default'){
          Zend_Loader::loadClass($className, $controllerDirectories['default']);
                }else{
                    $dir = $controllerDirectories[ strtolower($moduleName) ]."/".$className.".php";
                    include_once($dir);
                    $className = ucfirst($moduleName)."_".$className;
                }
        $actionName = $request->getActionName();
 
        if (empty($actionName))
            $actionName = $dispatcher->getDefaultAction();
        $methodName = $dispatcher->formatActionName($actionName);
 
        $class = new ReflectionClass( $className );
 
        if( $class->hasMethod( $methodName ) )
            return;
        }catch (Zend_Exception $e){
        }
     }
    // we only arrive here if can't find controller or action
    $request->setControllerName( 'index' );
    $request->setActionName( 'noroute' );
    $request->setDispatched( false );
    }
}
The moste notable thing here is the if($moduleName… part. If the module name is not default then we basically tell ZF to take a hike and do it freestyle instead. This change was required in order to get the folder structure I want to work. With having the admin in it’s own self contained folder.

That means loadModel in ExtController could be made much simpler since we - with the above code - is moving the whole module logic upwards in the logical chain:
Code:
function loadModel($table){
  $class_name = strtolower($table);
  $file_name = $class_name.".php";
  include_once($file_name);
  return new $class_name;
}
If I had to do this thing all over again I might just have scrapped the whole routing logic and written my own much simpler version of the routing that would’ve been much more flexible. I feel that there has been too much fighting against the framework conventions in this area.

INSTALL:
- There should be an SQL file in there that you can import to get some test data going.
- Make sure mod_rewrite is turned on and working properly.
- Check what you have to do in the framework_config/config.ini file, I think you will get the picture.
- Make sure you link to ZF and Smarty properly, default is ../PEAR/Zend and ../PEAR/libs for Smarty.

TODO:
- Getting rid of repeat select statements for optimization.
- What happens if the user doesn’t have flash enabled when we have a shockwave size set to 0? Most likely the automatic download procedure will be broken so this needs fixing somehow. Perhaps with that new PHP module that will allow multiple uploads with progress feedback? If that module is working properly we can scrap the whole Shockwave uploader which would be great.
- Get the City/Country/Whatever drop downs out of the code and into their own tables, editable from the admin section of course.
- The whole admin interface where it should be possible to edit articles, users and menus to name a few things. At the moment the only thing that works is editing global config values.

If you are interested in maintaining and working on this thing comment here or email me or whatever and I’ll give you the Google code password so you can start using subversion.

Good luck!
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
cmxq
Administrator
Sr. Member
*****
Posts: 365

Thank You
-Given: 0
-Receive: 0


View Profile WWW
« Reply #10 on: February 27, 2010, 02:44:57 AM »

I’m in the process of replacing the horrible PHP code. And I’m using the beefed up version of the Zend Framework that I’ve built in this series of course! However, I’ve started to find some bugs and the fixes will be posted here.

First up is a problem with fetching several templates in the same controller, here is the fix in in ExtController:
Code:
function setSmartyTplDir($addition){
    $dir_arr = explode("/", $this->smarty->template_dir);
    if($addition != array_pop($dir_arr))
        $this->smarty->template_dir .= "/".$addition;
}
 
function fetch($template = false, $folder = false){
    if($template == false){
        $this->smarty->template_dir .= "/".$this->name;             
        return utf8_encode($this->smarty->fetch($this->template));
    }else{
        if($folder !== null){
            if($folder == false)
                $this->setSmartyTplDir($this->name);
            else
                $this->setSmartyTplDir($folder);
        }
        return $this->smarty->fetch($template);
    }
}

source code
Logged

http://www.eabeauty.net - The world of beauty and cute girls !!!
http://www.newgamesonline.net -  Free flash games online !!!
Pages: [1]
Print
Jump to:  

Powered by SMF 1.1.11 | SMF © 2006-2009, Simple Machines LLC
Note, by Smoky