《Yii1.1 Application Development Cookbook》

Chapter 12: Using External Code

In this chapter, we will cover:

  • Using Zend Framework from Yii
  • Customizing the Yii autoloader
  • Using Kohana inside Yii
  • Using PEAR inside Yii

Introduction

Typically, an application requires more than any framework can give. Sometimes, you need a full-featured library to send an e-mail and sometimes, it is just about implementation of the specific API. No framework can cover every possible task that a developer has. That is why Yii covers the most common ones and leaves the rest to the developer and external libraries.

In this chapter, we will try to use a non-Yii code with Yii, including Zend Framework, Kohana, and Pear.

Using Zend Framework from Yii

Yii provides many excellent solutions with which you can build an application. Still, you probably will need more. One of the best places to look at is Zend Framework classes. These are of high quality and solve many tasks, such as using Google APIs or working with e-mails.

In this recipe, we will see how to use the Zend_Mail package to send e-mails from Yii application. We will use both a simple approach of using the whole framework and will also implement a custom autoloader that will allow us to use only Zend_Mail and its dependencies.

Getting ready

  • Create a fresh application by using yiic webapp
  • Download the Zend Framework code from the following URL: http://framework.zend.com/download/current/ In this recipe, we have used Version 1.11.6
  • Extract library/Zend from the downloaded archive to protected/vendors/ Zend

How to do it...

Carry out the following steps:

  1. We will create a simple controller that will send an e-mail. Create protected/ controllers/MailtestController.php as follows:
<?php
class MailtestController extends CController
{
    public function actionIndex()
    {
        $mail = new Zend_Mail('utf-8');
        $mail->setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE);
        $mail->addTo("alexander@example.com", "Alexander Makarov");
        $mail->setFrom("robot@example.com", "Robot");
        $mail->setSubject("Test email");
        $mail->setBodyText("Hello, world!");
        $mail->setBodyHtml("Hello, <strong>world</strong>!");
        $mail->send();
        echo "OK";
    }
}
  1. Now try to run mailtest/index and you will get the following result:
  1. This means that the Yii autoloader failed to include the Zend_Mail class. This is expected because it knows nothing about the Zend Framework naming convention. So logically, we have the following two solutions to this:
    • Include classes explicitly
    • Create our own autoloaderM
  1. We will now start with including classes. All Zend Framework classes do have require_once statements for all dependencies. These statements rely on adding an additional PHP include path and look like the following:
require_once 'Zend/Mail/Transport/Abstract.php';
  1. When using Yii::import to import a directory, it works like adding a directory into the PHP include path, so we can solve our problem as follows:
class MailtestController extends CController
{
    public function actionIndex()
    {
        Yii::import('application.vendors.*');
        require "Zend/Mail.php";
        $mail = new Zend_Mail('utf-8');
        $mail->setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE);
        $mail->addTo("alexander@example.com", "Alexander Makarov");
        $mail->setFrom("robot@example.com", "Robot");
        $mail->setSubject("Test email");
        $mail->setBodyText("Hello, world!");
        $mail->setBodyHtml("Hello, <strong>world</strong>!");
        $mail->send();
        echo "OK";
    }
}
  1. Now it will send an e-mail properly without any error. This method will work if you don't have too many Zend Framework classes used. If you are using it heavily, then you will have to include a lot that will add unnecessary complexity. Now, let's use Zend_Loader_Autoloader to achieve this.
  1. The best place to add another autoloader is in the index.php bootstrap. This way, you will be able to autoload classes during the whole execution flow:
// change the following paths if necessary
$yii=dirname(__FILE__).'/../framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
 
// remove the following lines when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
// specify how many levels of call stack should be shown in each log message
defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3);
       
require_once($yii);
$app = Yii::createWebApplication($config);
 
// adding Zend Framework autoloader
Yii::import('application.vendors.*');
require "Zend/Loader/Autoloader.php";
Yii::registerAutoloader(array('Zend_Loader_Autoloader', 'autoload'), true);
 
$app->run();
  1. Now we can remove
Yii::import('application.vendors.*');
require "Zend/Mail.php";

from MailtestController and it will still work fine without any errors meaning that Zend Framework autoloading now works.

