From 3daf4954884f5f6b24ec03b03c4205e9b4966ea9 Mon Sep 17 00:00:00 2001 From: the_undefined Date: Fri, 14 Mar 2008 01:05:56 +0000 Subject: [PATCH] Added initial XPath support for Set::extract git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@6567 3807eeeb-6ff5-0310-8944-8be069107fe0 --- cake/libs/set.php | 127 ++++++++++++++++++++++++- cake/tests/cases/libs/set.test.php | 145 +++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 4 deletions(-) diff --git a/cake/libs/set.php b/cake/libs/set.php index c545f854e..2158672bc 100644 --- a/cake/libs/set.php +++ b/cake/libs/set.php @@ -359,6 +359,125 @@ class Set extends Object { } return $out; } +/** + * undocumented function + * + * @param string $path + * @param string $data + * @param string $options + * @return void + * @author Felix + */ + + function extract($path, $data = null, $options = array()) { + if (is_array($path) || empty($data)) { + return Set::classicExtract($path, $data); + } + $contexts = $data; + $options = am(array('flatten' => true), $options); + if (!isset($contexts[0])) { + $contexts = array($data); + } + if (is_string($path)) { + $last = substr($path, -1, 1); + if ($last == '*' && substr($path, -2, 1) != '/') { + $path = substr($path, 0, -1); + } else { + $last = false; + } + $tokens = array_slice(explode('/', $path), 1); + } + do { + $token = array_shift($tokens); + $conditions = false; + if (preg_match_all('/\[([^\]]+)\]/', $token, $m)) { + $conditions = $m[1]; + $token = substr($token, 0, strpos($token, '[')); + } + + $matches = array(); + foreach ($contexts as $i => $context) { + if (!isset($context['trace'])) { + $context = array('trace' => array(), 'item' => $context, 'key' => null); + } + if ($token == '..') { + $context['item'] = Set::extract(join('/', $context['trace']), $data); + $context['key'] = array_pop($context['trace']); + $context['item'] = $context['item'][0][$context['key']]; + $matches[] = $context; + continue; + } + if (array_key_exists($token, $context['item']) && (!$conditions || Set::matches($conditions, $context['item'][$token], $i+1))) { + $context['trace'][] = $context['key']; + $context['key'] = $token; + $context['item'] = $context['item'][$token]; + $matches[] = $context; + } + } + if (empty($tokens)) { + break; + } + $contexts = $matches; + } while(1); + + $r = array(); + foreach ($matches as $match) { + if (!$options['flatten'] || is_array($match['item'])) { + $r[] = array($match['key'] => $match['item']); + } else { + $r[] = $match['item']; + } + } + return $r; + } +/** + * This function can be used to see if a single item or a given xpath match certain conditions. + * + * @param mixed $conditions An array of condition strings + * @param array $data + * @param integer $i Optional: The 'nth'-number of the item being matched. + * @return boolean + * @author Felix + */ + + function matches($conditions, $data = array(), $i = null) { + if (empty($conditions)) { + return true; + } + if (is_string($conditions)) { + return !!Set::extract($conditions, $data); + } + foreach ($conditions as $condition) { + if (!preg_match('/(.+?)([><])(.+)/', $condition, $match)) { + if (ctype_digit($condition)) { + if ($i != $condition) { + return false; + } + } elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) { + return in_array($i, $matches[0]); + } elseif (!array_key_exists($condition, $data)) { + return false; + } + continue; + } + list(,$key,$op,$expected) = $match; + $val = $data[$key]; + if ($op == '=' && $val != $expected) { + return false; + } elseif ($op == '!=' && $val == $expected) { + return false; + } elseif ($op == '>' && $val <= $expected) { + return false; + } elseif ($op == '<' && $val >= $expected) { + return false; + } elseif ($op == '<=' && $val > $expected) { + return false; + } elseif ($op == '>=' && $val < $expected) { + return false; + } + } + return true; + } /** * Gets a value from an array or object that is contained in a given path using an array path syntax, i.e.: * "{n}.Person.{[a-z]+}" - Where "{n}" represents a numeric key, "Person" represents a string literal, @@ -370,7 +489,7 @@ class Set extends Object { * @return array Extracted data * @access public */ - function extract($data, $path = null) { + function classicExtract($data, $path = null) { if ($path === null && is_a($this, 'set')) { $path = $data; $data = $this->get(); @@ -405,7 +524,7 @@ class Set extends Object { if (empty($tmpPath)) { $tmp[] = $val; } else { - $tmp[] = Set::extract($val, $tmpPath); + $tmp[] = Set::classicExtract($val, $tmpPath); } } } @@ -417,7 +536,7 @@ class Set extends Object { if (empty($tmpPath)) { $tmp[] = $val; } else { - $tmp[] = Set::extract($val, $tmpPath); + $tmp[] = Set::classicExtract($val, $tmpPath); } } } @@ -431,7 +550,7 @@ class Set extends Object { if (empty($tmpPath)) { $tmp[$j] = $val; } else { - $tmp[$j] = Set::extract($val, $tmpPath); + $tmp[$j] = Set::classicExtract($val, $tmpPath); } } } diff --git a/cake/tests/cases/libs/set.test.php b/cake/tests/cases/libs/set.test.php index 6772d097f..85cc00fcb 100644 --- a/cake/tests/cases/libs/set.test.php +++ b/cake/tests/cases/libs/set.test.php @@ -236,6 +236,151 @@ class SetTest extends UnitTestCase { } function testExtract() { + $a = array( + array( + 'Article' => array('id' => '1', 'user_id' => '1', 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'), + 'User' => array('id' => '1', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'), + 'Comment' => array( + array('id' => '1', 'article_id' => '1', 'user_id' => '2', 'comment' => 'First Comment for First Article', 'published' => 'Y', 'created' => '2007-03-18 10:45:23', 'updated' => '2007-03-18 10:47:31'), + array('id' => '2', 'article_id' => '1', 'user_id' => '4', 'comment' => 'Second Comment for First Article', 'published' => 'Y', 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'), + ), + 'Tag' => array( + array('id' => '1', 'tag' => 'tag1', 'created' => '2007-03-18 12:22:23', 'updated' => '2007-03-18 12:24:31'), + array('id' => '2', 'tag' => 'tag2', 'created' => '2007-03-18 12:24:23', 'updated' => '2007-03-18 12:26:31') + ), + 'Deep' => array( + 'Nesting' => array( + 'test' => array( + 1 => 'foo', + 2 => array( + 'and' => array('more' => 'stuff') + ) + ) + ) + ) + ), + array( + 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'), + 'User' => array('id' => '2', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'), + 'Comment' => array(), + 'Tag' => array() + ), + array( + 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'), + 'User' => array('id' => '3', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'), + 'Comment' => array(), + 'Tag' => array() + ), + array( + 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'), + 'User' => array('id' => '4', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'), + 'Comment' => array(), + 'Tag' => array() + ), + array( + 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'), + 'User' => array('id' => '5', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'), + 'Comment' => array(), + 'Tag' => array() + ) + ); + $b = array('Deep' => $a[0]['Deep']); + $c = array( + array( + 'a' => array( + 'I' => array( + 'a' => 1 + ) + ) + ), + array( + 'a' => array( + 2 + ) + ), + array( + 'a' => array( + 'II' => array( + 'a' => 3, + 'III' => array( + 'a' => array('foo' => 4) + ) + ) + ) + ), + ); + + $expected = array( + $c[0], $c[0]['a']['I'], $c[1], $c[2], array('a' => $c[2]['a']['II']['a']), $c[2]['a']['II']['III'] + ); + + $expected = array(1,2,3,4,5); + $r = Set::extract('/User/id', $a); + $this->assertEqual($r, $expected); + + $expected = array(array('id' => 1), array('id' => 2), array('id' => 3), array('id' => 4), array('id' => 5)); + $r = Set::extract('/User/id', $a, array('flatten' => false)); + $this->assertEqual($r, $expected); + + $expected = array(array('test' => $a[0]['Deep']['Nesting']['test'])); + $this->assertEqual(Set::extract('/Deep/Nesting/test', $a), $expected); + $this->assertEqual(Set::extract('/Deep/Nesting/test', $b), $expected); + + $expected = array(array('test' => $a[0]['Deep']['Nesting']['test'])); + $r = Set::extract('/Deep/Nesting/test/1/..', $a); + $this->assertEqual($r, $expected); + + $expected = array(array('test' => $a[0]['Deep']['Nesting']['test'])); + $r = Set::extract('/Deep/Nesting/test/2/and/../..', $a); + $this->assertEqual($r, $expected); + + $expected = array(array('test' => $a[0]['Deep']['Nesting']['test'])); + $r = Set::extract('/Deep/Nesting/test/2/../../../Nesting/test/2/..', $a); + $this->assertEqual($r, $expected); + + $expected = array(2); + $r = Set::extract('/User[2]/id', $a); + $this->assertEqual($r, $expected); + + $expected = array(4, 5); + $r = Set::extract('/User[id>3]/id', $a); + $this->assertEqual($r, $expected); + + $expected = array(2, 3); + $r = Set::extract('/User[id>1][id<=3]/id', $a); + $this->assertEqual($r, $expected); + } +/** + * undocumented function + * + * @return void + * @author Felix + */ + + function testMatches() { + $a = array( + array('Article' => array('id' => 1, 'title' => 'Article 1')), + array('Article' => array('id' => 2, 'title' => 'Article 2')), + array('Article' => array('id' => 3, 'title' => 'Article 3'))); + + $this->assertTrue(Set::matches(array('id=2'), $a[1]['Article'])); + $this->assertFalse(Set::matches(array('id>2'), $a[1]['Article'])); + $this->assertTrue(Set::matches(array('id>=2'), $a[1]['Article'])); + $this->assertTrue(Set::matches(array('id>1'), $a[1]['Article'])); + $this->assertTrue(Set::matches(array('id>1', 'id<3', 'id!=0'), $a[1]['Article'])); + + $this->assertTrue(Set::matches(array('3'), null, 3)); + $this->assertTrue(Set::matches(array('5'), null, 5)); + + $this->assertTrue(Set::matches(array('id'), $a[1]['Article'])); + $this->assertTrue(Set::matches(array('id', 'title'), $a[1]['Article'])); + $this->assertFalse(Set::matches(array('non-existant'), $a[1]['Article'])); + + $this->assertTrue(Set::matches('/Article[id=2]', $a)); + $this->assertFalse(Set::matches('/Article[id=4]', $a)); + } + + function testClassicExtract() { $a = array( array('Article' => array('id' => 1, 'title' => 'Article 1')), array('Article' => array('id' => 2, 'title' => 'Article 2')),