Setting up multiple contexts in Modx for a multi language site

Modx Revolution now has a feature which allows you to set up sites using contexts which map to sub domains.

The documentation for setting these up is currently here.

Once I had followed those instructions, I had multiple domains, one for each language. In this case, the client wanted one for Russia, Middle East and Asia.

There was an additional twist to the language domains. They wanted a news and casestudy domain which would contain a central repository of well, news and casestudies. The idea was that each language domain could create symlink to the case study they wanted. This would give editors the power to pick and choose which case study they wanted to appear on the relevant language site. Should be simple, right?

Unfortunately not. At the moment, the creators of Modx have locked down cross context symlinking and the only way to get around it is to edit the source code. Here’s the offending code in /core/model/modx/modrequest.class.php at line 180.

if ($resource->get('context_key') !== $this->modx->context->get('key')) {

       if (!$this->modx->getCount('modContextResource', 
      array($this->modx->context->get('key'), $resourceId))) {

       return null;

    }

}

A quick amend to comment those lines of code out and voila, symlinking across domains works fine. It’s not great to hack the core of any system, if I’ve time, I might find a way to extend the Request class.

When the user is viewing a case study on the casestudies domain, the client wanted the left hand menu to reflect the language they had initially selected. The menu was generated by Wayfinder and I quickly found that wayfinder doesn’t work across contexts. There’s a snippet called getResources which does but it would mean some extra work to get it to the menu highlight. How to get Wayfinder to work across contexts? I wrapped the Wayfinder call in a snippet.


$desired_context = isset($desired_context) ? $desired_context : 'casestudies';

  $fullLink = isset($fullLink ) ? $fullLink : '0'; 

  global $modx;


$cs_config =array(

  'startId' => $startId,

  'level' => $level,

  'lastClass' => $lastClass,

  'outerTpl' => $outerTpl,

  'rowTpl' => $rowTpl,

  'fullLink' => $fullLink,

  'debug' => isset($debug) ? $debug : FALSE

  );


if($desired_context == $wcontext ) {

  return $modx->runSnippet('wayfinder',$cs_config);


}


//Switch to desired context

  $modx->switchContext($desired_context);

  //Get the wayfinder output

  $output = $modx->runSnippet('wayfinder',$cs_config);

  //Switch back to the original context

  $modx->switchContext($wcontext);


//return the output

  return $output;

The wrapped call is now:

[[!caseStudiesWayfinder? &startId=`44` &level=`1` &lastClass=``
outerTpl=`ThumbOuterTpl` &rowTpl=`ThumbRowTpl` &wcontext=`web` ]]

There’s another important step to make sure that any cross context Wayfinder call or getResources call actually gets pages you want the user to see. You need to add the contexts to the anonymous user, otherwise your Modx error logs will show something like

[-- 13:02:21] (ERROR @ /index.php) Could not load context: mycontext

Go to Security -> Access Controls and then right click (anonymous). Click add context and this should allow your web users to view the different contexts.

Exporting large databases with MySQL dump

I had to export a 7GB database recently using mysqldump.

I started out with the usual export call

mysqldump -u admin -p sugar > sugar_28_may.sql

However, the users complained that they couldn’t access the system while it was being exported.

The culprit was mysqldump’s habit of locking tables to export them. The solution was this

mysqldump -u admin -p --lock-tables=false sugar > sugar_28_may.sql

The other problem I got was this message

mysqldump: Error 2020: Got packet bigger than 'max_allowed_packet' bytes when dumping table `emails_text` at row: 84538

That message meant that I had to wait 15 minutes while the backup went up to 2GB and then the error kicked in.

I checked the max_allowed_packet by running

mysqldump --help
---
max_allowed_packet                25165824

It turned out the server had a 24MB packet limit. I increased it to 1GB.

mysqldump -u admin -p --max_allowed_packet=1073741824 --lock-tables=false sugar >sugar_28may12.sql

Note that the –max_allowed_packet option on MySQL 5.0xx seems to be max-allowed-packet on MySQL 5.5x. Running mysqldump –help will sort out the correct syntax to use.

Zend Framework, creating a contact form

Creating a contact form is quite straightforward in the Zend Framework.

zf create form Contact

zf create controller Form

zf create view Form

Specify the form in the Application_Form_Contact class in application/forms/Contact.php

class Application_Form_Contact extends Zend_Form
{

