Accessing package.xml properties with Pyrus

Introduction

If you are not familiar with the package.xml format, you will need to read The description of the file.

This guide is divided into several segments.

  • If you want to access basic properties such as the name of the package, the channel, summary, description, license, version, stability, date, time, or notes, read the section on Accessing basic properties

  • To learn about how to manage maintainers of the package, read the section on Accessing package maintainers.

  • To learn about how to manage and iterate over dependencies, read the section on Accessing dependencies.

  • To understand how to iterate over and add files to both the contents and release sections of package.xml, read the section on Accessing files

  • To learn about managing custom tasks, roles, and compatible packages, read the section on usesrole, usestask and compatible.

  • To learn about accessing PECL-specific properties such as configureoption, srcpackage, srcuri and providesextension, read the section on Accessing PECL properties

The package.xml file is represented using a PEAR2\Pyrus\PackageFile\v2 object, but most of the time you will access it using the most packagefile version-agnostic PEAR2\Pyrus\Package class, which supports the same API seamlessly for accessing any kind of package as well as a few higher level options such as directly accessing the source of a file in a package. This documentation assumes you are working with a PEAR2\Pyrus\Package object named $package.

To instantiate a PEAR2\Pyrus\Package, simply pass in the path to a local package, a URI of a remote package, or an abstract remote package name to the constructor.

<?php
$package 
= new PEAR2\Pyrus\Package('package.xml');
$package = new PEAR2\Pyrus\Package('Release-1.2.3.tgz');
$package = new PEAR2\Pyrus\Package('http://example.com/Release-1.2.3.tgz');
$package = new PEAR2\Pyrus\Package('pear2/Release-1.2.3');
?>

Accessing basic properties

Basic properties can be accessed as if they were object properties:

<?php
echo "Package name is "$package->name"\n";
echo 
"Package channel is "$package->channel"\n";
echo 
"Package summary is "$package->summary"\n";
echo 
"Package description is "$package->description"\n";
echo 
"Package license name is "$package->license['name'], "\n";
if (
$package->license['uri']) {
    echo 
"Package license URI is "$package->license['uri'], "\n";
}
if (
$package->license['filesource']) {
    echo 
"Package license path within the package.xml is "$package->license['filesource'], "\n";
}
echo 
"Package release version is "$package->version['release'], "\n";
echo 
"Package API version is "$package->version['api'], "\n";
echo 
"Package release stability is "$package->stability['release'], "\n";
echo 
"Package API stability is "$package->stability['api'], "\n";
echo 
"Package release date in YYYY-MM-DD format is "$package->date"\n";
if (
$package->time) {
    echo 
"Package release time in HH:MM:SS format is "$package->time"\n";
}
echo 
"Package release notes is "$package->notes"\n";
?>

By the same token, changing these properties is as simple as setting an object property:

<?php
$package
->name 'MyPackage';
// this must be full channel, not a shortcut like "pear2"
$package->channel 'pear2.php.net';
$package->summary "My package's short description";
$package->description "My package's multi-line description
of how things are done with it."
;

$package->license 'BSD License';
// or
$package->license['name'] = 'BSD License';
$package->license['uri'] = 'http://www.opensource.org/licenses/bsd-license.php';
$package->license['filesource'] = 'LICENSE';

$package->version['release'] = '1.2.3';
$package->version['api'] = '1.0.0';

$package->stability['release'] = 'stable';
$package->version['api'] = 'stable';

$package->date '2009-12-25';
$package->time '00:00:01';

$package->notes "This version of MyPackage has the blessing of the pope, so
it's way better than the last one."
;
?>

Accessing package maintainers

Maintainers are accessed through the maintainer property. An of maintainers organized by maintainer role can be obtained via the allmaintainers property, but it is also possible to iterate over the maintainer property to iterate over all maintainers.

The maintainer property acts as an array organized by developer handle. To learn more about maintainers in package.xml, see the concepts section on maintainers in the user guide.

An individual maintainer's properties are set via a fluent method call. The order of method call is not important, but a call to the role() method is required in order to properly save the data.

