Eric Kennedy at Iceland's Jökulsárlón Glacier River Lagoon

Drupal to Yii Migration Tips

So you've gotten fed up with the coding contortions required to scale Drupal and you've decided to switch to Yii. Now comes the daunting task of deciding what and how to migrate. The best advice I can give you is to critically examine which features are used by 5% or more of your site visitors. The product team at Netflix believes that "simple trumps complete", and they remove features used by fewer than 5% of users to improve usability. Take their advice and pare back your initial re-launch featureset to only those used by more than 5% of users. Anyone migrating from Drupal probably has a lot of rewritten URLs that they want to keep. Yii's support for database queries to determine the controller and ID for a URL was a big reason we chose it over PHP frameworks that don't support database queries until a controller has been called. Create an uri_alias table in your new database:

CREATE TABLE `uri_alias` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `uri` varchar(128) NOT NULL DEFAULT '',
  `controller_action` varchar(32) NOT NULL DEFAULT '',
  `content_id` int(10) unsigned NOT NULL DEFAULT '',
  `redirect` varchar(128) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uri` (`uri`),
  KEY `controller_id` (`content_id`,`controller_action`(8))
) ENGINE=MyISAM DEFAULT CHARSET=utf8

and insert entries into it from the url_alias table in Drupal. (We switched the url to a uri because it is only the path after the domain, which is technically not a full URL.) As an example, here's what a single insert statement would look like for a blog node with nid = 3 on drupal and uri 'blog/drupal-launch'

INSERT INTO yii.uri_alias (uri, controller_action, content_id) values ('blog/drupal-launch', 'blog/view', 3);

When the extended CUrlManager finds this in the DB (see below), it will tell the Blog controller to call actionView and display the post with id = 3.

/**
 * Override CUrlManager to allow paths to append more arguments and still resolve the path
 */
class UrlManager extends CUrlManager {
 
  /**
     * Parses the user request.
     * @param CHttpRequest the request application component
     * @return string the route (controllerID/actionID) and $_GET[] parameters set by this function
     */
    public function parseUrl($request) {
    $uri = $request->pathInfo;
       
    // 99% of requests hit a rewritten url, so we should query DB first
    $rs = db()->createCommand("SELECT controller_action,content_id,redirect,uri FROM uri_alias WHERE uri = :uri")->queryRow(TRUE, array('uri' => $uri));    
       
    if ($rs['content_id'] > 0) {
          
      if (strcmp($uri, $rs['uri']) == 0) {   // case matches exactly
         $_GET['id'] = $rs['content_id'];     // set GET parameter for controller
         return $rs['controller_action'];     
      }
      // case must not match, so redirect
      $request->redirect('/' . $rs['uri'], true, 301);
                  
    } elseif (!empty($rs['redirect'])) {
      $request->redirect($rs['redirect'], true, 301);
    }
    
    /**
     * if no DB match, try pattern matching
     * standard paths are
     * blog/$id/view = blog/$id
     * blog/$id/update or blog/$id/delete
     */
    if (is_numeric(arg(1))) {
            $controller = str_replace('-', '', arg(0));
            $action = arg(2);
            $_GET['id'] = arg(1);
            return $this->removeDashes($controller) . '/' . ($action ? $this->removeDashes($action) : 'view');
    }
    
    switch (arg(0)) {
        case 'minify':
            $_GET['group'] = arg(1);
            return 'minify/index';
            break;
        case 'xmlsitemap0.xml':
            return 'XMLSitemap/view';
            break;
    }
       
    // Direct
    return $this->removeDashes($uri); // Remove dashes to support dash-path actions (linux case sensitive too!)
    }
    
    /**
     * Replaces dashes, and also ensures the next letter will be uppercase, since Linux is case-sensitive
     */
    private function removeDashes($str) {
        return str_replace(' ', '', ucwords(str_replace('-', ' ', $str)));
    }
}

Since Drupal used a central node table, we preserved the old nids with an id_controller table that also ensures that a content id correspondes to a specific Model (instead of allowing a Blog with id = 3 and a Topic with id = 3, etc.)

CREATE TABLE `id_controller` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `controller` varchar(32) NOT NULL DEFAULT '',
 `views` int(10) unsigned NOT NULL DEFAULT '0',
 `notes` text,
 PRIMARY KEY (`id`),
 UNIQUE KEY `id_controller` (`id`,`controller`)
) ENGINE=MyISAM AUTO_INCREMENT=300616 DEFAULT CHARSET=utf8

This post is a work in progress, so post questions in the comments.

Drop-in widgets in Yii

Drupal provides a number of ways to add complex self-contained modules to various pages on your site. Yii can accomplish this with its extendable widget class, CWidget, that allows you to create widgets that you can drop in anywhere in your code.

One of the things that makes Yii very fast is its aggressive use of PHP's autoloading feature available out of the box. The first time you reference a class, such as MyClass::doSomething(), is when the class file is included. Yii's use of autoloading allows it to minimize the number of resources loaded during each request. In contrast, Drupal loads every module for each request, even if some modules are never called. With Yii, you can create a large library of on-demand components without comprising speed and resources with extra execution overhead.

A widget class is a good place to handle drop-in forms (newsletter sign-up) and other modules that appear on many different pages. Here's a widget class that outputs a simple pager (located at protected/components/PagerWidget.php):

class Pager extends CWidget {
    public $perPage;
    public $numItems;
    public $curPage;
    public function run() {
        $uri = Yii::app()->getRequest()->getPathInfo();
        $totalPages = ceil($this->numItems / $this->perPage);
        for ($i = 1; $i < $totalPages; $i++) {
            if ($i == $curPage) {
                print '' . $i . ''; // No link for current page
            } else {
                print '<a href="/' . $uri . '?page=' . $i . '">' . $i . '</a>';
            }
        }
    }
}

You can include this widget in your output by calling the widget() method of your current controller (tip: when working in your view files, $this is a reference to the controller). The second argument is an array that populates the public attributes of your widget class:

$this->widget('PagerWidget', array('perPage' => 10, 'numItems' => $numItems, 'curPage' => $curPage))

If your widget has a complex view, you can put your view code in a separate file in the component views directory (/root/protected/components/views/pagerWidget.php), and call it from your widget class's run() method:

$this->render('pagerWidget', $view_args);

Tips:

 

Helpful site?

Buying products I link to on Amazon gives me credits at no cost to you. As an Amazon Associate I earn from qualifying purchases.