How it works...

Let's review what is going on behind the scenes and how it works starting with the first way. We have used Yii::import that, when used like Yii::import('path.alias.*'), behaves like adding another PHP include path. As there was no autoloader in Zend Framework originally, it has all the necessary require_once calls. So, if you use a single component, such as Zend_Mail, you don't need more than one require_once.

The second method doesn't force you to use a single require statement. As Yii allows using multiple autoloaders and in latest versions, Zend Framework has its own autoloader, we can use it in our application. The best time to do this is right after the application bootstrap was loaded, but application was not run. To achieve this, we break Yii::createWebAppl ication($config)->run() into two separate statements in index.php and insert an autoloader initialization between these:

Yii::import('application.vendors.*');
require "Zend/Loader/Autoloader.php";
Yii::registerAutoloader(array('Zend_Loader_Autoloader', 'autoload'), true);

We still need Yii::import('application.vendors.*') because Zend Framework classes will continue to use require_once. Then, we require an autoloader class and add it to the end of the PHP autoloading stack by using Yii::registerAutoloader with the second argument set to true.

There's more...

In order to learn more about the Yii import, autoloading, and Zend Framework usage, refer to the following URLs:

See also

  • The recipe named Customizing the Yii autoloader in this chapter

Customizing the Yii autoloader

Yii uses a naming convention and an autoloader to load only classes which are really needed and to avoid including files explicitly. As other frameworks and libraries could use a different naming convention, Yii provides an ability to customize rules of autoloading classes. In the Using Zend Framework from Yii recipe in this chapter, we used Zend_Loader_Autoloader to be able to use Zend Framework classes without including them explicitly. If we are using only Zend Framework core classes, then its complex autoloader is a bit too much. Moreover, there are still require_once calls in each Zend Framework class, so it still loads tons of unused files. In this recipe, we will create a very simple and fast autoloader that will allow us to do the same, but faster.

Getting ready

  • Create a fresh application by using yiic webapp
  • Download Zend Framework code from the following URL: http://framework.zend.com/download/current/ In this recipe, we have used Version 1.11.6
  • Extract library/Zend from the downloaded archive to protected/vendors/Zend
  • Create protected/controllers/MailtestController.php as follows:
<?php
class MailtestController extends CController
{
    public function actionIndex()
    {
        $mail = new Zend_Mail('utf-8');
        $mail->setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE);
        $mail->addTo("alexander@example.com", "Alexander Makarov");
        $mail->setFrom("robot@example.com", "Robot");
        $mail->setSubject("Test email");
        $mail->setBodyText("Hello, world!");
        $mail->setBodyHtml("Hello, <strong>world</strong>!");
        $mail->send();
        echo "OK";
    }
}

How to do it...

Carry out the following steps:

  1. Create protected/components/EZendAutoloader.php as follows:
<?php
class EZendAutoloader
{
    /**
     * @var array class prefixes
     */
    static $prefixes = array(
        'Zend'
    );
    
    /**
     * @var string path to where Zend classes root is located
     */
    static $basePath = null;
    
    /**
     * Class autoload loader.
      *
     * @static
     * @param string $className
     * @return boolean
     */
    static function loadClass($className)
    {
        foreach(self::$prefixes as $prefix)
        {
            if(strpos($className, $prefix.'_')!==false)
            {
                if(!self::$basePath) self::$basePath =
                    Yii::getPathOfAlias("application.vendors").'/';
                include self::$basePath.str_replace('_','/',$className).'.php';
                return class_exists($className, false) ||
                interface_exists($className, false);
            }
        }
        return false;
    }
}
  1. Now modify index.php. Replace
Yii::createWebApplication($config)->run();

with

$app = Yii::createWebApplication($config);
 
// adding custom Zend Framework autoloader
Yii::import("application.vendors.*");
Yii::import("application.components.EZendAutoloader", true);
Yii::registerAutoloader(array('EZendAutoloader','loadClass'), true);
 
$app->run();
  1. Try running mailtest/index. It should send an e-mail and output "OK" which means autoloading works correctly. Still, we are importing the vendors directory to satisfy Zend Framework require_once calls by loading all possible classes explicitly. The only way to fix it is to remove all occurrences of require_once from Zend Framework. If you're under Linux you can use:
