KnpLabs / KnpMenu

Menu Library for PHP
https://knplabs.com
MIT License
1.39k stars 191 forks source link

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

Open vishalmelmatti opened 9 years ago

vishalmelmatti commented 9 years ago

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});

eved42 commented 8 years ago

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 !

stof commented 8 years ago

$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

eved42 commented 8 years ago

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 ;-)

wouterj commented 8 years ago

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.

stof commented 8 years ago

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

eved42 commented 8 years ago

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...

stof commented 8 years ago

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

eved42 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

dbu commented 8 years ago

@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.