<?php
// setting a maintainer's properties
$package->maintainer['cellog']
    ->
role('lead')
    ->
name('Gregory Beaver')
    ->
email('cellog@php.net')
    ->
active('yes');

// access a maintainer's properties
echo "Maintainer cellog's name is ",
     
$package->maintainer['cellog']->name"\n"// Gregory Beaver
echo "Maintainer cellog's role is ",
     
$package->maintainer['cellog']->role"\n"// lead
echo "Maintainer cellog's handle is ",
     
$package->maintainer['cellog']->user"\n"// cellog
echo "Maintainer cellog's email is ",
     
$package->maintainer['cellog']->email"\n"// cellog@php.net
echo "Is maintainer cellog active? ",
     
$package->maintainer['cellog']->active"\n"// yes
?>

The maintainers can be iterated over as well, and even modified:

<?php
foreach ($package->maintainer as $handle => $maintainer) {
    echo 
"Maintainer $handle's name is ",
         
$maintainer->name"\n";
    echo 
"Maintainer $handle's role is ",
         
$maintainer->role"\n";
    echo 
"Maintainer $handle's handle is ",
         
$maintainer->user"\n";
    echo 
"Maintainer $handle's email is ",
         
$maintainer->email"\n";
    echo 
"Is maintainer $handle active? ",
         
$maintainer->active"\n";
    
$maintainer->active('no'); // shows modifying a setting in iteration
}
?>

The allmaintainers property returns an associative array of arrays of maintainer objects indexed by maintainer role.

Accessing dependencies

Before it is possible to understand how Pyrus provides access to dependencies, it is necessary to understand their format in package.xml, which is documented here.

Pyrus provides access to dependencies through the dependencies property. In addition to supporting access to and creation of individual dependencies, it is possible to iterate over all dependencies, or over just required dependencies, or just a particular dependency type.

Each of the dependency types (package, subpackage, pearinstaller, extension, php, arch, and os) is accessed slightly differently based on how they are used in package.xml.

Perhaps the best introduction is 3 examples highlighting the different methods of accessing dependency properties. First, an example highlighting the methods for reading existing dependency properties:

<?php
echo "First, required/optional dependencies:\n";

// these same principles apply to all properties for dependencies.
// isset() is used to test presence, ->get to retrieve.

echo "Minimum PHP version supported: ",
     
$package->dependencies['required']->php->min"\n";

echo 
"Minimum PEAR Installer/Pyrus version required: ",
     
$package->dependencies['required']->pearinstaller->min"\n";
if (isset(
$package->dependencies['required']->pearinstaller->max)) {
    echo 
"Maximum PEAR Installer/Pyrus version supported: ",
         
$package->dependencies['required']->pearinstaller->max"\n";
}

echo 
"PHP extension ext is required? "
     
isset($package->dependencies['required']->extension['ext'])
         ? 
"Yes\n" "No\n";

if (isset(
$package->dependencies['required']->os['windows'])) {
    echo 
"Conflicts with Windows operating system? "
         
$package->dependencies['required']->os['windows']->conflicts
             
"Yes\n" "No\n";
}

if (isset(
$package->dependencies['required']->arch['*386*'])) {
    echo 
"Requires i386 processor? "
         
$package->dependencies['required']->arch['*386*']->conflicts
             
"No\n" "Yes\n";
}

echo 
"Now for package and subpackage dependencies:\n";

echo 
"Is package foo from channel gronk.example.com required? ",
     isset(
$package->dependencies['required']->package['gronk.example.com/foo'])
         ? 
"Yes\n" "No\n";

echo 
"Does this package conflict with our package? ",
     
$package->dependencies['required']->package['gronk.example.com/foo']->conflicts
         
"Yes\n" "No\n";

echo 
"gronk.example.com/foo2 minimum version required is ",
     
$package->dependencies['required']->subpackage['gronk.example.com/foo2']->min"\n";

echo 
"gronk.example.com/foo2 incompatible versions:";
foreach (
$package->dependencies['required']->subpackage['gronk.example.com/foo2']->exclude
         
as $version) {
    echo 
"\n "$version;
}

