KNP Pagination links stucked on page 1 - symfony

Good evening everybody,
I'm actually pushing my symfony2 website online and I have some issues. I have set the knp paginator bundle and have followed their intructions regarding the installation. But the fact is that my pagination is not working and therefore it only shows the items that are appearing on page one. When I click next/page2, the url seems to be behaving correctls but the page is still "freezed" the first display.
Here is my code (and I have all the config set up for the different bundles in the autoloader and the kernel):
// Controller/HomeController.php (used for the homepage)
<?php
namespace Pf\Bundle\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HomeController extends Controller
{
public function indexAction()
{
$em = $this->get('doctrine.orm.entity_manager');
$dql = "SELECT a FROM PfBlogBundle:Projects a ORDER by a.id DESC";
$query = $em->createQuery($dql);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$query,
$this->get('request')->query->get('page', 1)/*page number*/,
8/*limit per page*/
);
return $this->render('PfBlogBundle:Default:home.html.twig', array('pagination'=>$pagination));
}
}
and the twig
// Default/home.html.twig
{% extends '::home.html.twig'%}
{% block body %}
<div id="works">
<div class="container">
<div class="row-fluid">
<div class="span12">
{% for project in pagination %}
<div class="span3">
<div class="workBlocks">
<img src="{{ project.thumbnail }}" />
<div class="workCaption">
<h3>{{ project.title }}</h3>
See
</div><!-- end workCaption -->
</div><!-- end workBlocks -->
</div><!-- end span3 -->
{% endfor %}
</div>
{# display navigation #}
{{ knp_pagination_render(pagination) }}
</div><!-- end row -->
</div>
</div><!-- end works -->
{% endblock %}
I can't see where I'm wrong. All the help is much appreciated ;)
EDIT:
// this is the route I want to use...
pf_blog_index:
path: /
defaults: { _controller: PfBlogBundle:Home:index }
(EDIT) Well here I will continue by providing more details as I can't figure out what's wrong:
So in my vendors I have the following folder structure:
knplabs:
knp-components
knp-menu
knp-menu-bundle
knp-paginator-bundle
After having checked my autoload and my AppKernel, I have what I need to have regarding the doc..
In my composer.json :
"require": {
"knplabs/knp-components": "1.2.*#dev",
"knplabs/knp-menu-bundle": "2.0.*#dev",
"knplabs/knp-paginator-bundle": "dev-master"
},
In app/autoload.php I have
$loader->registerNamespaces(array(
'Knp\\Component' => __DIR__.'/../vendor/knp-components/src',
'Knp\\Bundle' => __DIR__.'/../vendor/bundles',
));
And finally, in my AppKernel.php
$bundles = array(
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
);
EDIT:
Also here are my settingy in the app/config.yml
knp_paginator:
page_range: 3 # default page range used in pagination control
default_options:
page_name: page # page query parameter name
sort_field_name: sort # sort field query parameter name
sort_direction_name: direction # sort direction query parameter name
distinct: true # ensure distinct results, useful when ORM queries are using GROUP BY statements
template:
pagination: KnpPaginatorBundle:Pagination:twitter_bootstrap_pagination.html.twig # sliding pagination controls template
sortable: KnpPaginatorBundle:Pagination:sortable_link.html.twig # sort link template

Ok so I found THE or ONE solution to my problem. In fact as I said in my previous comments, the issue seemed to come from the routing. Knp bundle did effectively not appreciate the index route. Therefore, I have modified my controller in order to make a redirection with my indexAction(). Here is what works for me:
// Controller\HomeController.php
<?php
namespace Pf\Bundle\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class HomeController extends Controller
{
public function indexAction()
{
return $this->redirect($this->generateUrl('pf_blog_index2'));
}
public function listAction()
{
$em = $this->get('doctrine.orm.entity_manager');
$dql = "SELECT a FROM PfBlogBundle:Projects a ORDER by a.id DESC";
$query = $em->createQuery($dql);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$query,
$this->get('request')->query->get('page', 1)/*page number*/,
8 /*limit per page */
);
// Puis modifiez la ligne du render comme ceci, pour prendre en compte l'article :
return $this->render('PfBlogBundle:Default:home.html.twig', array('pagination'=>$pagination));
}
}
// src/..../.../.../config/routing.php
pf_blog_index:
path: /
defaults: { _controller: PfBlogBundle:Home:index }
pf_blog_index2:
path: /home
defaults: { _controller: PfBlogBundle:Home:list }
I don't know if that's the best way to solve this issue but it works and in my case, I don't mind having such a url for the time being.
I hope that would help a beginner like me!

