CHAPTER 15

image

An Introduction to PEAR and Pyrus

Programmers aspire to produce reusable code. This is one of the great goals in object-oriented coding. We like to abstract useful functionality from the messiness of specific context, turning it into a tool that can be used again and again. To come at this from another angle, if programmers love the reusable, they hate duplication. By creating libraries that can be reapplied, programmers avoid the need to implement similar solutions across multiple projects.

Even if we avoid duplication in our own code, though, there is a wider issue. For every tool you create, how many other programmers have implemented the same solution? This is wasted effort on an epic scale: wouldn’t it be much more sensible for programmers to collaborate and to focus their energies on making a single tool better, rather than producing hundreds of variations on a theme? This is where PEAR (PHP Extension and Application Repository) comes in.

PEAR is a repository of quality-controlled PHP packages that extend the functionality of PHP. It is also a client-server mechanism for distributing and installing packages and for managing interpackage dependencies.

This chapter will cover:

  • PEAR basics: What is this strange fruit?
  • Installing PEAR package installation: All it takes is one command.
  • Working with Pyrus, PEAR’s younger sibling
  • Adding PEAR packages to your projects: An example and some notes on error handling.
  • package.xml: The anatomy of a build file.
  • Creating your own channel: Providing transparent dependency management and package downloads for users.

What Is PEAR?

At its core, PEAR is a collection of packages, organized into broad categories, such as networking, mail, and XML. The PEAR repository is managed centrally, so that when you use an official PEAR package, you can be sure of its quality.

You can browse the available packages at http://pear.php.net. Before you create a tool for a project, you should get into the habit of checking the PEAR site to see if someone has got there first.