echo 
"\n";

echo 
"Optional dependencies are accessed the same way with optional index\n";

echo 
"Optional dependency gronk.example.com/foo3 minimum version allowed is ",
     
$package->dependencies['optional']->subpackage['gronk.example.com/foo3']->min"\n";

echo 
"Dependency groups are accessed with the group index\n";

echo 
"Dependency group foo hint is ",
     
$package->dependencies['group']->foo->hint"\n";

echo 
"channel pear2.php.net package Foo package dependency in dependency group",
     
" foo minimum version is ",
     
$package->dependecies['group']->foo->package['pear2.php.net/Foo']->min"\n";

// and so on...
?>

Iteration is similar to the access, in that each dependency object is the same object as that when accessed using the full access syntax:

<?php
// display all required and optional package and subpackage dependencies
foreach (array('required''optional') as $required) {
    foreach (array(
'package''subpackage') as $packagedep) {
        foreach (
$package->dependencies[$required]->$packagedep as $dep) {
            echo 
$required' '$packagedep' 'dependency,
                 
$dep->channel'/'$dep->name;
            if (isset(
$dep->uri)) {
                
// the channel is "__uri" for all URI dependencies
                
echo "\nPackage URI: "$dep->uri;
            }
            if (isset(
$dep->min)) {
                echo 
"\nMinimum version: "$dep->min;
            }
            if (isset(
$dep->recommended)) {
                echo 
"\nRecommended installation version: "$dep->recommended;
            }
            echo 
"\n";
        }
    }
}

// iterating over dependency groups and their contents
foreach ($package->dependencies['group'] as $group) {
    echo 
"Optional dependency group "$group->name,
         
" ("$group->hint")\n";
    foreach (
$group->extension as $ext) {
        echo 
"Extension "$ext->name;
    }
    foreach (array(
'package''subpackage') as $type) {
        foreach (
$group->$type as $dep) {
            echo 
ucfirst($type), ' '$dep->channel'/'$dep->name;
        }
    }
}
?>

Finally, setting dependencies can be accomplished in several ways.

<?php
// setting minimum PHP version required:
$package->dependencies['required']->php '5.3.0';
$package->dependencies['required']->php->min '5.3.0';
$package->dependencies['required']->php->min('5.3.0');

// setting minimum PEAR Installer/Pyrus version required:
$package->dependencies['required']->pearinstaller '2.0.0a1';
$package->dependencies['required']->pearinstaller->min '2.0.0a1';
$package->dependencies['required']->pearinstaller->min('2.0.0a1');

// setting excluded versions (all deps supporting the <exclude> tag use this syntax):
$package->dependencies['required']->php->exclude '5.2.0';
$package->dependencies['required']->php->exclude('5.2.0')->exclude('5.2.10');
$package->dependencies['required']->php->exclude('5.2.0''5.2.10');

// setting OS/architecture requirements:
$package->dependencies['required']->os['windows'] = true;  // Windows required
$package->dependencies['required']->arch['386'] = false// Conflicts with 386 architecture

// saying "this package must be installed" to the installer
$package->dependencies['required']->package['pear2.php.net/Packagename']->save();

// saying "this package must not be installed"
$package->dependencies['required']->package['pear2.php.net/Packagename']->conflicts();

// setting up complex versioning requirements
$package->dependencies['optional']->package['pear2.php.net/Packagename']
    ->
min('1.2.0')
    ->
max('1.3.0')
    ->
recommended('1.2.3')
    ->
exclude('1.3.0''1.2.2');

// depending on a PECL package
$package->dependencies['required']->package['pecl.php.net/lua']
    ->
providesextension('lua');

// optionally depending on a core PHP extension
$package->dependencies['optional']->extension['PDO']->save();

// setting up a dependency group "remotefeatures"
$package->dependencies['group']->remotefeatures->hint('Remote management capabilities');
// add dependencies to the group
$package->dependencies['group']->remotefeatures
    
->package['pear2.php.net/Foo']->save();
$package->dependencies['group']->remotefeatures
    
->extension['curl']->save();
?>

Accessing files