Pay attention on this code
$this->get('request')->query->get('page', 1)/*page number*/
You get the 'page' value of _GET parameter of request,by this code , but you have to get your 'page' as Request Attribute, not as value of _GET.
In other words the right code is
$this->get('request')->get('page')

The example in KnpPaginatorBundle repo shows that you need to pass Request $request as a parameter to the controller method that creates $pagination, which is indexAction in your case. I noticed that you don't have this parameter. This is probably why the paginator is not paginating. I've had exactly the same problem and solved it by including the $request parameter in the controller, as the example in KnpPaginatorBundle repo shows. Also, you need to remember to include use Symfony\Component\HttpFoundation\Request in the controller class.

I have the same problem, after many hour of search, I finaly find the solution, we just have to put
$request = Request::createFromGlobals();
before and after get the variable you need, in my case a get it from "GET" by doing
$request->query->get('variable')

Related

Adding a search box to the homepage of a website built on Sylius

Could someone please explain how I can add a search box to the homepage of my website built on Sylius?
I have tried using the SyliusSearchBundle but that seems to be very out of data and the sample config settings throw errors.
I'd be grateful for any help.
Disclaimer
Steps below work at the time I'm writing. SyliusElasticSearchBundle plugin is in a heavy development state, some of the things crash now, but will be fixed for sure.
I've created a demo repository here: https://github.com/mheki/sylius-search-demo
You need running ElasticSearch and a SyliusElasticSearchBundle plugin.
Follow installation instructions from readme: https://github.com/Lakion/SyliusElasticSearchBundle
Just bear in mind to install sylius dev-master (circular dependency...)
Import bundle's config, routing, enable it in AppKernel.
At the moment I'm writing the bundle needed filter_sets config, otherwise it crashed
So start with the simple search by product name:
lakion_sylius_elastic_search:
filter_sets:
default:
filters:
name:
type: string
Populate elastic index with:
bin/console fos:elastic:pop
Override original routing for lakion_elastic_search_shop_product_index - use filter_set: for your channel code.
lakion_elastic_search_shop_product_index:
path: /products
methods: [GET]
defaults:
_controller: lakion_sylius_elastic_search.controller.search:filterAction
_sylius:
template: "#LakionSyliusElasticSearch/Product/index.html.twig"
resource_class: "%sylius.model.product.class%"
filter_set: default
requirements:
slug: .+
Original product index page was failing for me and I had to remove
{{ form_row(form.search) }}
from it. So copied #LakionSyliusElasticSearch/Product/index.html.twig into Resources directory:
Resources\LakionSyliusElasticSearchBundle\views\Product\index.html.twig and made that change.
Now the last thing is to create a form, for example copying the file _security.html.twig from SyliusShopBundle. Add something like this:
<div class="item">
<form action="{{ path('lakion_elastic_search_shop_product_index') }}" method="get">
<div class="ui icon input">
<input type="text" placeholder="{{ 'sylius.ui.search'|trans }}..." name="filter_set[name]" />
<button type="submit" class="ui button mini">
<i class="search icon"></i>
</button>
</div>
</form>
</div>
and here we go :)
Alternatively, if you don't want to use the SyliusElasticSearchBundle plugin, you can implement a simple search on your own (which search inside the product name, description and category name) :
Create a classic html search form, for example:
<form role="search" method="get" action="{{path('app_search_results') }}">
<input type="search" id="site-search" name="q"
placeholder="{{ 'walrus.search.input.placeholder'|trans }}"
aria-label="{{ 'walrus.search.aria.label'|trans }}">
<button>{{ 'walrus.search.aria.button'|trans }}</button>
</form>
Create a controller action for the route 'app_search_results' in order to get the search request sent by your search form :
<?php
namespace AppBundle\Controller;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Sylius\Component\Locale\Context\LocaleContextInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use \Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class SearchController extends Controller
{
/**
* #var LocaleContextInterface
*/
private $locale;
/**
* #var ChannelContextInterface
*/
private $channelContext;
/**
* #var RepositoryInterface
*/
private $productRepository;
public function __construct(
LocaleContextInterface $locale,
ChannelContextInterface $channelContext,
RepositoryInterface $productRepository
)
{
$this->locale = $locale;
$this->channelContext = $channelContext;
$this->productRepository = $productRepository;
}
/**
* #Route("/search", name="app_search_results")
*/
public function searchAction(Request $request) : Response
{
$searchTerm = $request->query->get('q');
$channel = $this->channelContext->getChannel();
$localeCode = $this->locale->getLocaleCode();
$products = $this->productRepository->findByTerm($channel, $localeCode, $searchTerm);
return $this->render(
'#App/search/searchListProducts.html.twig',
[
'products' => $products
]
);
}
}
Don't forget to manually wire the dependency injected through the constructor.
Override the product repository in order to implements the findByTerm() method :
<?php
namespace AppBundle\Repository;
use Sylius\Bundle\CoreBundle\Doctrine\ORM\ProductRepository as
BaseProductRepository;
use Sylius\Component\Core\Model\ChannelInterface;
class ProductRepository extends BaseProductRepository
{
public function findByTerm(ChannelInterface $channel, string $locale, $searchTerm): array
{
$qb = $this->createQueryBuilder('p')
->addSelect('translation')
// get the translated product for the product regarding the current locale
->innerJoin('p.translations', 'translation', 'WITH', 'translation.locale = :locale')
->orWhere('translation.name LIKE :searchTerm')
->orWhere('translation.description LIKE :searchTerm')
// get the taxons of the product
->innerJoin('p.productTaxons', 'productTaxon')
->innerJoin('productTaxon.taxon', 'taxon')
// get the translated taxon
->innerJoin('taxon.translations', 'taxonTranslation', 'WITH', 'taxonTranslation.locale = :locale')
->orWhere('taxonTranslation.name LIKE :searchTerm')
->andWhere(':channel MEMBER OF p.channels')
->andWhere('p.enabled = true')
->setParameter('searchTerm', '%'.$searchTerm.'%')
->setParameter('locale', $locale)
->setParameter('channel', $channel)
->getQuery();
return $qb->getResult();
}
}
You just need now to create the page rendering the list of product (the #App/search/searchListProducts.html.twig). And to tell Sylius to use your custom HomepageController instead of the original one. Same goes for your Product Repository. This can be done inside the services.yml file of you app folder :
services:
sylius.controller.shop.homepage:
public: true
class: AppBundle\Controller\Shop\HomepageController
arguments:
- '#templating'
- '#sylius.context.locale'
- '#sylius.repository.taxon'
sylius_product:
resources:
product:
classes:
repository: AppBundle\Repository\ProductRepository

Twig: Render view with different parameters based on one Controller action

this might be a stupid question but maybe you can still help me out here.
What I want: I have a Twig template, that has tabs in it (using Bootstrap tab panels) and now I want to render the tab contents using one Action from one Controller but with different parameters.
So basically, this controller action should take some param "type" and then return different results based on this type. The goal is to not have 2 Controllers to render the twig view that do almost the same except extracting different information based on the param "type.
However, I cannot get it to work. It's either that both tabs render the same results since apparently the view is only rendered once OR that I get exceptions in the twig template at time of rendering.
What would be the right way to approach this problem?
Many thanks.
There are multiple ways to achieve this. You can use if/else statements in your Twig template, but you can also set the template in you controller. It's up to you what suits best.
Method 1: custom template
By default, Symfony uses 'foobar.html.twig' on your foobarAction() method, but you can override this in your method:
public function recentArticlesAction($max = 3)
{
// make a database call or other logic
// to get the "$max" most recent articles
$articles = ...;
return $this->render(
'article/recent_list.html.twig',
array('articles' => $articles)
);
}
(warning: example from How to Embed Controllers in a Template, but the article itself has nothing to do with your question)
You can set a variable (for example $templateName) and change it:
$templateName = 'recent_list.html.twig';
if ($admin) {
$templateName = 'another_template.html.twig';
}
//or using parameters from you Request
if ($type = $request->request->get('type')) {
$templateName = $type . '.html.twig';
}
return $this->render(
$templateName,
array('articles' => $articles)
);
Method 2: using Twig
Controller:
public function foobarAction(Request $request)
{
return [
'type' => $request->request->get('type');
];
}
You Twig template:
{% if type == 'foo' %}
<h1>Foo!</h1>
<p>Hi, welcome you the foo page!</p>
{% elseif type == 'bar' %}
<h1>Bar!</h1>
<p>Hi, you've reached the Bar page.</p>
{% else %}
<h1>Error!</h1>
<p>Type not found.</p>
{% endif %}
Try render manually each view:
if ($type) {
return $this->render('home/template.html.twig', array('data' => $data));
}
return $this->render('home/another.html.twig', array('another_data' => $another_data));
Many thanks for the answers here, but they did not really helped me. Next time I try to add more code examples ;)
My goal was to render from 1 action into 1 template but with different parameters and render those different contents as partials in the parent view.
Example: Given a template called "view.html.twig" that includes 2 times the template "myPartial.html.twig", but first time with param and second time without param. Based on this param different contents should be returned.
My question was now, why apparently only the first action is rendered in Symfony as both my partial views had the same content.
So this is what I did now:
1 Controller, 2 Actions, both call a 3rd action to fetch the data and return the values to the calling action. Then both actions call the render method with the values rendered from the 3rd action.
This is what it looks now:
<div class="tab-content clearfix">
<div class="tab-pane" id="1b">
{% render controller('MyBundle:MyController:list1') %}
</div>
<div class="tab-pane" id="2b">
{% render controller('MyBundle:MyController:list2', {'type' : 1}) %}
</div>
But what I wanted to achieve was to do something like this (which did not work because then both tabs would show the same content):
<div class="tab-content clearfix">
<div class="tab-pane" id="1b">
{% render controller('MyBundle:MyController:list') %}
</div>
<div class="tab-pane" id="2b">
{% render controller('MyBundle:MyController:list', {'type' : 1}) %}
</div>
Which I find confusing since in both times "render" is called, so I would expect that the Controller is called in both times so that also the partial view in the controller is rendered both times. But apparently this was not the case :( The Controller itself looks something like this:
public function list1Action()
{
$twigVars = //do something to build the twig vars without param;
return $this->render('#MyBundle/Test/partial_list.html.twig', array(
'vars' => $twigVars,
));
}
public function list2Action($param)
{
$twigVars = //do something to build the twig vars with param;
return $this->render('#MyBundle/Test/partial_list.html.twig', array(
'vars' => $twigVars,
));
}
While what I wanted was something like this:
public function listAction($param = '')
{
if ($param) {
$twigVars = //do something to return the twig vars with param;
} else {
$twigVars = //do something else to return twig vars without param;
}
return $this->render('#MyBundle/Test/partial_list.html.twig', array(
'vars' => $twigVars,
));
}

Is it good practice to put Edge Side Includes into my templates?

In our Symfony2 Application we render reusable blocks with render_esi. We have this kind of templates:
{% for products as product %}
<div class="product">
<h4>{{ product.name }}</h4>
<div class="ratings">
{{ render_esi(controller('AcmeDemoCommunityBundle:Rating:ratingStars', {
objectType: 'product',
objectId: product.id,
readOnly: true
})) }}
</div>
</div>
{% endfor %}
And of cause we use the render_esi also in the detail page of the product.
I would like to differentiate different types of blocks:
Blocks that renders other actions of the same controller.
Blocks that could be used across other parts of the application.
What is the difference?
Blocks that only renders other actions of the same controller as the parent template are most of the times there to modularize one page and make parts cacheable. This blocks are only used one times in the whole application.
Blocks that renders parts like rating stars or comments are kind of independent widgets that provide an specific functionality. The current controller dose not know anything about this widgets. This kind of blocks are mostly used multiple times in an application.
What does that mean for the software design?
It means that we may want to change the way comments and ratings work in the future. May the not get rendered by an ESI anymore in the future because we have outsourced the functionality to an third-party service and only need to include some kind of JavaScript in this place? Or we render them directly?
This is something that has to be decided by the widget and not by the part that include the widget.
So what could I do to improve my design?
You could keep using ESI (because it makes sense for your usecase), but you should change the way of how the modules are included in the Twig files. You should move the logic for this out of the template into an separate Twig Extension in the AcmeDemoCommunityBundle.
namespace Acme\DemoCommunityBundle\Twig;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Acme\DemoCommunityBundle\Rating\RateableInterface;
class CommunityExtension extends \Twig_Extension
{
/**
* #var string
*/
const RATING_ACTION = 'AcmeDemoCommunityBundle:Rating:ratingStars';
/**
* #var FragmentHandler
*/
protected $handler;
public function __construct(FragmentHandler $handler)
{
$this->handler = $handler;
}
public function getFunctions()
{
return array(
'community_rating' => new \Twig_Function_Method($this, 'communityRating', array('is_safe' => array('html'))),
);
}
public function communityRating(RateableInterface $object, $readOnly = false)
{
return $this->handler->render(new ControllerReference(self::RATING_ACTION, array(
'objectType' => $object->getRatingType(),
'objectId' => $object->getId(),
'readOnly' => $readOnly
)), 'esi', $options);
}
public function getName()
{
return 'community';
}
}
services:
acme_community.twig.community:
class: Acme\DemoCommunityBundle\Twig\CommunityExtension
arguments: [ #fragment.handler ]
tags:
- { name: twig.extension }
Now your template should look like this:
{% for products as product %}
<div class="product">
<h4>{{ product.name }}</h4>
<div class="ratings">
{{ community_rating(product, true) }}
</div>
</div>
{% endfor %}
With this design it is easy to use the rating stars in our application, but we also have the flexibility to change the implementation how ratings work in the future without touching the templates where ratings are used.

How to reload part of twig with ajax

I need to reload part of my html.twig:
in controller:
$entity = $em->getRepository('PublishDemandsBundle:Demands')->find($id);
In twig:
{% for n in entity %} {{ n.Id }} {% endfor %}.
i need how to reload $entity with ajax.Can someone help me and thanks.
You can do this with jQuery. I think the best way to do this (I think) is to have a method in your controller that do nothing but a findAll() on your Demands repo :
public function demandsAction()
{
$entity = $em->getRepository('PublishDemandsBundle:Demands')->findAll();
return $this->render('PublishDemandsBundle:Demands:liste.html.twig', array(
'entity' => $entity
));
}
Make sure this Action can be called by a route, let's say /ajax/demands/
Then, in your twig template, just do :
<div id="demands">
{{ render(controller("PublishDemandsBundle:MainController:demands")) }}
</div>
reload
With a bit of jQuery :
$('#reload').click(function() {
$.get("/ajax/demands", function( data ) {
$('#demands').html( data );
});
I haven't tested this yet, and it might be adapted to your case, but again, I would do it this way.

Can verbatim be used on contents of an include?

I'm sharing templates between client and server and would like to output the raw template inside a script tag which is possible with verbatim.
http://twig.sensiolabs.org/doc/tags/verbatim.html
However it would be nicer if this could be applied as a filter to the include but it doesn't seem possible?
I'm new to twig so excuse me if i've missed obvious functionality.
I ran into the same problem, and this page came up in my search results. In the time since this question was answered, the Twig developers added this functionality into the library. I figured I should add some details for future searchers.
The functionality to include raw text (aka for client-side templates using the same syntax as Twig) is accomplished with the source function.
Ie: {{ source('path/to/template.html.twig') }}
http://twig.sensiolabs.org/doc/functions/source.html
I was looking for something like this too because I'm using Twig.js for some client-side templating along with Symfony. I was trying to share templates between the server-side and client-side code, so I needed content to be parsed in some cases and treated as verbatim in others, which proved to be a bit tricky.
I couldn't find anything built into Twig to help with this, but luckily, it's pretty easy to extend Twig to get what you're looking for. I implemented it as a function, but you may be able to do it as a filter too.
services.yml
statsidekick.twig.include_as_template_extension:
class: StatSidekick\AnalysisBundle\Twig\IncludeAsTemplateExtension
tags:
- { name: twig.extension }
IncludeAsTemplateExtension.php
<?php
namespace StatSidekick\AnalysisBundle\Twig;
use Twig_Environment;
use Twig_Extension;
class IncludeAsTemplateExtension extends Twig_Extension {
/**
* Returns a list of global functions to add to the existing list.
*
* #return array An array of global functions
*/
public function getFunctions() {
return array(
new \Twig_SimpleFunction( 'include_as_template', array( $this, 'includeAsTemplate' ), array( 'needs_environment' => true, 'is_safe' => array( 'html' ) ) )
);
}
function includeAsTemplate( Twig_Environment $env, $location, $id ) {
$contents = $env->getLoader()->getSource( $location );
return "<script data-template-id=\"{$id}\" type=\"text/x-twig-template\">{$contents}</script>";
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName() {
return 'include_as_template_extension';
}
}
Usage in Twig file
{{ include_as_template( 'Your:Template:here.html.twig', 'template-id' ) }}
If you have a Twig file like this:
<ul class="message-list">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
The output will be this:
<script data-template-id="template-id" type="text/x-twig-template">
<ul class="message-list">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
</script>
The work is derived from Kari Söderholm's answer here. Hope that helps!

Resources