How to use KNPPaginatorBundle to paginate results using Doctrine Repository? - php

I'm working on a Symfony2 project and I decided to use KNPPaginatorBundle to build an easy pagination system. So I created a Product entity and I want to add the paginator to indexAction action (generated by CRUD command).
// Retrieving products.
$em = $this->getDoctrine()->getManager();
//$entities = $em->getRepository('LiveDataShopBundle:Product')->findAll();
$dql = "SELECT a FROM LiveDataShopBundle:Product a";
$entities = $em->createQuery($dql);
// Creating pagnination
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$entities,
$this->get('request')->query->get('page', 1),
20
);
It works fine but I want to use the Product's repository instead of creating the query directly in the controller. How can I do that ?
In fact, directly add the collection of results to the paginate object is just too slow because its load all products then paginate the ArrayCollection.
Thanks in advance.
K4

I suggest using QueryBuilder in your ProductRepository and then passing that to the paginator:
ProductRepository extends EntityRepository
{
// This will return a QueryBuilder instance
public function findAll()
{
return $this->createQueryBuilder("p");
}
}
In the controller:
$products = $productRepository->findAll();
// Creating pagnination
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$products,
$this->get('request')->query->get('page', 1),
20
);

I think in some cases we could use Closure and pass to it a QueryBuilder object.
In your ProductRepository you could do something like this:
ProductRepository extends EntityRepository
{
public function findAllPublished(callable $func = null)
{
$qb = $this->createQueryBuilder('p');
$qb->where('p.published = 1');
if (is_callable($func)) {
return $func($qb);
}
return $qb->getQuery()->getResult();
}
}
and then in ProductController:
public function indexAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$paginator = $this->get('knp_paginator');
$func = function (QueryBuilder $qb) use ($paginator, $request) {
return $paginator->paginate($qb, $request->query->getInt('page', 1), 10);
};
$pagination = $em->getRepository('AppBundle:Report')->findAllPublished($func);
// ...
}
I think it more flexible and you could use findAllPublished method to get both paginated or NOT paginated results if you need.
Also keep in mind that callable type hint work in PHP >=5.4! Please, check docs for more info.

In our project we want to avoid using Doctrine queries in controllers. We have also separate layers. Controllers must not access the database. So I included pagination in the Repository.
Here my code in controller:
public function indexAction(Request $request)
{
$userRepository = $this->get('user_repository');
$page = intval($request->query->get('page', 1));
$pages = 0;
$users = $userRepository->findAllPaginated($pages, $page - 1, 10);
return $this->render('User:index.html.twig', array(
'users' => $users,
'page' => $page,
'pages' => $pages,
));
}
And here is the important code in my repository:
use Doctrine\ORM\Tools\Pagination\Paginator;
class UserRepository extends EntityRepository
{
/**
* #return User[]
*/
public function findAllPaginated(&$pages, $startPage = 0, $resultsPerPage = 5)
{
$dql = 'SELECT u FROM CoreBundle:User u';
$query = $this->getEntityManager()->createQuery($dql)
->setFirstResult($startPage * $resultsPerPage)
->setMaxResults($resultsPerPage);
$paginator = new Paginator($query);
$count = $paginator->count();
$pages = floor($count/$resultsPerPage);
return $paginator; // on $paginator you can use "foreach", so we can say return value is an array of User
}
}

Related

laravel scout search with multiple model

I used this old code, and start refactor it with use of laravel/scout.
This code is a simply search function thet search in posts and pages too, and show result mixed paginate.
Old code:
public function search($term) {
$post = Post::where('title', 'LIKE', $term);
$page = Page::where('content', 'LIKE', $term);
$result = $page->union($post)->orderBy('created_at')->paginate();
return $result;
}
New Code not working:
public function search($term) {
$post = Post::search($term);
$page = Page::search($term);
$result = $page->union($post)->orderBy('created_at')->paginate();
return $result;
}
get error: Method Laravel\Scout\Builder::union does not exist.
What is the best syntax for this problem?
Since Scout\Builder doesn't support union. And it would be non trivial to implement union functionality for all possible search engines supported by Scout.
However, Scout\Builder provides a query() function to customise the eloquent results query.
This provides kind of an escape hatch, where by scout can be leveraged on one model (out of the two)
public function search($term)
{
$postQuery = Post::query()
->where('some_column', 'like', $term . "%");
$results = Page::search($term)
->query(
fn($query) => $query->union($postQuery)
->orderBy('created_at', 'desc')
)
->paginate();
}
Laravel Scout - Customizing the Eloquent Results Query
For Algolia Engine
If using Algolia as search engine, there's Algolia Scout Extended which supports search with multiple models.
For other Engines
Another approach to search multiple models with Scout and get paginated results would be:
Get search results for individual models using Scout (using Model::search('query')->get();)
Concat the resulting collections
Paginate the collection
Define a macro for Illuminate\Support\Collection in the boot method of a Service Provider (for eg in App\Providers\AppServiceProvider)
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Collection::macro('paginate', function ($perPage, $total = null, $page = null, $pageName = 'page') {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$total ? $this : $this->forPage($page, $perPage)->values(),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
});
}
}
Then in the controller
public function search($term) {
$post = Post::where('title', 'LIKE', $term)->get();
$page = Page::where('content', 'LIKE', $term)->get();
$result = $page->concat($post)->sortBy('created_at')->paginate(5);
return $result;
}