% cd path/to/ZendFramework/library
% find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \
  -not -wholename '*/Application.php' -print0 | \
  xargs -0 sed --regexp-extended --in-place 's/(require_once)/\/\/ \1/g'

In addition, on MacOSX:

% cd path/to/ZendFramework/library
% find . -name '*.php' | grep -v './Loader/Autoloader.php' | \
xargs sed -E -i~ 's/(require_once)/\/\/ \1/g'
% find . -name '*.php~' | xargs rmf

Or you can simply use IDE or other tools to replace require_once with //require_once.

  1. After doing it you can remove Yii::import("application.vendors.*") from index. php and try loading mailtest/index again. It should send another e-mail and output "OK". That means Zend Framework classes are now working without require_once.

How it works...

Frameworks and libraries that use autoloading rely on PHP SPL autoload. It is triggered when you use a class that is not included yet and PHP is going to fail. Using spl_autoload_ register, you can register multiple autoload callbacks. Therefore, if the first one fails, then another one will take initiative and will try to load a class. Yii is not an exception. By default, it uses its own autoloader implementation YiiBase:: autoload. We have used Yii::registerAutoloader in index.php to add an additional autoloader. The method implementation is as follows:

public static function registerAutoloader($callback, $append=false)
{
    if($append)
    {
        self::$enableIncludePath=false;
        spl_autoload_register($callback);
    }
    else {
        spl_autoload_unregister(array('YiiBase','autoload'));
        spl_autoload_register($callback);
        spl_autoload_register(array('YiiBase','autoload'));
    }
}

So internally, this is the same SPL autoloader, and the registerAutoloader method just adds another callback and by default makes sure it is registered before Yii's own autoloader. If we pass true as the second parameter value then the custom autoloader is registered after the Yii internal one. It does not allow triggering of the custom autoloader for Yii classes.

Now, we will move on to our custom autoloader. All SPL autoload callbacks accept a single argument containing the name of the class that needs to be loaded. Given this name, it should try to include a file containing a class with the specified name. In order to get more flexibility, we defined the following two properties:

  1. The prefixes property defines a list of prefixes a class should begin with to be autoloaded with our custom autoloader. By default, it is Zend.
  2. The basePath defines a path to a directory where the Zend directory is. By default, it equals to protected/vendors.

For each prefix, we check if the class we are trying to load begins with it and if we need to use an autoloader. If it matches, then we replace "_" with "/" in the class name and use it as a complete path to the file we are including.

In the final step, we get rid of require_once that prevents us from loading only classes that are absolutely required.

There's more...

As the class loading happens all the time, we use an external library; it should perform as fast as it can. This means that autoloading method should be simple and efficient. Other things to note are as follows:

  • require_once is slower than just require
  • Using file_exists or is_file will slow down the loading
  • You should use absolute paths instead of relative ones to ensure that APC performs efficiently when apc.stat = 0 (it allows not to check if the file was changed and gain more performance on the production server)
Further reading

In order to learn more about Yii autoloading and APC, refer to the following URLs:

See also

  • The recipe named Using Zend Framework from Yii in this chapter

Using Kohana inside Yii

Sometimes to write a custom autoloader, you need to dig into another framework source code. An example of it is the Kohana framework. In this recipe, we will handle the image resizing by using one of the Kohana classes.

Getting ready

  • Create a fresh application using yiic webapp
  • Download the Kohana framework archive from the following URL: http://kohanaframework.org/download In this recipe, we have used Version 3.1.
  • Extract the system and modules directories to protected/vendors/Kohana.

How to do it...

Carry out the following steps:

  1. First, we will need the actual code which performs the image resizing and displays an image. Create protected/controllers/ImageController.php as follows:
<?php
class ImageController extends CController
{
    public function actionIndex()
    {
        $image = new Image_GD(Yii::getPathOfAlias("system")."/yii-powered.png");
        $image->resize(80, 80);
        Yii::app()->request->sendFile("image.png", $image->render());
    }
}
  1. Try to run image/index and you will get the following error:
  1. This means that Yii cannot find Kohana classes. In order to help it, we will need a custom autoloader. So, create protected/components/EKohanaAutoloader. php as follows:
