diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d72793..4c90c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +1.2.11 +---- +* Add new class `SugarQueryIterator` + 1.2.10 ---- * Make PDO parameters available publicly diff --git a/README.md b/README.md index af8e94f..c061ad5 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,23 @@ $inetSugarUtils = new Utils(EntryPoint::getInstance()); $convertedArray = $inetSugarUtils->arrayToMultiselect(array('test' => 'inet')); echo $convertedArray; ``` + +## Inet\SugarCRM\SugarQueryIterator +Iterator class to iterate in a memory safe way SugarQuery results. + +Usage Example: +Loop records 100 to 300 +```php +setStartOffset(100); +foreach ($iter as $id => $bean) { + // Do something with $bean + if ($iter->getIterationCounter() >= 200) { + break; + } +} +``` diff --git a/src/SugarQueryIterator.php b/src/SugarQueryIterator.php new file mode 100644 index 0000000..6e3a5ed --- /dev/null +++ b/src/SugarQueryIterator.php @@ -0,0 +1,163 @@ + 5.6 + * SugarCRM Versions 6.5 - 7.6 + * + * @author Emmanuel Dyan + * @copyright 2005-2018 iNet Process + * + * @package inetprocess/sugarcrm + * + * @license Apache License 2.0 + * + * @link http://www.inetprocess.com + */ + +namespace Inet\SugarCRM; + +/** + * Iterator class to iterate in a memory safe way SugarQuery results. + * Usage: + * $query = new \SugarQuery(); + * // setup $query + * + * foreach (new SugarQueryIterator($query) as $id => $bean) { + * // Do something with $bean + * } + */ +class SugarQueryIterator implements \Iterator +{ + protected $retrieve_params; + protected $query; + protected $use_fixed_offset; + protected $start_offset = 0; + protected $clean_memory_on_fetch = true; + + protected $query_offset = 0; + + protected $results_cache = array(); + protected $cache_current_index = 0; + + protected $iteration_counter = 0; + + /** + * $query A sugarCRM query to fetch record ids. + * $retrieve_params Parameters passed to \BeanFactory::retrieveBean on each iteration + */ + public function __construct(\SugarQuery $query, $retrieve_params = array()) + { + $this->query = $query; + $this->retrieve_params = $retrieve_params; + // Set query parameters + $this->query->select(array('id')); + $this->setPaginationSize(100); + } + + /** + * Start the iteration at $offset + */ + public function setStartOffset($offset) + { + $this->start_offset = $offset; + } + + public function getStartOffset() + { + return $this->start_offset; + } + + /** + * Fetch only $size records at a time from the database + */ + public function setPaginationSize($size) + { + $this->query->limit($size); + } + + /** + * Set to true if the results are modified during the iteration + * in a way that they are not return on the next query call. + * This way the iterator keep quering the same first records hoping + * eventually the query returns no results. + * Be carefull when setting to true as infinite loop are really easy to create. + */ + public function useFixedOffset($fixed_offset) + { + $this->use_fixed_offset = $fixed_offset; + } + + /** + * Should the iterator try to free some memory + * before fetching new results. + */ + public function setCleanMemoryOnFetch($value) + { + $this->clean_memory_on_fetch = $value; + } + + /** + * Return the number of iteration. Starts at 1. + */ + public function getIterationCounter() + { + return $this->iteration_counter; + } + + /** + * Iterator interface. + */ + public function current() + { + $module = $this->query->getFromBean()->module_name; + return \BeanFactory::retrieveBean($module, $this->key(), $this->retrieve_params); + } + + public function key() + { + return $this->results_cache[$this->cache_current_index]['id']; + } + + public function next() + { + $this->cache_current_index++; + $this->query_offset++; + $this->iteration_counter++; + if ($this->cache_current_index > (count($this->results_cache) - 1)) { + $this->fetchNextRecords(); + } + } + + public function rewind() + { + $this->cache_current_index = 0; + $this->results_cache = array(); + $this->query_offset = $this->getStartOffset(); + $this->iteration_counter = 1; + $this->fetchNextRecords(); + } + + public function valid() + { + return !empty($this->results_cache); + } + + /** + * Fetch the next page of ids from the database + */ + protected function fetchNextRecords() + { + if (!$this->use_fixed_offset) { + $this->query->offset($this->query_offset); + } + if ($this->clean_memory_on_fetch) { + // Attempt to clean php memory + $this->results_cache = null; + BeanFactoryCache::clearCache(); + gc_collect_cycles(); + } + $this->results_cache = $this->query->execute(); + $this->cache_current_index = 0; + } +} diff --git a/tests/SugarQueryIteratorTest.php b/tests/SugarQueryIteratorTest.php new file mode 100644 index 0000000..efbe14f --- /dev/null +++ b/tests/SugarQueryIteratorTest.php @@ -0,0 +1,85 @@ +getEntryPointInstance()->setCurrentUser('1'); + + $this->query = new \SugarQuery(); + $this->query->from(\BeanFactory::newBean('Accounts')); + } + + public function testRewindValid() + { + $iter = new SugarQueryIterator($this->query); + $iter->rewind(); + $this->assertTrue($iter->valid()); + } + + public function testFetchAllAccounts() + { + $this->query->limit(10); + $results = $this->query->execute(); + + $iter = new SugarQueryIterator($this->query); + $iter->setPaginationSize(5); + $i = 0; + foreach ($iter as $id => $bean) { + $i++; + $this->assertEquals($i, $iter->getIterationCounter()); + $this->assertInternalType('string', $id); + $this->assertInstanceOf('Account', $bean); + $this->assertEquals($id, $bean->id); + if ($iter->getIterationCounter() >= 10) { + break; + } + } + $this->assertGreaterThan(0, $i); + $this->assertEquals(count($results), $i); + } + + public function testStartOffset() + { + $this->query->limit(10); + $this->query->select(array('id')); + $results = $this->query->execute(); + + $iter = new SugarQueryIterator($this->query); + $iter->setPaginationSize(5); + $iter->setStartOffset(5); + + $iter_results = array(); + foreach ($iter as $key => $bean) { + $iter_results[] = array('id' => $key); + if ($iter->getIterationCounter() >= 5) { + break; + } + } + $this->assertEquals(array_slice($results, 5), $iter_results); + } + + public function testFixedOffset() + { + $iter = new SugarQueryIterator($this->query); + $iter->useFixedOffset(true); + $iter->setCleanMemoryOnFetch(false); + $iter->setPaginationSize(1); + + $iter->rewind(); + $this->assertTrue($iter->valid()); + $bean_id = $iter->key(); + $iter->next(); + $this->assertTrue($iter->valid()); + $bean2_id = $iter->key(); + + $this->assertEquals($bean_id, $bean2_id); + } +}