KnpMenu icon indicating copy to clipboard operation
KnpMenu copied to clipboard

is there any way to get previous and next menu items with respect to the current page (menu) item opened ?

Open vishalmelmatti opened this issue 10 years ago • 11 comments

Hi,

This is common requirement in blogging that when user is on current page, we need to add previous and next links.

Is there any way to get previous and next menu items with respect to the page opened ? Something like,

knp_menu_render('menu', {'next': 1});

vishalmelmatti avatar Aug 05 '15 12:08 vishalmelmatti

Hi, it's exactly what I'm trying to do !

I think we should use iterators in KnpMenu, see "Filtering only current items".

I installed KnpMenuBundle and created my menu as a service, then in my default controller, I test this :

$root = $this->get('knp_menu.menu_provider')->get('main');
$menu = $root['my-item'];

$itemMatcher = \Knp\Menu\Matcher\Matcher();

// create the iterator
$iterator = new \Knp\Menu\Iterator\CurrentItemFilterIterator($menu->getIterator(), $itemMatcher);

foreach ($iterator as $item) {
    echo $item->getName() . " ";
}

But I have a Symfony error : Attempted to call function "Matcher" from namespace "Knp\Menu\Matcher".

So I add these lines at the beginning of my controller :

use Knp\Menu\Matcher\Matcher;
use Knp\Menu\Iterator\CurrentItemFilterIterator;

But I still have this error. I don't understand...

Anyway! After get items infos according to the current page, we could use php functions prev() and next() to find what we want.

Here is a simple example which works for me. Instead of using a "manual array", we should be using the structure array of our KnpMenu.

// DefaultController.php

    /**
    * @Route("/my-section/{article}", name="section", defaults={"article" = "index"})
    */
    public function showAction(Request $request, $article) {
        $tpl = array(
            'my-first-article-url'  => 'article1',
            'my-second-article-url' => 'article2',
            'my-third-article-url'  => 'article3'
        );

        // Find the previous and next articles according to current article
        foreach ($tpl as $k => $v) {
            if ($k == $article) {
                $next = current($tpl);     // current corresponds to next article (weird)
                $prev = prev($tpl);
                $prev = prev($tpl);
            }
        }

        $prevLink = (!empty($prev)) ? $this->generateUrl('section', array('article' => $prev)) : "";
        $nextLink = (!empty($next)) ? $this->generateUrl('section', array('article' => $next)) : "";

        return $this->render('section/' . $tpl[$article] . '.html.twig', array(
            'prev' => $prevLink,
            'next' => $nextLink,
        ));
    }

In a twig file :

<div class="nav-page-bar">
    {% if prev is empty == false %}
        <a class="prev" href="{{ prev }}">Previous</a>
    {% endif %}

    {% if next is empty == false %}
        <a class="next" href="{{ next }}">Next</a>
    {% endif %}
</div>

With KnpMenu array, we could replace "Previous" and "Next" with items labels.

Please, help us to find a solution !

eved42 avatar Apr 14 '16 08:04 eved42

$itemMatcher = \Knp\Menu\Matcher\Matcher();

You are missing new here, meaning you are doing a function call (but there is no such function) instead of doing a class instantiation

stof avatar Apr 14 '16 08:04 stof

Ok thanks... Yet, the code is exactly like this in the documentation, it will be good to correct it !

I test this :

$root = $this->get('knp_menu.menu_provider')->get('main');
$menu = $root['tout-savoir'];

$itemMatcher = new \Knp\Menu\Matcher\Matcher();

// create the iterator
$iterator = new \Knp\Menu\Iterator\CurrentItemFilterIterator($menu->getIterator(), $itemMatcher);

foreach ($iterator as $item) {
    echo $item->getName() . " ";
}

echo doesn't show anything. $iterator returns an object with empty properties...

object(Knp\Menu\Iterator\CurrentItemFilterIterator)[2142]
  private 'matcher' => 
    object(Knp\Menu\Matcher\Matcher)[2146]
      private 'cache' => 
        object(SplObjectStorage)[1935]
          private 'storage' => 
            array (size=0)
              ...
      private 'voters' => 
        array (size=0)
          empty

$menu is a Knp\Menu\MenuItem object, here is what it looks like :

object(Knp\Menu\MenuItem)[2153]
  protected 'name' => string 'tout-savoir' (length=11)
  protected 'label' => string 'S'informer' (length=10)
  protected 'linkAttributes' => 
    array (size=2)
      'title' => string 'Tout savoir sur le mal de dos' (length=29)
      'class' => string 'hvr-bubble-bottom' (length=17)
  protected 'childrenAttributes' => 
    array (size=0)
      empty
  protected 'labelAttributes' => 
    array (size=0)
      empty
  protected 'uri' => string '/symfony3.0/monmaldedos/web/app_dev.php/tout-savoir/' (length=52)
  protected 'attributes' => 
    array (size=0)
      empty
  protected 'extras' => 
    array (size=1)
      'routes' => 
        array (size=1)
          0 => 
            array (size=2)
              ...
  protected 'display' => boolean true
  protected 'displayChildren' => boolean true
  protected 'children' => 
    array (size=5)
      'maux' => 
        object(Knp\Menu\MenuItem)[2152]
          protected 'name' => string 'maux' (length=4)
          protected 'label' => string 'Les maux les plus fréquents' (length=28)
          protected 'linkAttributes' => 
            array (size=2)
              ...

So I have all informations that are useful for me, but iterator seems to not work. Thank you stof to help me ;-)

eved42 avatar Apr 14 '16 08:04 eved42

Yet, the code is exactly like this in the documentation, it will be good to correct it !

If you click on the "edit" symbol on the top left corner of the article, you can correct it and submit a PR. Can you please do that?