Support for PEAR comes bundled with PHP (at least up until the time of this writing), which means that some of the core packages may be available on your system straightaway (unless PHP was compiled to exclude it using the –without-pear configure flag). Packages are installed in a configurable location (on Linux or Unix systems this will often be /usr/local/lib/php You can check this using the pear command line application:

$ pear config-get php_dir
/usr/local/lib/php

The core packages (known as the PEAR Foundation Classes provide a backbone for the wider repository—including core functions such as error handling and the processing of command line arguments.

image Note  If you use a Unix distribution to install PHP, you may begin with a minimal installation. For example, to get PHP and PEAR on Fedora 18, you would need to issue these commands:

sudo yum install php
sudo yum install php-pear

See your distribution’s documentation if you wish to use its package management tools to manage PHP. You have already seen the pear application in action. This is a tool for interacting with all aspects of PEAR, and as such, it is an important part of PEAR in itself. The pear application supports a number of subcommands. We used config-get, which shows the value of a particular configuration setting. You can see all settings and their values with the config-show subcommand:

$ pear config-show
Configuration (channel pear.php.net):
=====================================
Auto-discover new Channels     auto_discover    <not set>
Default Channel                default_channel  pear.php.net
HTTP Proxy Server Address      http_proxy       <not set>
PEAR server [DEPRECATED]       master_server    pear.php.net
Default Channel Mirror         preferred_mirror pear.php.net
Remote Configuration File      remote_config    <not set>
PEAR executables directory     bin_dir          /usr/bin

Phar Out with Pyrus

Before I go any further, I should introduce a  relative newcomer to the PEAR world. Pyrus is the installer associated with PEAR2, an upgraded version of the PEAR repository. Many PEAR packages have been ported to PEAR2. Repository standards include better testing, the use of namespaces and improved error handling. The Pyrus installer is designed to be lighter weight than its older cousin. The name comes from the genus of trees and shrubs that includes the pear tree. Incidentally, Pirus is the Latin for “pear tree.” You can get Pyrus at http://pear2.php.net. The application you will download is a phar package (PHP Archive). If you are familiar with Java, you will recognize a PHP analog to the jar file. A phar is essentially a bundle of code compressed into a zip file. It’s a neat way of passing round library code. After download you can simply run Pyrus like this:

$ php pyrus.phar
Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52
Pyrus: No user configuration file detected
It appears you have not used Pyrus before, welcome!  Initialize install?

We’ll need to tell it where to save a configuration file, and where you want it to save packages.

Although Pyrus has been around for a while (it was designed initially for PHP 5.3.1) the pear application remains the default by virtue of its presence in PHP distributions. For this reason, third-party tools that work with PEAR may not yet be ready for Pyrus. On the other hand, Pyrus is the future. Migrating now may work out cheaper in the long run.

Because there are good reasons to go either way, I’ll try to steer a path between both PEAR and Pyrus in this chapter.

So, to bring you up to date, Pyrus provides the get command which is equivalent to the pear application’s config-show:

$ php pyrus.phar get
Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52
Using PEAR installation found at /usr/share/pear2
System paths:
  php_dir => /usr/share/pear2/php
  ext_dir => /usr/lib/php/modules
  cfg_dir => /usr/share/pear2/cfg

Although both PEAR and Pyrus support many subcommands, you will probably get the most use out of one in particular. install is used for installing PEAR packages.

Installing a Package

Once you have selected your package, you can download and install it with a single command. Here is the process for installing Log, a package that provides enhanced support for error logging:

$ pear install pear/Log

image Note  In most instances, you will need root privileges to install, update, and remove packages with PEAR, because this involves writing to areas of the computer outside of your home space. If you do not have write permissions for these areas, however, all is not lost. In the section “Creating Your Own PEAR Packages,” I describe how to change the default installation locations. This will allow you to install packages within your own writable space.

It really is as simple as that. The PEAR installer is bundled with PHP and locates, downloads, and installs the Log package on your behalf. Here’s the command’s output:

WARNING: "pear/DB" is deprecated in favor of "pear/MDB2"
Did not download optional dependencies: pear/DB, pear/MDB2, pear/Mail, use --alldeps to download automatically
pear/Log can optionally use package "pear/DB" (version >= 1.3)
pear/Log can optionally use package "pear/MDB2" (version >= 2.0.0RC1)
pear/Log can optionally use package "pear/Mail"
pear/Log can optionally use PHP extension "sqlite"
downloading Log-1.12.7.tar ...
Starting to download Log-1.12.7.tar (Unknown size)
............................................................done: 289,280 bytes
install ok: channel://pear.php.net/Log-1.12.7

The Log package has some optional dependencies, which you can safely ignore unless you require functionality associated with the missing packages. Notice the last line. PEAR tells you that it acquired the Log package from the channel pear.php.net . In fact, I specified as much by installing pear/Log rather than just Log. I’ll return to channels shortly.

Pyrus can also install the Log package:

$ php ./pyrus.phar install pear/Log
Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52
Using PEAR installation found at /usr/share/pear2
Downloading pear.php.net/Log   
Mime-type: application/octet-stream
Installed pear.php.net/Log-
1.12.7====================================================================>] 100% (45/45 kb)
Optional dependencies that will not be installed, use --optionaldeps:
pear.php.net/DB depended on by pear.php.net/Log
pear.php.net/MDB2 depended on by pear.php.net/Log
pear.php.net/Mail  depended on by pear.php.net/Log

From here onward, I’ll mention differences between the two systems rather than demonstrate them both in parallel.

If the package you wish to install has compulsory dependencies, the installation will fail with a warning message by default:

pear/dialekt requires package "pear/Fandango" (version >= 10.5.0)
No valid packages found

You can either install the required package before trying again, or you could run pear install with the -o flag.

$ pear install -o dialekt

The -o flag ensures that the PEAR installer will automatically install any required dependencies for you. Some PEAR packages specify optional dependencies, and these are ignored if -o is specified. To have all dependencies installed automatically, use the -a flag instead.

image Note  Pyrus will attempt to install dependencies by default. It supports an -o flag, which will cause optional dependencies to be installed as well.

Although PEAR is designed to talk to a repository over a network , you will find that some developers produce PEAR-compatible packages for ease of installation. You may be given the location of a tarball (a tarred and gzipped package). Installing this using PEAR is almost as easy as installing an official package:

$ pear install -o http://www.example.com/dialekt-1.2.1.tgz
downloading dialekt-1.2.1.tgz ...
Starting to download dialekt-1.2.1.tgz (1,783 bytes)
....done: 1,783 bytes
install ok: channel://pear.php.net/dialekt-1.2.1

You can also download a package and install it from the command line Here, we use a Unix command called wget to fetch the dialekt package before installing it from the command line:

$ wget -nv http://127.0.1.2:8080/dialekt-1.2.1.tgz
20:21:40 URL: http://127.0.1.2:8080/dialekt-1.2.1.tgz [1783/1783]
    -> "dialekt-1.2.1.tgz.1" [1]
$ pear install dialekt-1.2.1.tgz
install ok:channel://pear.example.com/Dialekt-1.2.1

You can install a PEAR package by referencing an XML file (usually named package.xml), which provides information about what files are to be installed where:

$ pear install package.xml

PEAR Channels

PEAR introduced channels in version 1.4. This powerful feature allows you to poll repositories other than pear.php.net for updates and dependencies. This means that you can build an application that requires packages from multiple repositories. PEAR can then handle acquiring dependencies on the user’s behalf. Before the advent of channels, the application developer had to instruct his user to install dependencies or had to bundle them inside his own distribution.

Sebastian Bergmann’s PHPUnit package is a real-world example. In order to install it, we first need PEAR to know about the channel in which it can be found:

$ pear channel-discover pear.phpunit.de
Adding Channel "pear.phpunit.de" succeeded
Discovery of channel "pear.phpunit.de" succeeded

Once you have established communication with this channel using discover, you can refer to a package within it by prefacing its name with phpunit/. In fact, phpunit is just an alias for pear.phpunit.de. You can find out about the alias for a channel by running channel-info:

$ pear channel-info pear.phpunit.de
Channel pear.phpunit.de Information:
====================================
Name and Server         pear.phpunit.de
Alias                   phpunit
Summary                 PHPUnit channel server
...

image Note  Pyrus does not support the channel-info subcommand

Before I install PHPUnit, I must first make another PEAR channel available:

$ pear channel-discover pear.symfony.com
Adding Channel "pear.symfony.com" succeeded
Discovery of channel "pear.symfony.com" succeeded

Now I can go ahead:

$ pear install -a  phpunit/PHPUnit
phpunit/PHP_CodeCoverage can optionally use PHP extension "xdebug" (version >= 2.0.5)
 
phpunit/PHPUnit_MockObject can optionally use PHP extension "soap"
phpunit/PHP_Invoker requires PHP extension "pcntl"
downloading PHPUnit-3.7.22.tar ...
Starting to download PHPUnit-3.7.22.tar (1,085,440 bytes)
....................done: 1,085,440 bytes
downloading File_Iterator-1.3.3.tar ...
Starting to download File_Iterator-1.3.3.tar (30,720 bytes)
...done: 30,720 bytes
downloading Text_Template-1.1.4.tar ...
Starting to download Text_Template-1.1.4.tar (18,944 bytes)
...done: 18,944 bytes
downloading PHP_CodeCoverage-1.2.12.tar ...
Starting to download PHP_CodeCoverage-1.2.12.tar (642,048 bytes)
.............................................done: 642,048 bytes
downloading PHP_Timer-1.0.4.tar ...
Starting to download PHP_Timer-1.0.4.tar (18,944 bytes)
...done: 18,944 bytes
downloading PHPUnit_MockObject-1.2.3.tar ...
Starting to download PHPUnit_MockObject-1.2.3.tar (230,400 bytes)
...done: 230,400 bytes
downloading Yaml-2.3.2.tar ...
Starting to download Yaml-2.3.2.tar (201,216 bytes)
...done: 201,216 bytes
downloading PHP_TokenStream-1.1.5.tar ...
Starting to download PHP_TokenStream-1.1.5.tar (66,048 bytes)
...done: 66,048 bytes
install ok: channel://pear.phpunit.de/File_Iterator-1.3.3
install ok: channel://pear.phpunit.de/Text_Template-1.1.4
install ok: channel://pear.phpunit.de/PHP_Timer-1.0.4
install ok: channel://pear.symfony.com/Yaml-2.3.2
install ok: channel://pear.phpunit.de/PHP_TokenStream-1.1.5
install ok: channel://pear.phpunit.de/PHP_CodeCoverage-1.2.12
install ok: channel://pear.phpunit.de/PHPUnit_MockObject-1.2.3
install ok: channel://pear.phpunit.de/PHPUnit-3.7.22

Notice that I used the -a flag, which asks PEAR to download all dependent packages. This includes Symfony’s Yaml package, and the PHP_TokenStream package among many others.

Using a PEAR Package

Once you have installed a PEAR package you should be able to use it in your projects immediately. Your PEAR directory should already be in your include path—there should be no problem including the package once it has been installed. Let’s install PEAR_Config and any dependencies it might have:

$ pear install -a Config
WARNING: "pear/XML_Parser" is deprecated in favor of "pear/XML_Parser2"
downloading Config-1.10.12.tar ...
Starting to download Config-1.10.12.tar (Unknown size)...........................................done: 208,896 bytesdownloading XML_Parser-1.3.4.tar ...Starting to download XML_Parser-1.3.4.tar (Unknown size).....................done: 90,624 bytesinstall ok: channel://pear.php.net/Config-1.10.12install ok: channel://pear.php.net/XML_Parser-1.3.4

Here’s how you would include the package:

require_once("Config.php");
 
class MyConfig {
    private $rootObj;
 
    function __construct( $filename=null, $type='xml' ) {
        $this->type=$type;
        $conf = new Config();
        if ( ! is_null( $filename ) ) {
            $this->rootObj = $conf->parseConfig($filename, $type);
        } else {
            $this->rootObj = new Config_Container( 'section', 'config' );
            $conf->setroot($this->rootObj);
        }
    }
        
    function set( $secname, $key, $val ) {
        $section=$this->getOrCreate( $this->rootObj, $secname );
        $directive=$this->getOrCreate( $section, $key, $val );
        $directive->setContent( $val );
    }
 
    private function getOrCreate( Config_Container $cont, $name, $value=null ) {
        $itemtype=is_null( $value )?'section':'directive';
        if ( $child = $cont->searchPath( array($name) ) ) {
            return $child;
        }
        return $cont->createItem( $itemtype, $name, null );
    }
 
    function __toString() {
        return $this->rootObj->toString( $this->type );
    }
}

We begin by including Config.php Most PEAR packages work in this way, providing a single top-level point of access. All further require statements are then made by the package itself.

The rest of the example simply works with the classes provided by the Config package: Config and Config_Container The Config package lets you access and create configuration files in a variety of formats. This simple MyConfig class uses Config to work with configuration data. Here’s a quick usage example

$myconf = new MyConfig();
$myconf->set("directories", "prefs", "/tmp/myapp/prefs" );
$myconf->set("directories", "scratch", "/tmp/" );
$myconf->set("general", "version", "1.0" );
echo $myconf;

By default, this generates output in XML format:

<config>
  <directories>
    <prefs>/tmp/myapp/prefs</prefs>
    <scratch>/tmp/</scratch>
  </directories>
  <general>
    <version>1.0</version>
  </general>
</config>

As is often the case with sample code, this class is incomplete—it still requires additional error checking as well as methods for writing the configuration data to file. Still, it is pretty useful already, thanks to the power of the PEAR package. By passing different type strings to Config, we could have rendered the previous output in various configuration formats (like the INI format that the PHP application itself uses, for example). Of course, the details of the Config package are beyond the scope of this chapter. The good news is that for official PEAR packages, you will find API instructions on the web site at http://pear.php.net/. In all cases, you should expect to be able to add the functionality of a PEAR package to your script with minimal effort. The package should provide you with a clear, well-documented API.

image Note  The bad news about PEAR packages is that the struggle to support older versions of PHP is extremely hard to square with the demands of later versions. Like many PEAR packages, Config now relies on deprecated language features, which cannot be easily discarded for the sake of backward compatibility. In order to turn off warnings about this, you can set an error_reporting directive like this:

error_reporting = E_ALL & ∼E_DEPRECATED

in your php.ini file.

Handling PEAR Errors

Many, if not most, official PEAR packages use the standard PEAR error class PEAR_Error This is often returned in place of the expected value if something goes wrong in an operation. This behavior should be documented, and you can test return values using the static PEAR::isError method.

$this->rootObj = @$conf->parseConfig($filename, $type);
if ( PEAR::isError( $this->rootObj ) ) {
   print "message:    ". $this->rootObj->getMessage()   ." ";
   print "code:       ". $this->rootObj->getCode()      ." ";
   print "Backtrace: ";
 
   foreach ( $this->rootObj->getBacktrace() as $caller ) {
       print $caller['class'].$caller['type'];
       print $caller['function']."() ";
       print "line ".$caller['line']." ";
   }
   die;
}

Here, I test the return value from Config::parseConfig():

PEAR::isError( $this->rootObj )

is the functional equivalent of:

$this->rootObj instanceof PEAR_Error

So within my conditional block, I know that $this->rootObj is a PEAR_Error rather than a Config_Container object.

Once I am sure I have a PEAR_Error object, I can interrogate it for more information about the error. In my example, I have three of the most useful methods: getMessage returns a message that describes the error; getCode returns an integer corresponding to the error type (this is an arbitrary number that the package author will have declared as a constant and, we hope, documented); and finally, getBacktrace returns an array of the methods and classes that lead to the error. This enables us to work our way back through our script’s operation and locate the root cause of the error. As you can see, getBacktrace() is itself an array, which describes each method or function that led to the error. The elements are described in Table 15-1.

Table 15-1. Fields Provided by PEAR_Error::getBacktrace()

Field

Description

file Full path to PHP file
args The arguments passed to the method or function
class The name of the class (if in class context)
function The name of the function or method
type If in class context, the nature of the method call (:: or ->)
line The line number

The way that PEAR_Error pollutes a method’s return value was an unfortunate necessity before the advent of PHP 5. With PHP 4 at or near the end of its life, it’s no surprise that PEAR_Error has been deprecated.

Although many packages continue to use PEAR_Error and will probably do so for some time, more are beginning to use PEAR_Exception If you were to use the XML_Feed_Parser package for example, you would be catching exceptions rather than testing return types:

$source="notthere";
try {
    $myfeed = new XML_Feed_Parser( $source );
 
} catch ( XML_Feed_Parser_Exception $e ) {
    print "message: ".     $e->getMessage()      ." ";
    print "code: ".        $e->getCode()         ." ";
    print "error class: ". $e->getErrorClass()   ." ";
    print "error method: ".$e->getErrorMethod()  ." ";
    print "trace: ".       $e->getTraceAsString()." ";
    print "error data: ";
    print_r(               $e->getErrorData() );
}

Typically, a PEAR package will extend PEAR_Exception, partly so that it can add any functionality it needs, but mainly so that you can use your catch clause to distinguish between Exception types PEAR_Exception, of course, itself extends Exception, so you get the standard methods I covered in Chapter 4. You also benefit from some additions. getErrorClass()and getErrorMethod(),for example, tell you the class and method from which the error originated. getErrorData()may include additional error information in an associative array, although this is left for extending classes to implement. Before being thrown to you, a PEAR_Exception object can be initialized with another Exception or with an array of Exception objects. In this way, PEAR packages can wrap Exception objects. You can get at wrapped exceptions by calling PEAR::getCause().This will either return a wrapped Exception object, an array if there is more than one, or null if none are found.

PEAR_Exception also uses the Observer pattern, allowing you to register callback functions or methods that will be called whenever an exception is thrown. First, let’s create some error conditions:

class MyPearException extends PEAR_Exception {
}
 
 
class MyFeedThing {
    function acquire( $source ) {
        try {
            $myfeed = @new XML_Feed_Parser( $source );
            return $myfeed;
        } catch ( XML_Feed_Parser_Exception $e ) {
            throw new MyPearException( "feed acquisition failed", $e );
        }
    }
}

I extend PEAR_Exception and create a simple class that wraps XML_Feed_Parser. If the XML_Feed_Parser constructor throws an exception, I catch it and pass it to the constructor of MyPearException, which I then rethrow. This trick allows me to raise my own error while bundling the root cause.

Here is a client class and a couple of lines of code to invoke it:

class MyFeedClient {
    function __construct() {
        PEAR_Exception::addObserver( array( $this, "notifyError") );
    }
 
    function process() {
        try {
            $feedt = new MyFeedThing();
            $parser = $feedt->acquire('wrong.xml'),
        } catch ( Exception $e ) {
            print "an error occurred. See log for details ";
        }
    }
 
    function notifyError( PEAR_Exception $e ) {
        print get_class( $e ).":";
        print $e->getMessage()." ";
        $cause = $e->getCause();
        if ( is_object( $cause ) ) {
            print "[cause] ".get_class( $cause ).":";
            print $cause->getMessage()." ";
        } else if ( is_array( $cause ) ) {
            foreach( $cause as $sub_e ) {
                print "[cause] ".get_class( $sub_e ).":";
                print $sub_e->getMessage()." ";
            }
        }
        print "---------------------- ";
    }
}
 
$client = new MyFeedClient();
$client->process();

All the usual caveats about sample code apply here, of course—especially because this particular example is designed to fail. First of all, notice the constructor. PEAR_Exception::addObserver()is a static method that accepts a callback, either a function name or an array containing an object reference and a method name. The method or function will be invoked every time a PEAR_Exception is thrown. This trick allows us to design MyFeedClient so that it logs all exceptions.

The process()method passes a nonexistent file to MyFeedThing::acquire(),which passes it on to the XML_Feed_Parser constructor, thereby guaranteeing an error. We catch the inevitable exception and print a simple message. notifyError()is the callback method I referenced in the MyFeedClient constructor. Notice that it expects a PEAR_Exception object. In this case, I simply query the object and print out error information, although in a real-world situation, I would probably send this data to a log. Notice the call to PEAR_Exception::getCause().Because this could return an array or a single Exception object, I handle both cases. If I run this toy code, this is what I get:

XML_Feed_Parser_Exception:Invalid input: this is not valid XML
----------------------
MyPearException:feed acquisition failed
[cause] XML_Feed_Parser_Exception:Invalid input: this is not valid XML
---------------------
an error occurred. See log for details

Our logger method is invoked for both the exceptions thrown by this sample (the first by XML_Feed_Parser, the second by MyFeedThing). The XML_Feed_Parser_Exception object makes a second appearance in the log output because we added it to the MyPearException object as a cause.

Creating Your Own PEAR Package

Packages from the PEAR repository are well documented and designed to be easy to use. How easy are they to create, though, and how do you go about creating your own? In this section, we will look at the anatomy of a PEAR package.

package.xml

The package.xml file is the heart of any PEAR package. It provides information about a package, determines where and how its participants should be installed, and defines its dependencies. Whether it operates on a URL, the local file system, or a tarred and gzipped archive, the PEAR installer needs the package.xml file to acquire its instructions.

No matter how well designed and structured your package is, if you omit the build file, the install will fail. Here’s what happens if you attempt to install an archive that does not contain package.xml:

$ pear install baddialekt.tgz
could not extract the package.xml file from "baddialekt.tgz"
Cannot initialize 'baddialekt.tgz', invalid or missing package file
Package "baddialekt.tgz" is not valid
install failed

The PEAR installer first unpacks our archive to the temporary directory and then looks for package.xml. Here, it falls at the first hurdle. So if package.xml is so important, what does it consist of?

Package Elements

The package file must begin with an XML declaration. All elements are then enclosed by the root package element:

<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.4.11" version="2.0"
 xmlns="http://pear.php.net/dtd/package-2.0"
 xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
 http://pear.php.net/dtd/tasks-1.0.xsd
 http://pear.php.net/dtd/package-2.0
 http://pear.php.net/dtd/package-2.0.xsd">
 
<!-- additional elements here -->
 
</package>

This example would fail with an error. The PEAR installer requires a number of elements to work with. To start with, we must provide overview information:

 <name>Dialekt</name>
 <channel>pear.php.net</channel>
 <summary>A package for translating text and web pages into silly tones of voice</summary>
 <description>Be the envy of your friends with this hilarious dialect
translator. Easy to extend and altogether delightful.
 </description>
 
<!-- additional elements here -->

These new elements should be pretty self-explanatory. The name element defines the handle by which the user will refer to the package. The summary element contains a one-line overview of the package, and description provides a little more detail. All of these elements are compulsory with the exception of channel. If you are not intending to add your package to a channel you can use the uri element instead of channel, and in the same part of the file.. This should contain a URI that points to your package file:

<uri>http://www.example.com/projects/Dialekt-1.2.1 </uri>

The file name should not include an extension, even though the package file itself will likely end with .tgz.

Next, you should provide information about the team behind your package. You should include at least one lead element:

<lead>
 <name>Matt Zandstra</name>
 <user>mattz</user>
 <email>[email protected] </email>
 <active>yes</active>
</lead>

After this, you can define other projects participants in a similar way. Instead of lead, though, you can use developer, contributor, or helper elements. These are designations recognized by the PEAR community, but they should adequately cover most non-PEAR projects, too. The user element refers to the contributor’s user name with PEAR. Most teams use similar handles to allow users to log in to Subversion, a development server, or both.

Before you get to the files in your project, there are a few more details you must provide:

<date>2010-02-13</date>
<time>18:01:44</time>
<version>
 <release>1.2.1</release>
 <api>1.2.1</api>
</version>
<stability>
 <release>beta</release>
 <api>beta</api>
</stability>
<license uri="http://www.php.net/license">PHP License</license>
<notes>initial work
</notes>

Although this is mostly self-explanatory, it’s worth pointing out a couple of features. Of the elements inside version, release is the one that really counts as far as your package is concerned. The release element is used by PEAR in dependency calculations. If another system claims to require Dialekt 1.0.0, and the installing user only has version 0.2.1 on her system, PEAR will halt its installation or attempt to fetch a later version, depending on the mode in which it was run. The api element, on the other hand, is there so that you can keep track of changes in your code’s interface, which may affect compatibility.

The stability element is similarly split between release and api. The value can be one of snapshot, devel, alpha, beta, or stable; you should choose the one that best describes your project.

If you are releasing your package according to specific license terms (such as GNU’s GPL license, for example), you should add this information to the license element.

Unlike summary and description, the notes element will accept line breaks in the contents you add.

The contents Element

Now, we’re finally ready to talk about the files and directories in the package. The contents element defines the files that will be included in the package archive (sometimes called a tarball, because it’s archived with the tar and Gzip tools). You can describe the structure of your archive by combining dir and file elements.

Here’s a simplified example:

<contents>
 <dir name="/">
  <dir name="data">
   <file name="alig.txt" role="data" />
   <file name="dalek.txt" role="data" />
  </dir> <!-- /data -->
  <dir name="Dialekt">
   <file name="AliG.php" role="php" />
   <file name="Dalek.php" role="php" />
  </dir>
</contents>

Every file in a PEAR package has a role. Every role is associated with a default (configurable) location. Table 15-2 describes the common roles.

Table 15-2. Some Common PEAR File Roles

image

When installation takes place, files of role doc, data, and test are not dropped directly into their respective directories. Instead, a subdirectory named after the package is created in the test_dir and data_dir directories, and files are installed into this.

In a PEAR project, everything must have a role, and every role has its place. If you do not have the correct privileges to work with the default role locations, you can set your own locations using the pear command line tool:

$ pear config-set php_dir ∼/php/lib/
$ pear config-set data_dir ∼/php/lib/data/
$ pear config-set bin_dir ∼/php/bin/
$ pear config-set doc_dir ∼/php/lib/doc/
$ pear config-set test_dir ∼/php/lib/test/

image Note  Pyrus uses set rather than config-set for the same purpose.

Now, PEAR will use your directories rather than those described in Table 15-2. Remember that if you do this, you should add the lib directory to your include path: either in the php.ini file, an .htaccess file, or using the ini_set()function in your scripts. You should also ensure that the bin directory is in your shell’s path so that command line commands can be found.

My example revolves around a fictitious package called Dialekt. Here is the package’s directory and file structure:

./package.xml
./data
    ./data/dalek.txt
    ./data/alig.txt
./script
    ./script/dialekt.sh
    ./script/dialekt.bat
./cli-dialekt.php
./Dialekt.php
./Dialekt
    ./Dialekt/AliG.php
    ./Dialekt/Dalek.php

As you can see, I have mirrored some of the standard PEAR roles in my data structure. So I include data and script directories. The top-level directory contains two PHP files. These should be installed in the PEAR directory (/usr/local/php/lib by default). Dialekt.php is designed to be the first port of call for client code. The user should be able to include Dialekt with:

require_once("Dialekt.php");

Additional PHP files (Dalek.php and AliG.php) are stored in a Dialekt directory that will be added to the PEAR directory (these are responsible for the detailed process of translating web pages and text files into oh-so-funny versions of themselves). Dialekt.php will include these on behalf of client code. So that the installed Dialekt package will be callable from the command line, we have included a shell script that will be moved to PEAR’s script directory. Dialekt uses configuration information stored in text files. These will be installed in PEAR’s data directory.

Here’s the full contents tag:

<contents>
 <dir name="/">
  <dir name="data">
   <file name="alig.txt" role="data" />
   <file name="dalek.txt" role="data" />
  </dir> <!-- /data -->
  <dir name="Dialekt">
   <file name="AliG.php" role="php" />
   <file name="Dalek.php" role="php" />
  </dir> <!-- /Dialekt -->
  <dir name="script">
   <file name="dialekt.bat" role="script">
    <tasks:replace from="@php_dir@" to="php_dir" type="pear-config" />
    <tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config" />
    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config" />
   </file>
   <file name="dialekt.sh" role="script">
   <tasks:replace from="@php_dir@" to="php_dir" type="pear-config" />
    <tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config" />
    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config" />
   </file>
  </dir> <!-- /script -->
  <file name="cli-dialekt.php" role="php" />
  <file name="Dialekt.php" role="php">
   <tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config" />
  </file>
 </dir> <!-- / -->
</contents>

I have included a new element in this fragment. The tasks:replace element causes the PEAR installer to search the file for the trigger string given in the from attribute, replacing it with the pear-config setting in the to attribute. So the Dialekt.php file, for example, might start out looking like this:

<?php
/*
 * Use this from PHP scripts, for a CLI implementation use
 * @bin_dir@/dialekt
 */
class Dialekt {
    const DIALEKT_ALIG=1;
    const DIALEKT_DALEK=2;
//...
}

After installation, the same class comment should look something like this:

/*
 * Use this from PHP scripts, for a CLI implementation use
 * /home/mattz/php/bin/dialekt
 */

Dependencies

Although packages are generally stand-alone entities, they often make use of one another. Any use of another package introduces a dependency. If the used package is not present on the user’s system, then the package that uses it will not run as expected.

The dependencies tag is a required element, and within it, you must specify at least the PHP, and PEAR installer versions:

<dependencies>
 <required>
  <php>
   <min>5.3.0</min>
  </php>
  <pearinstaller>
   <min>1.4.1</min>
  </pearinstaller>
  <!-- other dependencies here if required -->
 </required>
</dependencies>

Both php and pearinstall can contain min, max, and exclude elements. exlude defines a version that will be treated as incompatible with the package, and you can include as many of these as you need. The pearinstaller element can also contain a recommended element, in which you can set a preferred installer for the package.

If these or other dependencies within the required element are not satisfied, PEAR will refuse to install the package by default. A package can depend on another package, a PHP extension (such as zlib or GD) or a particular version of PHP. Here, I insist that Dialekt has access to the Fandango package at version 10.5.0 or greater (note, that I add this within the required element):

<package>
 <name>Fandango</name>
 <channel>pear.example.com</channel>
 <min>10.5.0</min>
</package>

Notice the channel element; this specifies where pear should search for the package should it be invoked with the -a flag (which tells it to acquire all dependencies). You must specify either a channel or a uri element. The uri element should point to a package file:

<package>
 <name>Fandango</name>
 <uri>http://www.getinstance.com/packages/fandango-10.5.0.tgz</uri>
</package>

The package element accepts the same dependency specifiers as pearinstaller, with the addition of conflicts in which you can define a version with which this package will not work.

In addition to package you could also specify extension, os, or arch. Table 15-3 summarizes these dependency elements.

Table 15-3. package.xml Dependency Types

Element

Description

php The PHP application
package A PEAR package
extension A PHP extension (a capability compiled into PHP such as zlib or GD)
arch Operating system and processor architecture
os An operating system

Up until now I have specified mandatory dependencies. In fact, after requires, you can specify an optional element. This accepts the same dependency elements. When PEAR encounters an unfilled optional dependency, it will raise a warning but will continue to install nonetheless. You should add depencies to the optional element where your package can limp along adequately without the preferred package or extension.

If the user runs the pear install command with the -o flag:

$ pear install -o package.xml

then PEAR will attempt to download and install all unmet required dependencies (remember, though that passing -o to pyrus means that it will install optional requrements). Running the command with the -a flag also automates the download of dependencies but will take in optional as well as required packages.

Tweaking Installation with phprelease

Although you define the files in a package archive with the contents element, you can use phprelease to fine tune the files that are actually installed onto the users system. Here are the two phprelease elements in our package:

<phprelease>
<installconditions>
 <os>
  <name>unix</name>
 </os>
</installconditions>
<filelist>
 <install as="dialekt" name="script/dialekt.sh" />
 <install as="dalek" name="data/dalek.txt" />
 <install as="alig" name="data/alig.txt" />
 <ignore name="script/dialekt.bat" />
</filelist>
</phprelease>
<phprelease>
<installconditions>
 <os>
  <name>windows</name>
 </os>
</installconditions>
<filelist>
 <install as="dialekt" name="script/dialekt.bat" />
 <install as="dalek" name="data/dalek.txt" />
 <install as="alig" name="data/alig.txt" />
 <ignore name="script/dialekt.sh" />
</filelist>
</phprelease>

The installconditions element can be used to determine the phprelease element that is executed. It accepts the specifier elements os, extension, arch, and php. These elements work in the same way as their dependency namesakes. As well as providing phprelease elements qualified by installconditions, you can provide a default version to be executed if none of the others are matched.

Let’s focus on the unix phprelease. The install element specifies that the file dialekt.sh should be renamed dialekt on installation.

I specify that my data files should be installed without the .txt suffix. I do not need to specify the dialekt subdirectory—this is automatically included for files with a data role. Note that the install element’s as element also strips out the leading directory data that we specified in the contents element for these files. This means that they are installed as <data_dir>/dialekt/dalek and <data_dir>/dialekt/alig.

Note also that in Unix mode I don’t want to install the dialekt.bat script file. The ignore element takes care of that. All being well our package is ready to install locally.

Preparing a Package for Shipment

Now that I have created my package and created a package.xml file,, it is time to generate an archived and compressed product.

There is a single PEAR command to achieve this. We ensure we are in the root directory of our project and run this subcommand:

$ pear package package.xml
Analyzing Dialekt/AliG.php
Analyzing Dialekt/Dalek.php
Analyzing cli-dialekt.php
Analyzing Dialekt.php
Package Dialekt-1.2.1.tgz done

This will generate a tarred and gzipped archive (including all referenced files as well as the package.xml file itself) suitable for distribution. You can make this available for straight download. If you have dependencies between packages, you can reference URIs in your package elements and use the uri element in place of channel. If you are offering many interdependent packages to your users, though, perhaps you should consider taking things to the next level.

Setting Up Your Own Channel

Why set up your own channel? Aside from the sheer coolness of such a thing, the main benefits lie in PEAR’s automatic dependency management and the consequent ease of installation and upgrade for your users. It’s easy enough for a user to install a single package using a full path to a tarball URL. If you have designed a library system in tiers working from low-level utility packages to high-level applications, things become more complicated. It can be a real pain for users to manage multiple interdependent packages on their systems, especially as they evolve.

In order to create and host your own channel you will ideally have:

  • Root access to your web host computer
  • Administrative access to a web server (probably Apache) and the ability to support a subdomain (pear.yourserver.com, for example)

If you do not have this kind of control over your server, don’t worry, you can host your channel with a third-party provider such as Github (https://github.com/). However you decide to host it, first of all you will need to define your channel and add some packages to it.

Defining a Channel with Pirum Pirum is a third party tool for creating and managing a simple PEAR channel. You can find its website at http://pirum.sensiolabs.org/. Once you have set up a directory to house your pear repository (such as /var/www/pear/htdocs) and a URL that points to it (such as http://pear.example.com/), you are ready to install Pirum.  As you might expect the easiest way to do this is to use PEAR.

$ pear channel-discover pear.pirum-project.org
Adding Channel "pear.pirum-project.org" succeeded
 
Discovery of channel "pear.pirum-project.org" succeeded
$ pear install pirum/Pirum
downloading Pirum-1.1.5.tgz ...
Starting to download Pirum-1.1.5.tgz (15,658 bytes)
 
......done: 15,658 bytes

Pirum requires that you place a configuration file named pirum.xml at the top level of your PEAR directory. This file simply identifies the channel name, its alias, and URL:

<server>
  <name>pear.example.com</name>
  <summary>getinstance PEAR channel</summary>  <alias>getinstance</alias>
  <url>http://pear.example.com/</url>
</server>

With this file in place I can go ahead and create the channel:

$ pirum build /var/www/pear/htdocs/
Running the build command:
   INFO   Parsing package
   INFO   Building channel.
   INFO   Building maintainers.
   INFO   Building categories.
   INFO   Building packages.
   INFO   Building composer repository.
   INFO   Building releases.
   INFO   Building index.
   INFO   Building feed.
   INFO   Updating PEAR server files.
   INFO   Command build run successfully.

And that’s it. I have a PEAR channel! Of course, I’ve yet to add a package to it. Before I do that, though, I’d better check that it can be discovered by a remote host.

$ pear channel-discover pear.example.com
INFO   Updating PEAR server files.
Adding Channel "pear.example.com" succeeded
Discovery of channel "pear.example.com" succeeded

So I can discover my new channel. I can also confirm that it’s up and running by checking the simple web page that Pirum created for me. Figure 15-1 shows this channel front end.

9781430260318_Fig15-01.jpg

Figure 15-1. The Channel’s Web Page

Adding a Package to a Channel

Adding my Dialekt package to the repository. Is really just a matter of a command-line

$ pirum add /var/www/pear/htdocs/ Dialekt-1.2.1.tgz
...
Running the add command:
   INFO   Parsing package Dialekt 1.2.1
   INFO   Building channel.
   INFO   Building maintainers.
   INFO   Building categories.
   INFO   Building packages.
   INFO   Building package Dialekt.
   INFO   Building composer repository.
   INFO   Building releases.
   INFO   Building releases for Dialekt.
   INFO   Building release 1.2.1
   INFO   Building index.
   INFO   Building feed.
   INFO   Updating PEAR server files.
   INFO   Command add run successfully.

And that really is it! I now have a pear channel containing a package. Because I have already used channel-discover with the pear.example.com channel I can use its alias to install Dialekt:

$ pear install getinstance/dialekt
downloading Dialekt-1.2.1.tgz ...
Starting to download Dialekt-1.2.1.tgz (1,780 bytes)
....done: 1,780 bytes
install ok: channel://pear.localhost/Dialekt-1.2.1

I can also confirm that my repository contains Dialekt by looking at the web page. You can see the the updated channel page in Figure 15-2.

9781430260318_Fig15-02.jpg

Figure 15-2. The Channel Frontend Page Showing a Package

image Note  If you don’t want to manage your own PEAR channel, you might consider using a third-party repository such as OpenPear ( http://openpear.org/).

Summary

PEAR is extensive almost by definition, and I have only had space to provide an introduction here. Nevertheless, you should leave this chapter with a sense of how easy it is to leverage PEAR packages to add power to your projects. Through the package.xml file,  the PEAR installer (and Pyrus, its future replacement), you can also make your code accessible to other users. By setting up a channel, you can automate dependency downloads for your users and allow third-party packages to use yours without the need for bundling or complicated dependency management.

PEAR is best suited for relatively self-enclosed packages with well-defined functionality. For larger applications, other build solutions come into their own. We will be looking at Phing, a powerful tool for building applications, later in the book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.161.193