    public function init()
    {
        // Set the method for the display form to POST
        $this->setMethod('post');

        // Add a name element
        $this->addElement('text', 'name', array(
            'label' => 'Your name:',
            'required' => true,
            'filters' => array('StringTrim'),
            'validators' => array(
                'NotEmpty',
            )
        ));

        $this->addElement('text', 'phone', array(
            'label' => 'Your phone:',
            'required' => false,
            'filters' => array('StringTrim'),

        ));

        // Add an email element
        $this->addElement('text', 'email', array(
            'label' => 'Your email address:',
            'required' => true,
            'filters' => array('StringTrim'),
            'validators' => array(
                'EmailAddress',
            )
        ));

        // Add the comment element
        $this->addElement('textarea', 'comment', array(
            'label' => 'Please Comment:',
            'required' => true,
            'validators' => array(
                array('validator' => 'StringLength', 'options' => array(0, 20))
            )
        ));

        // Add a captcha
        $this->addElement('captcha', 'captcha', array(
            'label' => 'Please enter the 5 letters displayed below:',
            'required' => true,
            'captcha' => array(
                'captcha' => 'Figlet',
                'wordLen' => 5,
                'timeout' => 300
            )
        ));

        // Add the submit button
        $this->addElement('submit', 'submit', array(
            'ignore' => true,
            'label' => 'Sign Guestbook',
        ));

        // And finally add some CSRF protection
        $this->addElement('hash', 'csrf', array(
            'ignore' => true,
        ));
    }

}

Write an action in application/controllers/FormController.php

public function contactAction()
    {
        $request = $this->getRequest();
        $form = new Application_Form_Contact();
        if ($this->getRequest()->isPost()) {
            if ($form->isValid($request->getPost())) {
                echo 'OK';
            }
        }

        $this->view->form = $form;
    }

Update the routes so that /contact will get the contact form in /Bootstrap.php in the _initRoutes function. Note: the routes are matched in reverse order so the most generic one has to be first.

$frontController = Zend_Controller_Front::getInstance();
        $router = $frontController->getRouter();

        $contact = new Zend_Controller_Router_Route('contact',array('controller'=>'Form',
 'action'=>'contact'));

        $route_content = new Zend_Controller_Router_Route_Regex(
            '(\w*)',
            array('controller' => 'content',
                'action' => 'index' ),
            array(1 => 'url')
        );

        try{
            $router->addRoute('content', $route_content);
            $router->addRoute('form', $contact);
        }catch(Exception $e) {
            print($e->getTraceAsString());
        }

All fine, this is what I get:

Zend Framework, setting up a dynamic menu using a plugin

I wanted to set up the menu in layout.phtml using a dynamic call to the database.

I created a directory in library called plugins.

I put this in application.ini
autoloaderNamespaces[] = “Plugins”

I created a class in the plugins directory called Plugins_Layout with the file name Layout.php

class Plugins_Layout extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $layout = Zend_Layout::getMvcInstance();
        $view = $layout->getView();

        $content_mapper = new Application_Model_ContentMapper();

        $view->menu = $content_mapper->getMenu();
    }
}

I then added this to Bootstrap.php

protected function _initRoutes() {
        ...... 

       Zend_Controller_Front::getInstance()->registerPlugin(new Plugins_Layout());
    }

I was then able to create the dynamic menu in layout.phtml

 menu as $url => $title) { ?>
    

All was working as I expected.

Zend framework, mapping urls in the database

Having set up the application and got it to serve a page from the database, I need to make sure that it was getting the content I wanted.

I wanted to have something like http://localhost/my_title find the content from the url field with value my_title

I added this to my Bootstrap.php

protected function _initRoutes() {
        $frontController = Zend_Controller_Front::getInstance();
        $router = $frontController->getRouter();

        $route = new Zend_Controller_Router_Route_Regex(
            '(\w*)',
            array('controller' => 'content',
                'action' => 'index' ),
            array(1 => 'url')
        );


        try{
           $router->addRoute('content', $route);
        }catch(Exception $e) {
            print($e->getTraceAsString());
        }

    }

This mapped to the ContentController class with the following code

public function indexAction()
    {
        // action body
        $content_mapper = new Application_Model_ContentMapper();

        $url = $this->getRequest()->getParam('url');
        $content = new Application_Model_Content();
        $content_mapper->findByUrl($url, $content);
        $this->view->content = $content;

    }

Zend framework, database connection

I set up a database connection in application/config/application.ini

resources.db.adapter = "PDO_MYSQL"
resources.db.params.host = "localhost"
resources.db.params cialis preise holland.username = "username"
resources.db.params.password = "password"
resources.db.params.dbname = "montesnegro"

Next I created models:

zf create db-table Content content
zf create model ContentMapper
zf create model Content
zf create controller Content

I created the PHP for the Content model in application/models/content

class Application_Model_Content
{
    private $id;
    private $menu_id;
    private $content_text;
    private $update_date;

    function __construct($id = null,$menu_id = null,$content_text = null,$update_date = null)
    {
        $this->id = $id;
        $this->menu_id = $menu_id;
        $this->content_text = $content_text;
        $this->update_date = $update_date;
    }


    public function setContentText($content_text)
    {
        $this->content_text = $content_text;
        return $this;
    }

    public function getContentText()
    {
        return $this->content_text;
    }

    //other getters and setters.....
}

I created a ContentMapper class

class Application_Model_ContentMapper
{

    /**
     * @var Zend_Db_Table_Abstract
     */
    protected $_dbTable;

    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }

    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Application_Model_DbTable_Content');
        }
        return $this->_dbTable;
    }

    public function save(Application_Model_Content $content)
    {
        $data = array(
            'content_text'   => $content->getContentText(),
            'menu_id' => $content->getMenuId(),
            'update_text' => date('Y-m-d H:i:s'),
        );

        if (null === ($id = $content->getId())) {
            unset($data['id']);
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }

    public function getMenu()
    {
        $select = $this->getDbTable()->select()->order("order ASC ");
        $results = $this->getDbTable()->fetchAll($select);
        $menu   = array();
        foreach ($results as $row) {
            $menu[$row->url] = $row->title;
        }
        return $menu;

    }

    public function find($id, Application_Model_Content $content)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $this->setContent($result->current(), $content);
    }

    public function findByUrl($url,Application_Model_Content $content)
    {
        if(!$url){
            $url = 'home';
        }
        $select = $this->getDbTable()->select()->where("url = '$url' ");
        $result = $this->getDbTable()->fetchRow($select);
        if (0 == count($result)) {
            return;
        }
        $this->setContent($result->toArray(), $content);
    }

    private function setContent($row, Application_Model_Content $content)
    {
        $content->setId($row['id'])
            ->setContentText($row['content_text'])
            ->setMenuId($row['menu_id'])
            ->setUpdateDate($row['update_date'])
            ->setUrl($row['url'])
            ->setTitle($row['title'])
        ;
    }

    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Application_Model_Content();
            $this->setContent($row, $entry);
            $entries[] = $entry;
        }
        return $entries;
    }

}

