Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concatenation adapter #239

Merged
merged 7 commits into from
Oct 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,19 @@ $results = array(/* ... */);
$adapter = new FixedAdapter($nbResults, $results);
```

### ConcatenationAdapter

Concatenates the results of other adapter instances into a single adapter.
It keeps the order of sub adapters and the order of their results.

```php
<?php

use Pagerfanta\Adapter\ConcatenationAdapter;

$superAdapter = new ConcatenationAdapter(array($adapter1, $adapter2 /* ... */));
```

## Views

Views are to render pagerfantas, this way you can reuse your
Expand Down
126 changes: 126 additions & 0 deletions src/Pagerfanta/Adapter/ConcatenationAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Pagerfanta\Adapter;

use Pagerfanta\Exception\InvalidArgumentException;

/**
* Adapter that concatenates the results of other adapters.
*
* @author Surgie Finesse <finesserus@gmail.com>
*/
class ConcatenationAdapter implements AdapterInterface
{
/**
* @var AdapterInterface[] List of adapters
*/
protected $adapters;

/**
* @var int[]|null Cache of the numbers of results of the adapters. The indexes correspond the indexes of the
* `adapters` property.
*/
protected $adaptersNbResultsCache;

/**
* @param AdapterInterface[] $adapters
* @throws InvalidArgumentException
*/
public function __construct(array $adapters)
{
foreach ($adapters as $index => $adapter) {
if (!($adapter instanceof AdapterInterface)) {
throw new InvalidArgumentException(sprintf(
'Argument $adapters[%s] expected to be a \Pagerfanta\Adapter\AdapterInterface instance, a %s given',
$index,
is_object($adapter) ? sprintf('%s instance', get_class($adapter)) : gettype($adapter)
));
}
}

$this->adapters = $adapters;
}

/**
* {@inheritdoc}
*/
public function getNbResults()
{
if (!isset($this->adaptersNbResultsCache)) {
$this->refreshAdaptersNbResults();
}

return array_sum($this->adaptersNbResultsCache);
}

/**
* {@inheritdoc}
* @return array
*/
public function getSlice($offset, $length)
{
if (!isset($this->adaptersNbResultsCache)) {
$this->refreshAdaptersNbResults();
}

$slice = array();
$previousAdaptersNbResultsSum = 0;
$requestFirstIndex = $offset;
$requestLastIndex = $offset + $length - 1;

foreach ($this->adapters as $index => $adapter) {
$adapterNbResults = $this->adaptersNbResultsCache[$index];
$adapterFirstIndex = $previousAdaptersNbResultsSum;
$adapterLastIndex = $adapterFirstIndex + $adapterNbResults - 1;

$previousAdaptersNbResultsSum += $adapterNbResults;

// The adapter is fully below the requested slice range — skip it
if ($adapterLastIndex < $requestFirstIndex) {
continue;
}

// The adapter is fully above the requested slice range — finish the gathering
if ($adapterFirstIndex > $requestLastIndex) {
break;
}

// Else the adapter range definitely intersects with the requested range
$fetchOffset = $requestFirstIndex - $adapterFirstIndex;
$fetchLength = $length;

// The requested range start is below the adapter range start
if ($fetchOffset < 0) {
$fetchLength += $fetchOffset;
$fetchOffset = 0;
}

// The requested range end is above the adapter range end
if ($fetchOffset + $fetchLength > $adapterNbResults) {
$fetchLength = $adapterNbResults - $fetchOffset;
}

// Getting the subslice from the adapter and adding it to the result slice
$fetchSlice = $adapter->getSlice($fetchOffset, $fetchLength);
foreach ($fetchSlice as $item) {
$slice[] = $item;
}
}

return $slice;
}

/**
* Refreshes the cache of the numbers of results of the adapters.
*/
protected function refreshAdaptersNbResults()
{
if (!isset($this->adaptersNbResultsCache)) {
$this->adaptersNbResultsCache = array();
}

foreach ($this->adapters as $index => $adapter) {
$this->adaptersNbResultsCache[$index] = $adapter->getNbResults();
}
}
}
86 changes: 86 additions & 0 deletions tests/Pagerfanta/Tests/Adapter/ConcatenationAdapterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Pagerfanta\Tests\Adapter;

use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Adapter\CallbackAdapter;
use Pagerfanta\Adapter\ConcatenationAdapter;
use Pagerfanta\Adapter\FixedAdapter;
use Pagerfanta\Adapter\NullAdapter;
use PHPUnit\Framework\TestCase;

class ConcatenationAdapterTest extends TestCase
{
public function testConstructor()
{
new ConcatenationAdapter(array(
new ArrayAdapter(array()),
new NullAdapter(),
new FixedAdapter(0, array())
));

$this->setExpectedException(
'\Pagerfanta\Exception\InvalidArgumentException',
'Argument $adapters[1] expected to be a \Pagerfanta\Adapter\AdapterInterface instance, a string given'
);
new ConcatenationAdapter(array(
new ArrayAdapter(array()),
'foo'
));
}

public function testGetNbResults()
{
$adapter = new ConcatenationAdapter(array(
new ArrayAdapter(array('foo', 'bar', 'baz'))
));
$this->assertEquals(3, $adapter->getNbResults());

$adapter = new ConcatenationAdapter(array(
new ArrayAdapter(array_fill(0, 4, 'foo')),
new ArrayAdapter(array_fill(0, 6, 'bar')),
new ArrayAdapter(array('baq'))
));
$this->assertEquals(11, $adapter->getNbResults());

$adapter = new ConcatenationAdapter(array());
$this->assertEquals(0, $adapter->getNbResults());
}

public function testGetResults()
{
$adapter = new ConcatenationAdapter(array(
new ArrayAdapter(array(1, 2, 3, 4, 5, 6)),
new ArrayAdapter(array(7, 8, 9, 10, 11, 12, 13, 14)),
new ArrayAdapter(array(15, 16, 17))
));
$this->assertEquals(array(8, 9, 10), $adapter->getSlice(7, 3));
$this->assertEquals(array(5, 6, 7, 8), $adapter->getSlice(4, 4));
$this->assertEquals(array(6, 7, 8, 9, 10, 11, 12, 13, 14, 15), $adapter->getSlice(5, 10));
$this->assertEquals(array(16, 17), $adapter->getSlice(15, 5));
}

public function testWithTraversableAdapter()
{
$adapter = new ConcatenationAdapter(array(
new CallbackAdapter(
function () {
return 5;
},
function ($offset, $length) {
return new \ArrayIterator(array_slice(array(1, 2, 3, 4, 5), $offset, $length));
}
),
new CallbackAdapter(
function () {
return 3;
},
function ($offset, $length) {
return new \ArrayIterator(array_slice(array(6, 7, 8), $offset, $length));
}
)
));
$this->assertEquals(array(2, 3), $adapter->getSlice(1, 2));
$this->assertEquals(array(4, 5, 6), $adapter->getSlice(3, 3));
}
}