There are two reasons to access the files within a package.xml

  1. iterating over contents

  2. adding new files/setting properties

Iterating over package.xml contents

Pyrus supports several use cases for iterating over package.xml:

  • Iterating for installation purposes
  • Iterating for packaging purposes
  • Iterating for traversing a deep package.xml tree
  • General-purpose iteration

All of these use cases are illustrated in the following example:

<?php
// *************** installation iteration ***************
foreach ($package->installcontents as $file) {
    
// retrieve actual file path to be used with fopen
    
$path $package->getFilePath($file->packagedname);
    
// retrieve open file pointer
    
$fp $package->getFileContents($file->packagednametrue);
    
// retrieve file contents as string
    
$contents $package->getFileContents($file->packagedname);

    
// get file properties (XML attributes of the <file> tag)
    
$role $file->role;
    if (
$file->md5sum) {
        
$md5sum $file->md5sum;
    }
    
// get relative path of file after applying all modifications such
    // as <install name="" as=""/>
    
$relativepath $file->name;
    
// get the path as specified in the <contents> <file> tag
    // This is the same as $file->name if no <install as> tags exist
    
$originalpath $file->packagedname;

    
// get the tasks associated with this file
    
$tasks file->tasks;
    
// iterate over the tasks
    
$lastversion '1.0.0'// last version of $package that was installed, or null
    
foreach (new \PEAR2\Pyrus\Package\Creator\TaskIterator($tasks$package,
                                                          \
PEAR2\Pyrus\Task\Common::INSTALL$lastversion)
              as 
$name => $task) {
        
// $name is the task name, $task is the actual task object, which can be
        // processed with $task->startSession(resource $fp, '/full/final/install/path');
    
}
}

// *************** packaging iteration ***************
$packagingloc '/tmp/example';
foreach (
$package->packagingcontents as $packageat => $info) {
    
// $packageat is the location in which the file will reside in package.xml,
    // which is often the same relative path it will end up being installed into
    // $info is an array representing the actual file XML
    
$name $info['attribs']['name'];
    
// this retrieves an open file pointer to the file in order to process it
    
$contents $package->getFileContents($info['attribs']['name'], true);
    if (!
file_exists(dirname($packagingloc DIRECTORY_SEPARATOR $packageat))) {
        
mkdir(dirname($packagingloc DIRECTORY_SEPARATOR $packageat), 0777true);
    }
    
$fp fopen($packagingloc DIRECTORY_SEPARATOR $packageat'wb+');
    
ftruncate($fp0);
    
// copy from the source directory to the packaging location
    
stream_copy_to_stream($contents$fp);
    
fclose($contents);
    
rewind($fp);
    
$lastversion '1.0.0'// last version of $package that was installed, or null
    
foreach (new \PEAR2\Pyrus\Package\Creator\TaskIterator($info$package,
                                                          \
PEAR2\Pyrus\Task\Common::PACKAGE,
                                                          
$lastversion) as $task) {
        
// do pre-processing of file contents
        
try {
            
$task->startSession($fp$packageat);
        } catch (\
Exception $e) {
            
// handle task processing problems here
        
}
    }
    
fclose($fp);
}

// *************** traversing deep tree iteration ***************
foreach ($package->contents as $file) {
    
// retrieve actual file path to be used with fopen
    
$path $package->getFilePath($file->name);
    
// retrieve open file pointer
    
$fp $package->getFileContents($file->nametrue);
    
// retrieve file contents as string
    
$contents $package->getFileContents($file->name);

    
// get file properties (XML attributes of the <file> tag)
    
$role $file->role;
    if (
$file->md5sum) {
        
$md5sum $file->md5sum;
    }

    
// unlike installcontents, <install as> tags are not applied, so
    // $file->name == $file->packagedname
    
$relativepath $file->name;
}

// *************** general purpose iteration ***************
foreach ($package->files as $relativepath => $infoarray) {
    
// $relativepath is the same as $file->name in the other examples
    // $infoarray is the same array returned by the packagingcontents
    // iterator
}

// *************** retrieving post-install scripts ***************
foreach ($package->scriptfiles as $file) {
    
// $file is the same object returned by the installcontents iterator
}
?>