And then I created the controller

class ContentController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        // action body
        $content_mapper = new Application_Model_ContentMapper();

        $url = $this->getRequest()->getParam('url');
        $content = new Application_Model_Content();
        $content_mapper->findByUrl($url, $content);
        $this->view->content = $content;

    }


}

Finally, I created a MySQL table called content and put some stuff in there.

When I did http://localhost/content, all was well.

Zend framwork, layout

I have set up the Zend Framework and had it serving up the default page. Now it was time to set up a layout.

zf enable layout

Then I put in the HTML for application/layout/scripts/layout.phtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Montes Negros</title>
<?php echo $this->headLink()->appendStylesheet('/css/main.css') ?>
</head>

<body>
<div id="background_layer"><img src="/images/background.jpg" alt="" width="100%" /></div>
<div id="container">
<div id="allcontent">
<div id="header">
<div id="header_text">
<h1>Casa Montes Negros</h1>
<h2>Rural Organic Rental Cottage</h2>
</div>

</div>
<div id="left_menu">
<p>Menu</p>
<p>Menu</p>
<p>Menu</p>
<p>Menu</p>
<p>Menu </p>
</div>

<div id="main"><?php echo $this->layout()->content ?></div>

<div id="footer">&copy;Montes Negros 2012</div>
</div>
</div>
</body>
</html>

I updated application/views/scripts/index/index.phtml with some default text. This gave me:

Zend framework set up

I’m currently using the Zend framework to set up a holiday rental web site in Spain. I am putting up a series of blog posts to act as a how to and to give a heads up on some of the gotchas I encountered on the way.

I downloaded the Zend framework and installed it on my local server. I’m running a Windows 7 PC and I have OSX laptop running Lion. Set up was the same on both, I download the framework and put the path in the php.ini.

My Windows php.ini had this

include_path=".;C:\PHP\includes\zend\library

I ran

zf create project montesnegros

I then set up a virtual host (again the same process on Lion and Windows 7)

NameVirtualHost rachael.test:80

<VirtualHost rachael.test:80>
DocumentRoot "G:/data/WorkSites/rachael/montesnegros/public"
DirectoryIndex index.php
ServerName rachael.test

<Directory "G:/data/WorkSites/rachael/montesnegros/public">
Options +FollowSymLinks
AllowOverride All
#order allow,deny
allow from all
</Directory>
</VirtualHost>

I checked the that application had been created with the correct file structure. I found a great resource for understanding the application.ini file at https://github.com/feibeck/application.ini/blob/master/application.ini

All was fine and when I restarted Apache, I got this

Apache OSX Lion 403 Forbidden Problem

I had to set up multiple development sites using the Apache virtual hosting. All were coming back with the dreaded 403 page.

I did a lot of research and nothing seemed to work.

Finally, I found something that did. My virtual hosts were all running within directories that I had set up outside the /Library/Webserver location. I didn’t want to move the directories there and I didn’t want to sym link my directories to there either.

I found a line in the http.conf file

User _www
Group _www

I changed it to

User kevinsaunders
Group staff

All fine, except that PHP wasn’t allowed to set a session file in /var/tmp.

sudo chown -R kevinsaunders:staff /var/tmp/*

That seemed to fix it all.