echo doesn't show anything.

To be more precise, echo isn't executed. This is because you're instantiating a matcher without any voters. This way, there is no logic that votes if a menu item is current or not, so no item is marked as current.

You have to enable at least one voter. Take a look at the KnpMenu\Menu\Matcher\Voter namespace for the available voters.

wouterj avatar Apr 14 '16 09:04 wouterj

Well, you should not create a new matcher but get the one registered as a service. Otherwise your matcher will not have any voter configured, and so won't match any item as current

stof avatar Apr 14 '16 10:04 stof

If you click on the "edit" symbol on the top left corner of the article, you can correct it and submit a PR. Can you please do that?

Ok, no problem.

Well, you should not create a new matcher but get the one registered as a service.

How can I do that ?

Maybe I could use my menu item object $menu and modify a little bit ma next code (when I search previous and next values in my $tpl array).

I don't understand what are voters but maybe I don't need finally to use them...

eved42 avatar Apr 14 '16 10:04 eved42

Replace the line with $matcher = $this->get('knp_menu.matcher');

stof avatar Apr 14 '16 10:04 stof

The idea that I had works. But the only inconvenient is that I have to list manually the association url => item name.

    public function showAction(Request $request, $article) {
        $tpl = array(
            'my-first-article-url'  => 'article1',
            'my-second-article-url' => 'article2',
            'my-third-article-url'  => 'article3'
        );

        $root = $this->get('knp_menu.menu_provider')->get('main');
        $menu = $root['section'];

        // Find the previous and next articles according to current article
        foreach ($tpl as $k => $v) {
            if ($k == $article) {
                $next = current($tpl);     // current corresponds to next article (weird)
                $prev = prev($tpl);
                $prev = prev($tpl);
            }
        }

        $prevLink = (!empty($prev)) ? $this->generateUrl('section', array('article' => $prev)) : "";
        $nextLink = (!empty($next)) ? $this->generateUrl('section', array('article' => $next)) : "";

        return $this->render('section/' . $tpl[$article] . '.html.twig', array(
            'prev' => array('target' => $prevLink, 'label' => $menu->getChildren()[$prev]->getLabel()),
            'next' => array('target' => $nextLink, 'label' => $menu->getChildren()[$next]->getLabel()),
        ));
    }

Twig

<div class="nav-page-bar">
    {% if prev is empty == false %}
        <a class="prev" href="{{ prev.target }}">{{ prev.label }}</a>
    {% endif %}

    {% if next is empty == false %}
        <a class="next" href="{{ next.target }}">{{ next.label }}</a>
    {% endif %}
</div>

eved42 avatar Apr 14 '16 10:04 eved42

EDIT : 2016-04-15

Ok, I found the solution with matcher and iterators.

DefaultController.php

/**
* @Route("/my-section/{page}", name="my_section", defaults={"page" = "index"})
*/
public function showAction(Request $request, $page) {
    $root = $this->get('knp_menu.menu_provider')->get('main');  // get menu
    $menu = $root['my-section'];

    // get current menu item (= current page)
    $matcher  = $this->get('knp_menu.matcher');
    $iterator = new \Knp\Menu\Iterator\CurrentItemFilterIterator($menu->getIterator(), $matcher);

    // $iterator contains only one item
    foreach ($iterator as $item) {
       $current = $item->getName();
    }

    // get all sub-pages of menu item : "my-section"
    $itemIterator   = new \Knp\Menu\Iterator\RecursiveItemIterator($menu);
    $children       = new \RecursiveIteratorIterator($itemIterator, \RecursiveIteratorIterator::SELF_FIRST);

    // add index page
    $items = array(
        array(
            'name' => 'index',
            'label' => $menu->getLabel(),
            'route' => $this->generateUrl('my_section_index')
        )
    );

    foreach ($children as $c) {
       // routeParameter
       $param = $c->getExtras()['routes'][0]['parameters']['page'];

       $items[] = array(
            'name' => $c->getName(),
            'label' => $c->getLabel(),
            'route' => $this->generateUrl('my_section', array('page' => $param))
        );
    }

    // Find the previous and next pages according to current page
    foreach ($items as $k => $item) {
        if ($current == $item['name']) {
            # get previous page with key
            $prev = ($k-1 >= 0) ? $items[$k-1] : array('label' => "", 'route' => "");

            $next = current($items);

            # if 2 children only : $prev = $next
            # so $next is useless
            if (!empty($prev['name']) && $prev['name'] == $next['name'])
                $next = array('label' => '', 'route' => '');

            break;
        }
    }

    // Display
    return $this->render('my-section/' . $current . '.html.twig', array(
        'prev' => array('route' => $prev['route'], 'label' => $prev['label']),
        'next' => array('route' => $next['route'], 'label' => $next['label']),
    ));
}

Is it possible to find the routeParameter of a menu item easier than below ? $c->getExtras()['routes'][0]['parameters']['page']

Twig

<div class="nav-page-bar">
    {% if prev.route is empty == false %}
        <a class="prev" href="{{ prev.route }}">{{ prev.label }}</a>
    {% endif %}

    {% if next.route is empty == false %}
        <a class="next" href="{{ next.route }}">{{ next.label }}</a>
    {% endif %}
</div>

This code shows only children pages of "my-section".

Hope it helps ! ;-)

eved42 avatar Apr 14 '16 13:04 eved42

I edited my last post, few errors and I added index page in the loop.

eved42 avatar Apr 15 '16 07:04 eved42

@eved42 i would like to close this issue if its solved for you. would be great to provide this example in the documentation. maybe you could do a pull request to add a doc/05-cookbook.md file and add this there, starting with explaining what you want to achieve and then what you have here.

dbu avatar Jun 21 '16 14:06 dbu