<?php
class EKohanaAutoloader
{
    /**
     * @var list of paths to search for classes.
     * Add full paths to modules here.
     */
    static $paths = array();
         
    /**
     * Class autoload loader.
     *
     * @static
     * @param string $className
     * @return boolean
     */
    static function loadClass($className)
    {
        if(!defined("SYSPATH"))
            define("SYSPATH", Yii::getPathOfAlias("application.vendors.Kohana.system"));
        if(empty(self::$paths))
            self::$paths = array(Yii::getPathOfAlias("application.vendors.Kohana.system"));
        $path = 'classes/'.str_replace('_', '/', strtolower($className)).'.php';
        foreach (self::$paths as $dir)
        {
            if (is_file($dir."/".$path))
                require $dir."/".$path;
        }
        return false;
    }
}
  1. In order to use it, we need to modify index.php. Replace
Yii::createWebApplication($config)->run();

with the following:

$app = Yii::createWebApplication($config);
 
// adding custom Kohana autoloader
Yii::import("application.components.EKohanaAutoloader", true);
EKohanaAutoloader::$paths = array(Yii::getPathOfAlias
    ("application.vendors.Kohana.modules.image"));
Yii::registerAutoloader(array('EKohanaAutoloader','loadClass'), true);
 
$app->run();
  1. Now run image/index again and you should see a screen similar to one shown in the following screenshot instead of an error:

That means Kohana classes were loaded successfully.

Note that the Kohana class loader provided was not optimized in terms of performance and is not intended for intensive production use.

How it works...

Kohana 3 relies on autoloading and has a very special naming convention. As a result, calling its classes directly is too much work and creating an autoloader is the only reasonable way to implement it if we are not modifying Kohana classes.

We will take a look at the Kohana autoloader which is present at the following location:

protected/vendors/Kohana/system/classes/kohana/core.php

The method name is auto_load:

public static function auto_load($class)
{
    try {
        // Transform the class name into a path
        $file = str_replace('_', '/', strtolower($class));
        if ($path = Kohana::find_file('classes', $file))
        {
            // Load the class file
            require $path;
         
            // Class has been found
            return TRUE;
        }
        // Class is not in the filesystem
        return FALSE;
    }
    catch (Exception $e)
    {
        Kohana_Exception::handler($e);
        die;
    }
}

From this part, we can say that it uses a class to form a relative path which is then used to find a file inside of the classes directory:

$file = str_replace('_', '/', strtolower($class));

Now let's go deeper inside find_file:

public static function find_file($dir, $file, $ext = NULL, $array = FALSE)
{
    if ($ext === NULL)
    {
        // Use the default extension
        $ext = EXT;
    }
    elseif ($ext)
    {
        // Prefix the extension with a period
        $ext = ".{$ext}";
    }
    else {
        // Use no extension
        $ext = '';
    }
    
    // Create a partial path of the filename
    $path = $dir.DIRECTORY_SEPARATOR.$file.$ext;
    
    if (Kohana::$caching === TRUE AND isset
        (Kohana::$_files[$path.($array ? '_array' : '_path')]))
    {
        // This path has been cached
        return Kohana::$_files[$path.($array ? '_array' : '_path')];
    }
    
    if (Kohana::$profiling === TRUE AND class_exists
        ('Profiler', FALSE))
    {
        // Start a new benchmark
        $benchmark = Profiler::start('Kohana', __FUNCTION__);
    }
    
    if ($array OR $dir === 'config' OR $dir === 'i18n'
        OR $dir === 'messages')
    {
        // Include paths must be searched in reverse
        $paths = array_reverse(Kohana::$_paths);
        
        // Array of files that have been found
        $found = array();
        
        foreach ($paths as $dir)
        {
            if (is_file($dir.$path))
            {
                // This path has a file, add it to the list
                $found[] = $dir.$path;
            }
        }
    }
    else {
        // The file has not been found yet
        $found = FALSE;
        
        foreach (Kohana::$_paths as $dir)
        {
            if (is_file($dir.$path))
            {
                // A path has been found
                $found = $dir.$path;
                
                // Stop searching
                break;
            }
        }
    }
    
    if (Kohana::$caching === TRUE)
    {
        // Add the path to the cache
        Kohana::$_files[$path.($array ? '_array' : '_path')] = $found;
        
        // Files have been changed
        Kohana::$_files_changed = TRUE;
    }
    
    if (isset($benchmark))
    {
        // Stop the benchmark
        Profiler::stop($benchmark);
    }
    
    return $found;
}

