Skip to content

Added native query adapter #167

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,26 @@ $queryBuilder = $entityManager->createQueryBuilder()
$adapter = new DoctrineORMAdapter($queryBuilder);
```

### DoctrineORMNativeQueryAdapter

To paginate [Doctrine native queries](http://doctrine-orm.readthedocs.org/en/latest/reference/native-sql.html) objects.

```php
<?php

use Pagerfanta\Adapter\DoctrineORMNativeQueryAdapter;
use Doctrine\ORM\Query\ResultSetMappingBuilder;

$query = 'SELECT * FROM article';

$rsm = new ResultSetMappingBuilder($entityManager);
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u');

$adapter = new DoctrineORMNativeQueryAdapter($nq);
```

> The adapter accept 2 callable that should modify a given (as parameter) native query to respectively: make a count query / make a slice query

### DoctrineODMMongoDBAdapter

To paginate [DoctrineODMMongoDB](http://www.doctrine-project.org/docs/mongodb_odm/1.0/en/) query builders.
Expand Down
122 changes: 122 additions & 0 deletions src/Pagerfanta/Adapter/DoctrineORMNativeQueryAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

/*
* This file is part of the Pagerfanta package.
*
* (c) Pablo Díez <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Pagerfanta\Adapter;

use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query\ResultSetMapping;
use Pagerfanta\Exception\InvalidArgumentException;

/**
* @author Maxime Veber <[email protected]>
*/
class DoctrineORMNativeQueryAdapter implements AdapterInterface
{
/**
* @var NativeQuery
*/
private $nativeQuery;

/**
* @var callable
*/
private $countQueryBuilderModifier;

/**
* @var callable
*/
private $sliceQueryBuilderModifier;

/**
* @param NativeQuery $query A DBAL query builder.
* @param callable $countQueryBuilderModifier A callable to modify the query to count.
* @param callable $sliceQueryBuilderModifier A callable to modify the query to slice it (the default should do the job most part of the time).
* @throws InvalidArgumentException
*/
public function __construct(NativeQuery $query, $countQueryBuilderModifier = null, $sliceQueryBuilderModifier = null)
{
if (strpos(strtolower($query->getSQL()), 'select') !== 0) {
throw new InvalidArgumentException('Only SELECT queries can be paginated.');
}

if ($countQueryBuilderModifier !== null && !is_callable($countQueryBuilderModifier)) {
throw new InvalidArgumentException('The count query builder modifier must be a callable.');
}
if ($sliceQueryBuilderModifier !== null && !is_callable($sliceQueryBuilderModifier)) {
throw new InvalidArgumentException('The slice query builder modifier must be a callable.');
}

$this->nativeQuery = $query;
$this->countQueryBuilderModifier = $countQueryBuilderModifier;
$this->sliceQueryBuilderModifier = $sliceQueryBuilderModifier;
}

/**
* {@inheritdoc}
*/
public function getNbResults()
{
$query = $this->cloneQuery();
$this->countQueryBuilder($query);

if ($this->countQueryBuilderModifier !== null) {
call_user_func($this->countQueryBuilderModifier, $query);
}

$res = $query->getScalarResult();

return $res[0]['res'];
}

/**
* {@inheritdoc}
*/
public function getSlice($offset, $length)
{
$query = $this->cloneQuery();

if ($this->sliceQueryBuilderModifier !== null) {
call_user_func($this->sliceQueryBuilderModifier, $query, $offset, $length);
} else {
$this->sliceQueryBuilder($query, $offset, $length);
}

return $query->getResult();
}

private function cloneQuery()
{
$query = clone $this->nativeQuery;
$query->setParameters($this->nativeQuery->getParameters());

return $query;
}

private function countQueryBuilder(NativeQuery $query)
{
$sql = explode(' FROM ', $query->getSql());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail when you have a sub-query in the SELECT part.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail in a lot of case actually. And the implementation forces to apply this even when providing a custom modifier, meaning there is no way to get rid of this broken implementation.

Copy link
Author

@Nek- Nek- Jan 2, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first the method countQueryBuilder was originally called on the else statement of this if https://github.com/whiteoctober/Pagerfanta/pull/167/files#diff-47292a5a239d0185dbe662f2f1b0ed81R70 .

But then I wrote the test and I noticed that having the result set mapping that corresponds to the count is more than useful. That's why I changed it to make it like that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nek-, you've explained why you'd like the countQueryBuilder method to always be called (even if a modifier is provided), so thank you for that, but I'm not sure you've addressed @stof's concerns that the implentnation of that method will break in a lot of cases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The best answer I can give here is the detection of too complex queries and throw an exception for these cases. Otherwise good thing to do is to build an SQL analyzer.. :')

$sql[0] = 'SELECT COUNT(*) AS res';
$sql = implode(' FROM ', $sql);

$rsm = new ResultSetMapping();
$rsm->addScalarResult('res', 'res');

$query->setResultSetMapping($rsm);
$query->setSQL($sql);
}

private function sliceQueryBuilder(NativeQuery $query, $offset, $length)
{
$sql = $query->getSql();
$sql .= ' LIMIT ' . $length . ' OFFSET '. $offset;
$query->setSQL($sql);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not work. the syntax to limit a SQL result set is different in approximately all platforms, and I think your syntax here does not even work in any of them

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nek-, could you elaborate on how you'd address the fact that different platforms have different limit syntax?

}
}
117 changes: 117 additions & 0 deletions tests/Pagerfanta/Tests/Adapter/DoctrineORMNativeQueryAdapterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Pagerfanta\Tests\Adapter;

use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\Tools\SchemaTool;
use Pagerfanta\Adapter\DoctrineORMNativeQueryAdapter;
use Pagerfanta\Tests\Adapter\DoctrineORM\DoctrineORMTestCase;
use Pagerfanta\Tests\Adapter\DoctrineORM\Group;
use Pagerfanta\Tests\Adapter\DoctrineORM\Person;
use Pagerfanta\Tests\Adapter\DoctrineORM\User;

class DoctrineORMNativeQueryAdapterTest extends DoctrineORMTestCase
{
private $user1;
private $user2;

public function setUp()
{
parent::setUp();

$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->createSchema(array(
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User'),
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group'),
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Person'),
));

$this->user1 = $user = new User();
$this->user2 = $user2 = new User();
$group1 = new Group();
$group2 = new Group();
$group3 = new Group();
$user->groups[] = $group1;
$user->groups[] = $group2;
$user->groups[] = $group3;
$user2->groups[] = $group1;
$author1 = new Person();
$author1->name = 'Foo';
$author1->biography = 'Baz bar';
$author2 = new Person();
$author2->name = 'Bar';
$author2->biography = 'Bar baz';

$this->entityManager->persist($user);
$this->entityManager->persist($user2);
$this->entityManager->persist($group1);
$this->entityManager->persist($group2);
$this->entityManager->persist($group3);
$this->entityManager->persist($author1);
$this->entityManager->persist($author2);
$this->entityManager->flush();
}

public function testAdapterCount()
{
$sql = "SELECT * FROM User u";

$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u');

$nq = $this->entityManager->createNativeQuery($sql, $rsm);

$adapter = new DoctrineORMNativeQueryAdapter($nq);
$this->assertEquals(2, $adapter->getNbResults());
}

public function testAdapterCountFetchJoin()
{
$count = function (NativeQuery $query) {
$query->setSQL('SELECT COUNT(*) AS res FROM User u');
};

$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u');
$rsm->addJoinedEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group', 'g', 'u', 'groups', array('id' => 'user_id'));
$sql = "SELECT u.id AS id, g.id AS user_id" .
" FROM User u INNER JOIN user_group ug ON u.id = ug.user_id LEFT JOIN groups g ON g.id = ug.group_id"
;

$nq = $this->entityManager->createNativeQuery($sql, $rsm);
$adapter = new DoctrineORMNativeQueryAdapter($nq, $count);
$this->assertEquals(2, $adapter->getNbResults());
}

public function testGetSlice()
{
$sql = "SELECT * FROM User u";

$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u');

$nq = $this->entityManager->createNativeQuery($sql, $rsm);

$adapter = new DoctrineORMNativeQueryAdapter($nq);
$this->assertEquals(1, count( $adapter->getSlice(0, 1)) );
$this->assertEquals(2, count( $adapter->getSlice(0, 10)) );
$this->assertEquals(1, count( $adapter->getSlice(1, 1)) );
}

public function testGetSliceFetchJoin()
{
$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u');
$rsm->addJoinedEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group', 'g', 'u', 'groups', array('id' => 'user_id'));
$sql = "SELECT u.id AS id, g.id AS user_id" .
" FROM User u INNER JOIN user_group ug ON u.id = ug.user_id LEFT JOIN groups g ON g.id = ug.group_id"
;

$nq = $this->entityManager->createNativeQuery($sql, $rsm);
$adapter = new DoctrineORMNativeQueryAdapter($nq);
$this->assertEquals(1, count( $adapter->getSlice(0, 1)) );
$this->assertEquals(2, count( $adapter->getSlice(0, 10)) );
$this->assertEquals(1, count( $adapter->getSlice(1, 1)) );
}
}