Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Changes since the last development release:

- Completely re-coded for PHP 5 (implements ArrayAccess, Countable, Iterator)

- Added regex-based begin(), end(), and between() methods

- Added relativeTo() and cd() methods to allow traversal of path strings

- Wrote unit tests

git-svn-id: http://svn.php.net/repository/pear/packages/Text_PathNavigator/trunk@288521 c90b9560-bf6c-de11-be94-00142212c4b1
  • Loading branch information...
commit f78c79f2a9b25d49af09031556c34943f87ad4c4 1 parent c7654e6
Denny Shimkoski authored
Showing with 563 additions and 186 deletions.
  1. +399 −186 Text/PathNavigator.php
  2. +59 −0 Text/package.xml
  3. +105 −0 Text/tests/PathNavigatorTest.php
View
585 Text/PathNavigator.php
@@ -1,186 +1,399 @@
-<?php
-/**
- * Provides convenient access to path substrings
- *
- * PHP versions 4 and 5
- *
- * @category Text
- * @package PathNavigator
- * @author Denny Shimkoski <denny@bytebrite.com>
- * @copyright 2006 Denny Shimkoski
- * @license http://www.opensource.org/licenses/mit-license.html MIT
- * @link http://pear.php.net/package/Text_PathNavigator
- */
-
-/**
- * The Text_PathNavigator class provides convenient access to "/path/sub/strings".
- *
- * These strings can be accessed in various ways:
- *
- * 1. by numeric index (e.g., index 1 of /pages/staff.html returns "staff.html")
- * 2. by variable name (e.g., index "id" of /pages/id/1 returns 1)
- * 3. using the slice() method
- * 4. using the toArray() method
- * 5. in PHP 5, using the getIterator() method
- *
- * When retrieving values by variable name, it is assumed that the entry
- * following the first occurence of the given string is the value
- * to be assigned.
- *
- * Example 1.
- * <code>
- * require_once 'Text/PathNavigator.php';
- * $p = new Text_PathNavigator('/the/golden/path', '/');
- * echo $p->get(2); // prints 'path'
- * echo $p->get('golden'); // prints 'path'
- * echo $p->slice(1); // prints 'golden/path'
- * echo $p->slice(-2, 1); // prints 'golden'
- * echo $p->slice(-1); // prints 'path'
- * </code>
- *
- * @category Text
- * @package PathNavigator
- * @author Denny Shimkoski <denny@bytebrite.com>
- * @copyright 2006 Denny Shimkoski
- * @license http://www.opensource.org/licenses/mit-license.html MIT
- * @link http://pear.php.net/package/Text_PathNavigator
- */
-class Text_PathNavigator
-{
- /**
- * @var string
- */
- var $path;
- /**
- * @var string
- */
- var $slash;
- /**
- * Path after explode()
- *
- * @var array
- */
- var $vars;
- /**
- * Constructor
- *
- * @param $path array (e.g., array('test'=>'ok', 2, 3))
- * or string (e.g., '/test/ok/2/3')
- * @param $slash
- * @return Text_PathNavigator
- */
- function Text_PathNavigator($path, $slash = DIRECTORY_SEPARATOR)
- {
- $this->slash = $slash;
- $this->load($path);
- }
- /**
- * Loads a new path into the object
- *
- * @return false|array
- * @access public
- */
- function load($path)
- {
- $slash = $this->slash;
- if (is_array($path)) {
- foreach ($path as $i => $var) {
- $vars[] = is_int($i)
- ? $var
- : "$i$slash$var";
- }
- $path = implode($slash, $vars);
- }
- $path = str_replace(array('\\', '/'), $slash, $path);
- $this->path = substr($path, 0, 1) == $slash
- ? substr($path, 1)
- : $path;
- $this->vars = empty($this->path)
- ? false
- : explode($slash, $this->path);
- return $this->vars;
- }
- /**
- * Returns substrings by index or variable name
- *
- * If $key appears to be a numeric index, attempts to return
- * the corresponding substring (i.e., get(1) returns "val"
- * from "/key/val"). Otherwise, searches path for a substring
- * that matches $key and attempts to return the following entry.
- * If the following entry is non-existent, null is returned.
- * Returns false as a last resort.
- *
- * @param string $key the numeric index or variable name
- * @return null|false|string
- * @access public
- */
- function get($key)
- {
- if (is_int($key)) {
- return $this->slice($key, 1);
- }
- if (($id = array_search($key, $this->vars)) !== false) {
- return isset($this->vars[$id + 1])
- ? $this->vars[$id + 1]
- : null;
- }
- return false;
- }
- /**
- * Returns an ArrayIterator, starting at offset (PHP 5 only)
- *
- * @param int $offset the starting position
- * @return null|object
- * @access public
- */
- function getIterator($offset = 0)
- {
- if (class_exists('ArrayIterator')) {
- $slice = array_slice($this->vars, $offset);
- if (is_array($slice)) {
- return new ArrayIterator($slice);
- }
- }
- return null;
- }
- /**
- * Returns path as an associative array, starting at offset
- *
- * Maps '/a/hello/b/world' to array('a' => 'hello', 'b' => 'world').
- *
- * @param int $offset the starting position
- * @return false|array
- * @access public
- */
- function toArray($offset = 0)
- {
- $path = $offset ? $this->slice($offset) : $this->path;
- $path = explode($this->slash, $path);
- while ($var = current($path)) {
- $val = ($nextVal = next($path)) !== false
- ? $nextVal
- : null;
- $arr[$var] = $val;
- if ($val) next($path);
- }
- return isset($arr) ? $arr : false;
- }
- /**
- * Returns a slice from the path (see PHP's slice function for details)
- *
- * @param integer $offset
- * @param integer $length
- * @return false|string
- * @access public
- */
- function slice($offset, $length = null)
- {
- $a = $length
- ? array_slice($this->vars, $offset, $length)
- : array_slice($this->vars, $offset);
- if (is_array($a)) return implode($this->slash, $a);
- return false;
- }
-}
-
-?>
+<?php
+
+/**
+ * Facilitates navigation of path strings
+ *
+ * PHP version 5
+ *
+ * @category Text
+ * @package PathNavigator
+ * @author Denny Shimkoski <bytebrite@gmail.com>
+ * @copyright 2006-2009 Denny Shimkoski
+ * @license http://www.opensource.org/licenses/mit-license.html MIT
+ * @version SVN: $Id$
+ * @link http://pear.php.net/package/Text_PathNavigator
+ */
+
+require_once 'PEAR.php';
+
+/**
+ * Text_PathNavigator
+ *
+ * @category Text
+ * @package PathNavigator
+ * @author Denny Shimkoski <bytebrite@gmail.com>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/Text_PathNavigator
+ */
+
+class Text_PathNavigator implements ArrayAccess, Countable, Iterator
+{
+ /**
+ * Directory separator, i.e, forward or backward slash
+ *
+ * @var string
+ */
+ protected $slash;
+ /**
+ * Normalized path string
+ *
+ * @var string
+ */
+ protected $path;
+ /**
+ * Normalized path after explode() on $slash
+ *
+ * @var array
+ */
+ protected $segments = array();
+ /**
+ * Current path segment index (Iterator implementation)
+ *
+ * @var int
+ */
+ protected $currentIdx = 0;
+
+ /**
+ * Constructs a new Text_PathNavigator object
+ *
+ * @param array|string $path Path string or array of path segments
+ * @param string $slash character used to separate path segments
+ *
+ * @return string
+ * @access public
+ */
+ public function __construct($path, $slash = DIRECTORY_SEPARATOR)
+ {
+ $this->slash = $slash;
+ $this->path = $this->normalizePath($path);
+ if ($this->path !== null) {
+ $this->segments = explode($this->slash, $this->path);
+ }
+ }
+ /**
+ * Returns path string
+ *
+ * @return string
+ * @access public
+ */
+ public function __toString()
+ {
+ return implode($this->slash, $this->segments);
+ }
+ /**
+ * Removes all leading and trailing slashes from given path string/segment array.
+ *
+ * @param array|string $path Path string or array of path segments
+ *
+ * @return string
+ * @access protected
+ */
+ protected function normalizePath($path)
+ {
+ if (is_array($path)) {
+ $path = implode($this->slash, $path);
+ }
+
+ $offset = 0;
+ $len = strlen($path);
+
+ // trim leading slashes
+ while ($path{$offset} == $this->slash) {
+ if (++$offset == $len) {
+ return null;
+ }
+ }
+
+ // trim trailing slashes
+ while ($path{$len - 1} == $this->slash) {
+ --$len;
+ }
+
+ return substr($path, $offset, $len - $offset);
+ }
+ /**
+ * Slices current path and returns it as a new Text_PathNavigator object
+ *
+ * @param int $offset If offset is non-negative, the new path will start
+ at that offset in the path segment array.
+ If offset is negative, the new path will start
+ that far from the end of the path segment array.
+ * @param int $length If length is given and is positive, the new path
+ will have that many segments in it. If length is given
+ and is negative, the new path will stop that many
+ segments from the end of the path segment array.
+ If it is omitted, the new path will have everything
+ from offset up until the end of the path segment array.
+ *
+ * @return PEAR_Text_PathNavigator
+ * @access public
+ */
+ public function slice($offset = 0, $length = null)
+ {
+ // PHP gets all confused if you pass a null into the $length param
+ // of array_slice(). hence...
+ $segments = $length
+ ? array_slice($this->segments, $offset, $length)
+ : array_slice($this->segments, $offset);
+
+ $path = implode($this->slash, $segments);
+
+ return new Text_PathNavigator($path, $this->slash);
+ }
+ /**
+ * Returns path substring following the given regular expression
+ *
+ * @param string $pattern regular expression to use for matching
+ *
+ * @return Text_PathNavigator
+ * @access public
+ */
+ public function after($pattern)
+ {
+ $path = null;
+ if (preg_match("!$pattern!", $this->path, $matches, PREG_OFFSET_CAPTURE)) {
+ $path = substr($this->path, $matches[0][1] + strlen($matches[0][0]));
+ }
+ return new Text_PathNavigator($path, $this->slash);
+ }
+ /**
+ * Returns path substring preceding the given regular expression
+ *
+ * @param string $pattern regular expression to use for matching
+ *
+ * @return Text_PathNavigator
+ * @access public
+ */
+ public function before($pattern)
+ {
+ $path = null;
+ if (preg_match("!$pattern!", $this->path, $matches, PREG_OFFSET_CAPTURE)) {
+ $path = substr($this->path, 0, $matches[0][1]);
+ }
+ return new Text_PathNavigator($path, $this->slash);
+ }
+ /**
+ * Returns path substring between the given regular expressions $start and $end
+ *
+ * @param string $start regular expression
+ * @param string $end regular expression
+ *
+ * @return Text_PathNavigator
+ * @access public
+ */
+ public function between($start, $end)
+ {
+ return $this->after($start)->before($end);
+ }
+ /**
+ * Returns this path relative to another one. Assumes both paths are absolute.
+ *
+ * If located in a path $p2, we could navigate to this one
+ * using $p2->cd($this->relativeTo($p2))
+ *
+ * @param mixed $path new path will be expressed in relation to this
+ *
+ * @return Text_PathNavigator
+ * @access public
+ */
+ public function relativeTo($path)
+ {
+ // given path can also be a string or an array of segments,
+ // although Text_PathNavigator is preferred
+ if (!$path instanceof Text_PathNavigator) {
+ $path = new Text_PathNavigator($path, $this->slash);
+ }
+
+ // array to hold new path segments
+ $_segments = array();
+
+ // make a copy of path segments for shift operation
+ $segments = $this->segments;
+
+ // enable shift
+ $shift = true;
+
+ // for each segment in the given path....
+ foreach ($path as $i => $segment) {
+ // this effectively truncates any "absolute" portion of the two paths,
+ // i.e., beginning segments shared in common
+ if ($shift && isset($this->segments[$i]) && $this->segments[$i] == $segment) {
+ array_shift($segments);
+ } else {
+ // escape from the remaining elements of the given path
+ $_segments[] = '..';
+ // shifting no longer allowed since a differing segment was encountered
+ $shift = false;
+ }
+ }
+
+ // now just tack on any segments left over from the shift operation...
+ foreach ($segments as $segment) {
+ $_segments[] = $segment;
+ }
+
+ // ...and return a new Text_PathNavigator object
+ return new Text_PathNavigator($_segments, $this->slash);
+ }
+ /**
+ * Navigates from this path to another using the specified relative path.
+ *
+ * @param mixed $path relative path
+ *
+ * @return Text_PathNavigator
+ * @access public
+ */
+ public function cd($path)
+ {
+ // given path can also be a string or an array of segments
+ // (probably a string, i.e., '../../some/dir')
+ if (!$path instanceof Text_PathNavigator) {
+ $path = new Text_PathNavigator($path, $this->slash);
+ }
+
+ // make a copy of path segments for pop operation
+ $_segments = $this->segments;
+
+ // for each segment in the given path....
+ foreach ($path as $segment) {
+ // navigate up and down the hierarchy
+ if ($segment == '..') {
+ array_pop($_segments);
+ } else {
+ $_segments[] = $segment;
+ }
+ }
+
+ // ...and return a new Text_PathNavigator object
+ return new Text_PathNavigator($_segments, $this->slash);
+ }
+ /**
+ * Maps path segments to variable names given in $template.
+ * Suitable for use with PHP's extract() function.
+ *
+ * @param string $template e.g., '/controller/method/'
+ *
+ * @return array
+ * @access public
+ */
+ public function map($template)
+ {
+ $map = explode($this->slash, $template);
+ foreach ($map as $i => $key) {
+ $segments[$key] = isset($this->segments[$i]) ? $this->segments[$i] : null;
+ }
+ return $segments;
+ }
+ /**
+ * Check if a particular segment exists (ArrayAccess implementation)
+ *
+ * @param int $i path segment index
+ *
+ * @return boolean
+ * @access public
+ */
+ public function offsetExists($i)
+ {
+ return isset($this->segments[$i]);
+ }
+ /**
+ * Get a particular segment (ArrayAccess implementation)
+ *
+ * @param int $i path segment index
+ *
+ * @return string
+ * @access public
+ */
+ public function offsetGet($i)
+ {
+ return $this->segments[$i];
+ }
+ /**
+ * Throws Exception for the purpose of immutability (ArrayAccess implementation)
+ *
+ * @param int $i path segment index
+ *
+ * @throws Exception for the purpose of immutability
+ * @return void
+ * @access public
+ */
+ public function offsetUnset($i)
+ {
+ throw new PEAR_Exception('Text_PathNavigator is immutable');
+ }
+ /**
+ * Throws Exception for the purpose of immutability (ArrayAccess implementation)
+ *
+ * @param int $i path segment index
+ * @param string $val path segment
+ *
+ * @throws Exception for the purpose of immutability
+ * @return void
+ * @access public
+ */
+ public function offsetSet($i, $val)
+ {
+ throw new PEAR_Exception('Text_PathNavigator is immutable');
+ }
+ /**
+ * Returns the number of path segments (Countable implementation)
+ *
+ * @return int
+ * @access public
+ */
+ public function count()
+ {
+ return count($this->segments);
+ }
+ /**
+ * Returns current path segment (Iterator implementation)
+ *
+ * @return string
+ * @access public
+ */
+ public function current()
+ {
+ return $this->segments[$this->currentIdx];
+ }
+ /**
+ * Returns index of current path segment (Iterator implementation)
+ *
+ * @return int
+ * @access public
+ */
+ public function key()
+ {
+ return $this->currentIdx;
+ }
+ /**
+ * Advances to next path segment (Iterator implementation)
+ *
+ * @return void
+ * @access public
+ */
+ public function next()
+ {
+ ++$this->currentIdx;
+ }
+ /**
+ * Rewinds to first path segment (Iterator implementation)
+ *
+ * @return void
+ * @access public
+ */
+ public function rewind()
+ {
+ $this->currentIdx = 0;
+ }
+ /**
+ * Checks if current path segment index is valid (Iterator implementation)
+ *
+ * @return bool
+ * @access public
+ */
+ public function valid()
+ {
+ return isset($this->segments[$this->currentIdx]);
+ }
+}
View
59 Text/package.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.7.0RC2" 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">
+ <name>Text_PathNavigator</name>
+ <channel>pear.php.net</channel>
+ <summary>Facilitates navigation of path strings</summary>
+ <description>The Text_PathNavigator class facilitates navigation of path strings.
+ </description>
+ <lead>
+ <name>Denny Shimkoski</name>
+ <user>denny</user>
+ <email>denny@bytebrite.com</email>
+ <active>yes</active>
+ </lead>
+ <date>2009-09-21</date>
+ <time>00:15:55</time>
+ <version>
+ <release>0.1.0dev2</release>
+ <api>0.1.0dev2</api>
+ </version>
+ <stability>
+ <release>devel</release>
+ <api>devel</api>
+ </stability>
+ <license>MIT License</license>
+ <notes>Changes since the last development release:
+
+- Completely re-coded for PHP 5 (implements ArrayAccess, Countable, Iterator)
+
+- Added regex-based begin(), end(), and between() methods
+
+- Added relativeTo() and cd() methods to allow traversal of path strings
+
+- Wrote unit tests
+ </notes>
+ <contents>
+ <dir baseinstalldir="Text" name="/">
+ <file role="php" name="PathNavigator.php">
+ <tasks:replace from="@package_version@" to="version" type="package-info" />
+ </file>
+ <dir name="tests">
+ <file role="test" name="PathNavigatorTest.php" />
+ </dir>
+ </dir> <!-- / -->
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.0.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.5.4</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease />
+</package>
View
105 Text/tests/PathNavigatorTest.php
@@ -0,0 +1,105 @@
+<?php
+
+require_once 'PHPUnit/Framework.php';
+require_once 'Text/PathNavigator.php';
+
+class PathNavigatorTest extends PHPUnit_Framework_TestCase {
+ function setUp() {
+ $this->p1 = new Text_PathNavigator('/files/client1/files/woot', '/');
+ $this->p2 = new Text_PathNavigator('/files/client2/files/woot', '/');
+ $this->byNumSegs = array(
+ 0 => '//////////',
+ 1 => '/////one////',
+ 2 => 'two/segments',
+ 3 => '/three/segment/path/',
+ 4 => '///path/with/four/segments////'
+ );
+ }
+ function testArrayAccessImpl() {
+ $path = new Text_PathNavigator($this->byNumSegs[0], '/');
+ $this->assertEquals(null, $path[0]);
+ $path = new Text_PathNavigator($this->byNumSegs[1], '/');
+ $this->assertEquals('one', $path[0]);
+ $path = new Text_PathNavigator($this->byNumSegs[2], '/');
+ $this->assertEquals('two', $path[0]);
+ $this->assertEquals('segments', $path[1]);
+ $path = new Text_PathNavigator($this->byNumSegs[3], '/');
+ $this->assertEquals('three', $path[0]);
+ $this->assertEquals('segment', $path[1]);
+ $this->assertEquals('path', $path[2]);
+ $path = new Text_PathNavigator($this->byNumSegs[4], '/');
+ $this->assertEquals('path', $path[0]);
+ $this->assertEquals('with', $path[1]);
+ $this->assertEquals('four', $path[2]);
+ $this->assertEquals('segments', $path[3]);
+ }
+ function testCountableImpl() {
+ foreach ($this->byNumSegs as $count => $path) {
+ $path = new Text_PathNavigator($path, '/');
+ $this->assertEquals($count, count($path));
+ }
+ }
+ function testNullPathsAreEqual() {
+ $this->assertEquals(new Text_PathNavigator(null), new Text_PathNavigator(null));
+ }
+ function testNullPathToStringIsEmpty() {
+ $nullPath = (string)new Text_PathNavigator(null);
+ $this->assertEquals(true, empty($nullPath));
+ }
+ function testNullPathsWithDifferentSlashesAreNotEqual() {
+ $this->assertNotEquals(new Text_PathNavigator(null, '/'), new Text_PathNavigator(null, '\\'));
+ }
+ function testExtraSlashesAreTrimmed() {
+ $this->assertEquals('path/sub', (string)new Text_PathNavigator('///path/sub//', '/'));
+ }
+ function testArrayAccess() {
+ $uri = new Text_PathNavigator('/users/edit/1', '/');
+ $this->assertEquals('users', $uri[0]);
+ $this->assertEquals('edit', $uri[1]);
+ }
+ function testP1RelativeToP2IsCorrect() {
+ $this->assertEquals('../../../client1/files/woot', (string)$this->p1->relativeTo($this->p2));
+ }
+ function testGreedyAfterMatch() {
+ $this->assertEquals('woot', (string)$this->p1->after('f.*s'));
+ }
+ function testNonGreedyAfterMatch() {
+ $this->assertEquals('client1/files/woot', (string)$this->p1->after('f.*?s'));
+ }
+ function testPositiveSlice() {
+ $this->assertEquals('client1/files/woot', (string)$this->p1->slice(1));
+ }
+ function testNegativeSlice() {
+ $this->assertEquals('files/woot', (string)$this->p1->slice(-2));
+ }
+ function testBetweenWithNegativeSlice() {
+ $this->assertEquals('files', (string)$this->p1->between('files', 'woot')->slice(-1));
+ }
+ function testMapCanSkipSegments() {
+ $uri = new Text_PathNavigator('/base/accounts/users/edit/1', '/');
+ extract($uri->map('//controller/method/id'));
+ $this->assertEquals('users', $controller);
+ $this->assertEquals('edit', $method);
+ $this->assertEquals('1', $id);
+ }
+ function testSegmentsAreListable() {
+ $uri = new Text_PathNavigator('/users/edit/1', '/');
+ list($controller, $method, $id) = iterator_to_array($uri);
+ $this->assertEquals('users', $controller);
+ $this->assertEquals('edit', $method);
+ $this->assertEquals('1', $id);
+ }
+ function testCdFromP1ToParentIsCorrect() {
+ $this->assertEquals('files/client1/files', (string)$this->p1->cd('..'));
+ }
+ function testCdFromP1BeyondParentsIsNullPath() {
+ $this->assertEquals(new Text_PathNavigator(null, '/'), $this->p1->cd('../../../..'));
+ }
+ function testCdFromP1ToP2IsRelative() {
+ $this->assertEquals("$this->p1/$this->p2", (string)$this->p1->cd($this->p2));
+ }
+ function testCdFromP1ToP2RelativeToP1GivesP2() {
+ $p2RelativeToP1 = $this->p2->relativeTo($this->p1);
+ $this->assertEquals($this->p2, $this->p1->cd($p2RelativeToP1));
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.