ApiPlatform - custom action with dbal query

Maybe I don't understand this idea but I'm starting create my first test app that use api platform.
I have custom action for my search:
/**
* #Route(
* path="/api/pharmacies/search",
* name="pharmacy_search",
* defaults={
* "_api_resource_class"=Pharmacy::class,
* "_api_collection_operation_name"="search"
* }
* )
* #param Request $request
*
* #return Paginator
*/
public function search(Request $request)
{
$query = SearchQuery::createFromRequest($request);
return $this->pharmacyRepository->search($query);
}
and method in the repository:
public function search(SearchQuery $searchQuery: Paginator
{
$firstResult = ($searchQuery->getPage() - 1) * self::ITEMS_PER_PAGE;
$qb = $this->createQueryBuilder('p')
/...
->setFirstResult($firstResult)
->setMaxResults(self::ITEMS_PER_PAGE);
$doctrinePaginator = new DoctrinePaginator($qb, false);
return new Paginator($doctrinePaginator);
}
And it works fine but this action doesn't need all field form the entity and relations to other tables. Currently this action creates 22 queries. I'd like create query in the DBAL/QueryBuilder and return pagination object with DTO.
public function search(SearchQuery $searchQuery)
{
.../
$qb = $this->createQueryBuilder('p')
->select('p.id')
->addSelect('p.name')/....
$rows = $qb->getQuery()->getArrayResult();
foreach ($rows as $row){
$data[] = new SearchPharmacy($row['id'], $row['name']);
}
return $data;
}
The above code will work but if the response isn't Pagination object and I don't have hydra:totalItems, hydra:next etc in the response.
In theory I can use DataTransformer and transform entity to DTO, but this way can't allow simplify database queries.
Can I achieve this?
I don't know how to use dbal query and mapped the result on a DTO, but I know how to add custom select, return it like a scalar:
public function search(SearchQuery $searchQuery): Paginator
{
$firstResult = ($searchQuery->getPage() - 1) * self::ITEMS_PER_PAGE;
$qb = $this->createQueryBuilder('p')
->select('p.id')
->addSelect('p.name')
->where('p.name LIKE :search')
->setParameter('search', "%" . $searchQuery->getSearch() . "%");
$query = $qb->getQuery()->setFirstResult($firstResult)
->setMaxResults(self::ITEMS_PER_PAGE);
$doctrinePaginator = new DoctrinePaginator($query, false);
$doctrinePaginator->setUseOutputWalkers(false);
return new Paginator($doctrinePaginator);
}
It finally I have 2 light queries, selected field from entity and Pagination.
Sources:
https://github.com/APY/APYDataGridBundle/issues/931
https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/cookbook/dql-custom-walkers.html

How to pass and retrieve data between two controllers in Laravel

I'm trying to get to grips with Laravel and not finding the documentation any help at all. It seems to assume you know so much instead of actually walking new users through each section step by step.
I've got to the point where I need to make an internal call to another class, and sticking with MVC I can't seem to do what should be a simple thing.
My code:
class UsersController extends BaseController {
protected $layout = 'layouts.templates.page';
protected $messages;
public function getIndex()
{
$input = array('where', array('field' => 'email', 'operator' => '=', 'value' => 'tony#fluidstudiosltd.com'));
$request = Request::create('user/read', 'GET');
$users = json_decode(Route::dispatch($request)->getContent());
var_dump($users); exit;
$this->pageTitle = 'Fluid Customer Status :: Admin Users';
$this->content = View::make('layouts.admin.users');
}
}
Class UserController extends Base Controller
public function getRead()
{
$userId = (int) Request::segment(3);
if ($userId)
{
$user = User::findOrFail($userId);
return $user;
}
else
{
$users = new User;
var_dump(Input::all()); exit;
if (Input::has('where'))
{
var_dump(Input::get('where')); exit;
}
return $users->get();
}
}
Why isn't the input data from UsersController#getIndex available in UserController#getRead

Symfony2, Doctrine2, findBy() order not working

I created a repository for my Articles entity and I'm trying to get all values ordered by ID DESC. But, I'll get everytime values ordered by id ASC. Here is my ArticleRepository.php:
<?php
namespace Acme\BlogBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ArticleRepository extends EntityRepository
{
public function findAll()
{
return $this->findBy(array(), array('id' => 'DESC'));
}
public function findOneBySlug($slug)
{
$query = $this->getEntityManager()
->createQuery('
SELECT p FROM AcmePagesBundle:Article a
WHERE a.slug = :slug
')
->setParameter('slug', $slug);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return false;
}
}
}
Any ideas?
The Syntax looks good. This should provide a set of well ordered articles.
First, make sure the #Entity annotation is set with the right Repository class within the Article Entity,
/**
* #ORM\Entity(repositoryClass="...");
*/
class Article
{
// ...
}
Also, if you want to use native helpers, you've just to call findBy from the ArticleRepository within your controller,
$articles = $this->get('doctrine')
->getRepository('YourBundle:Article')
->findBy(array(), array('id' => 'DESC'));
You don't need to create a query in ArticleRepostory.php for that
In your controller you can just do:
$entities = $em->getRepository('YourBundle:Article')->findBy(array(), array( 'id' => 'DESC' ));
->findBy(array(), array( 'id' => 'DESC' )); // Order Works
->findAll(array(), array( 'id' => 'DESC' )); // Order doesn't work
This should be work:
public function findAll()
{
$em = $this->getEntityManager();
$query = $em->createQuery('
SELECT *
FROM AcmePagesBundle:Article a
ORDER BY a.id DESC
');
return $query->getResult();
}
I would tend to do this in my repository (to allow for using the same select DQL in different methods in the repository - especially when you have lots of fetch-joins to include):
class FooRepository extends EntityRepository
{
/**
* Get the DQL to select Foos with all joins
*
* #return string
*/
public function getSelectDql()
{
$dql = '
SELECT f
FROM Entity:Foo f
';
return $dql;
}
/**
* Fetch all foos, ordered
*
* #return array
*/
public function fetchAllOrdered()
{
$dql = sprintf(
'%s %s',
$this->getSelectDql(),
'ORDER BY f.id DESC'
);
return $this->getEntityManager()
->createQuery($dql)
->getResult();
}
}
This should make your repository quite flexible, allow different ordering in different methods (if you need to order them differently) and keep all DB-related code out of your controller.
Symfony 3.3 find by order working example.
From your controller:
public function indexAction(){
$entityManager = $this->getDoctrine()->getManager();
$categoryRepository = $entityManager->getRepository(ProductCategory::class);
$items = $categoryRepository->findBy(array(), array('id' => 'DESC'));
....
}

Doctrine : how to manipulate a collection?

With symfony && doctrine 1.2 in an action, i try to display the top ranked website for a user.
I did :
public function executeShow(sfWebRequest $request)
{
$this->user = $this->getRoute()->getObject();
$this->websites = $this->user->Websites;
}
The only problem is that it returns a Doctrine collection with all the websites in it and not only the Top ranked ones.
I already setup a method (getTopRanked()) but if I do :
$this->user->Websites->getTopRanked()
It fails.
If anyone has an idea to alter the Doctrine collection to filter only the top ranked.
Thanks
PS: my method looks like (in websiteTable.class.php) :
public function getTopRanked()
{
$q = Doctrine_Query::create()
->from('Website')
->orderBy('nb_votes DESC')
->limit(5);
return $q->execute();
}
I'd rather pass Doctrine_Query between methods:
//action
public function executeShow(sfWebRequest $request)
{
$this->user = $this->getRoute()->getObject();
$this->websites = $this->getUser()->getWebsites(true);
}
//user
public function getWebsites($top_ranked = false)
{
$q = Doctrine_Query::create()
->from('Website w')
->where('w.user_id = ?', $this->getId());
if ($top_ranked)
{
$q = Doctrine::getTable('Website')->addTopRankedQuery($q);
}
return $q->execute();
}
//WebsiteTable
public function addTopRankedQuery(Doctrine_Query $q)
{
$alias = $q->getRootAlias();
$q->orderBy($alias'.nb_votes DESC')
->limit(5)
return $q
}
If getTopRanked() is a method in your user model, then you would access it with $this->user->getTopRanked()
In your case $this->user->Websites contains ALL user websites. As far as I know there's no way to filter existing doctrine collection (unless you will iterate through it and choose interesting elements).
I'd simply implement getTopRankedWebsites() method in the User class:
class User extends BaseUser
{
public function getTopRankedWebsites()
{
WebsiteTable::getTopRankedByUserId($this->getId());
}
}
And add appropriate query in the WebsiteTable:
class WebsiteTable extends Doctrine_Table
{
public function getTopRankedByUserId($userId)
{
return Doctrine_Query::create()
->from('Website w')
->where('w.user_id = ?', array($userId))
->orderBy('w.nb_votes DESC')
->limit(5)
->execute();
}
}
You can also use the getFirst() function
$this->user->Websites->getTopRanked()->getFirst()
http://www.doctrine-project.org/api/orm/1.2/doctrine/doctrine_collection.html#getFirst()

Categories