Adding new files, changing file properties

Finally, adding files to package.xml is done by directly setting an index in the files property. Properties are manipulated with the setFileAttribute() method.

<?php
$package
->files['path/to/file.php'] = array(
    
'role' => 'php'
);
$package->setFileAttribute('path/to/file.php''md5sum'md5('hi'));

// adding a task to a file
$replace $package->getTask('path/to/file.php''replace');
$replace->setReplacement('package-info''@API-VER@''api-version');
$package->setFileAttribute('path/to/file.php''tasks:replace'$replace);
?>

Working with <release> sections

Releases are documented here, you should familiarize yourself with this before continuing.

Release properties can be accessed with the following syntax:

<?php
// setting release type
$package->type 'php';
$package->type 'extsrc';
$package->type 'zendextsrc';
$package->type 'bin';
$package->type 'zendextbin';
$package->type 'bundle';

// for types other than bundle
$package->release[0]->installconditions['php']->min('5.2.0');
// defaults to "min"
$package->release[0]->installconditions['php'] = '5.2.0';

// defaults to "pattern"
$package->release[0]->installconditions['arch'] = 'i386';
$package->release[0]->installconditions['arch']->pattern('i386')->conflicts();

// defaults to "name"
$package->release[0]->installconditions['os'] = 'windows';

// defaults to existing
$package->release[0]->installconditions['extension'][0]->name('PDO');
$package->release[0]->installconditions['extension'][0]->name('PDO')->min('1.0');
$package->release[0]->ignore('path/to/file.ext');
$package->release[0]->installAs('path/to/anotherfile.ext''new/name.php');

// add another release
$i count($package->release);
$package->release[$i]->ignore('path/to/anotherfile.ext');
$package->release[$i]->installAs('path/to/file.ext''new/name.php');

// remove release
unset($package->release[1]);

// remove all releases
$package->release null;

// retrieve the release that will be used on this system
$release $package->installrelease;
?>

Accessing PECL properties

Accessing PECL properties is relatively simple, here is an example illustrating them:

<?php
$package
->type 'extsrc'// extension source release
$package->configureoption['foothing']->prompt 'Value for thing';
$package->configureoption['with-zlib']->prompt('Support zlib compression?')->default('yes');

$package->providesextension 'foo';

$package->type 'extbin'// extension binary package release
$package->srcpackage 'pecl.php.net/foo';
// for packages that are binary releases of URI-based packages
$package->srcuri 'http://example.com/foo-1.2.3';
?>

Accessing usesrole, usestask and compatible tags

When declaring that a package.xml uses a custom role or task, this code should be used:

<?php
$package
->files['example/file.php'] = array(
    
'role' => 'myrole',
    
'tasks:mytask' => '',
);

$package->usesrole['myrole']->package('MyRole')->channel('mypear.example.com');
$package->usestask['mytask']->package('MyTask')->channel('mypear.example.com');

// for uri-based packages:
$package->usesrole['myrole']->uri('http://my.example.com/MyRole-1.2.3');
?>

If you are unfamiliar with the <compatible> tag, you should first read the documentation on its use here. Creating and accessing compatible tags in Pyrus is as easy as:

<?php
$package
->compatible['pear.php.net/Archive_Tar']
        ->
min('1.2')
        ->
max('1.3.0')
        ->
exclude('1.2.1''1.2.2');

// remove a compatibility declaration
unset($package->compatible['pear.php.net/Archive_Tar']);

// test for existence of compatible declaration
isset($package->compatible['pear.php.net/Archive_Tar']);

// display info:
echo $package->compatible['pear.php.net/Archive_Tar']->min;

// iterating over compatible packages
foreach ($package->compatible as $package => $info) {
    echo 
"Package :"$package"\n";
    echo 
"min: "$info->min"\n";
    echo 
"max: "$info->max"\n";
    if (isset(
$info->exclude)) {
        echo 
"Excludes versions ",
        foreach (
$info->exclude as $version) {
            echo 
' '$version"\n";
        }
    }
}
?>