As we know that our file extension is always .php, the directory is always classes, and as we don't care about the caching or profiling right now, the useful part is as follows:

$path = $dir.DIRECTORY_SEPARATOR.$file.$ext;
foreach (Kohana::$_paths as $dir)
{
    if (is_file($dir.$path))
    {
        // A path has been found
        $found = $dir.$path;
        
        // Stop searching
        break;
    }
}

We are pretty close. The only thing left is Kohana::$_paths:

    /**
     * @var  array   Include paths that are used to find files
     */
    protected static $_paths = array(APPPATH, SYSPATH);

We don't care about the application, so we can omit the APPPATH part. Moreover, SYSPATH is a path to the system directory. As most of the Kohana classes are there, it is reasonable to make this a default.

When the autoloader class is ready, we use Yii::registerAutoloader in index.php to register it. It is important to register the autoloader after the default one built in Yii, so we pass true as the Yii::registerAutoloader second parameter value. Our image class is not in the core and is located in the image module, so we set paths to the image module path in the following way:

EKohanaAutoloader::$paths = array(Yii::getPathOfAlias
    ("application.vendors.Kohana.modules.image"));

There's more...

As image resizing is a common task, it is better from both reusability and performance perspectives to separate this task from the rest of the application and create a separate PHP script that will handle the image resizing. For example, it will allow using the following code:

<img src="/image.php?src=avatar.png&size=s" />

This means, take avatar.png as the source image and resize it to 100×100 pixels. Possible steps the image.php script will take are as follows:

  • If an already processed image exists, serve it
  • If there is no image yet, read the source image, resize it, and write it as the processed one

In order to achieve better performance, you can configure the web server to serve existing images directly, avoid serving with a PHP script, and redirecting the non-existing ones to the processing script.

Further reading

In order to learn more about Yii autoloading and Kohana, refer to the following URLs:

See also

  • The recipe named Customizing the Yii autoloader in this chapter

Using PEAR inside Yii

Another traditional place to look for PHP libraries is PEAR. There is a very special naming convention, so in order to use the PEAR code, we can either implement another autoloader or include files directly. In this recipe, we will use the PEAR Text_Password class to generate a random password.

Getting ready

  • Create a fresh application by using yiic webapp
  • Make sure that PEAR is installed and configured properly (http://pear.php.net/ manual/en/installation.php)

How to do it...

The page of the PEAR package that we want to use is http://pear.php.net/package/Text_Password. Important things: there are "Easy Install" and documentation.

  1. We will install the package first. Open the console and type what is suggested in "Easy Install section":
pear install Text_Password
  1. It should respond with the following:
downloading Text_Password-1.1.1.tgz ...
Starting to download Text_Password-1.1.1.tgz (4,357 bytes)
.....        done: 4,357 bytes
install ok: channel://pear.php.net/Text_Password-1.1.1
  1. Now we can try using it. We will generate 10 random passwords, the length of which equals to 8. Create protected/controllers/PasswordController.php as follows:
class PasswordController extends CController
{
    public function actionIndex()
    {
        require "Text/Password.php";
        $textPassword = new Text_Password();
        $passwords = $textPassword->createMultiple(10, 8);
        echo "<ul>";
        foreach($passwords as $password)
        {
            echo "<li>".$password."</li>";
        }
        echo "</ul>";
    }
}

How it works...

Using PEAR packages in Yii is easy. You don't need to configure Yii or write any additional code to the one provided in the PEAR package's guide.

There's more...

In order to learn more about PEAR, refer to the following URLs:

See also

  • The recipe named Using Zend Framework from Yii in this chapter
评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com