Creating sorted array for KnpLabs/KnpMenu

569 Views Asked by At

I want to create a sorted Menu in PHP, Symfony which could be very deep. Therefor I have added 2 fields in category db (parent_id, sort).

My problem is to get a sorted array like:

 array(
    //MAIN CATEGORY 1
    array(
        'id' => 1,
        'name' => 'Main',
        'child'=> false
    ),
    //MAIN CATEGORY 2
    array(
        'id' => 2,
        'name' => 'Main2',
        'child'=> false
    ),
    //MAIN CATEGORY 3
    array(
        'id' => 6,
        'name' => 'Main3',
        'child'=> array(
            array(
                'id' => 4,
                'name' => 'Sub of Main3',
                'child'=> array(
                            'id' => 4,
                            'name' => 'Sub Sub og Main3',
                            'child'=> false
                )
            ),
            array(
                'id' => 7,
                'name' => '2. Sub og Main3',
                'child'=> false
            )
        )
    )
);

So I can use it to create the menu with KnpMenu Bundle. I could not find another way, which is economical in performance and works with this bundle.

Can anybody help me, how to create the array out of the DB?

I tested something around and found a solution with knpMenuBundle like this:

namespace AppBundle\Menu;

use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class Builder implements ContainerAwareInterface
{

    use ContainerAwareTrait;

    public function mainMenu(FactoryInterface $factory, array $options)
    {
        $em         =   $this->container->get('doctrine')->getManager();
        $mandant    =   $this->container->get('session')->get('mandantId');
        $nodes      =   $em->getRepository('AppBundle:Categories')->findSorted($mandant);

        $menu = $factory->createItem('root');
        $menu->addChild('Startseite', array('route' => 'homepage'));

        foreach($nodes as $node)
        {
            $childMenu = $menu->addChild($node['name'],array('route' => $node['route']));
            $this->loadChild($childMenu,$node);
        }

        return $menu;
    }

    private function loadChild($childMenu,array $node)
    {
        if(isset($node['child']))
        {
            foreach ($node['child'] as $child)
            {
                $childMenu = $childMenu->addChild($child['name'],array('route' => $child['route']));
                $this->loadChild($childMenu,$child);
            }
        }
        return;
    }
1

There are 1 best solutions below

0
Chris On

I had a very similar issue and here is how I solved it.

So my page entity looks something like this:

<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Knp\Menu\NodeInterface;

/**
 * Page
 *
 * @ORM\Table(name="PAGE")
 * @ORM\Entity()
 */
class Page implements NodeInterface
{
    /**
     * @var int
     *
     * @ORM\Column(name="ID", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="TITLE", type="string", length=255)
     */
    private $title;

    /**
     * page | link | pdf
     * @var string
     *
     * @ORM\Column(name="CONTENT_TYPE", type="string", length=255)
     */
    private $contentType;

    /**
     * A page can have one parent
     *
     * @var FacetPage
     *
     * @ORM\ManyToOne(targetEntity="Page", inversedBy="childrenPages")
     * @ORM\JoinColumn(name="PARENT_PAGE_ID", referencedColumnName="ID")
     */
    private $parentPage;


    /**
     * A parent can have multiple children
     *
     * @var arrayCollection
     *
     * @ORM\OneToMany(targetEntity="Page", mappedBy="parentPage")
     *
     */
    private $childrenPages;

    /**
     * @var resource
     *
     * @ORM\Column(name="CONTENT", type="text", length=200000)
     */
    private $content;

    /**
     * Many pages could have many allowed roles
     *
     * @var arrayCollection
     *
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="PAGE_ALLOWED_ROLES",
     *      joinColumns={@ORM\JoinColumn(name="page_id", referencedColumnName="ID")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="ID")}
     *      )
     */
    private $allowedRoles;

    /**
     * @var string
     *
     * @ORM\Column(name="SLUG", type="string", nullable=true)
     */
    private $slug;

    /**
     * @var string
     *
     * @ORM\Column(name="PERMALINK", type="string", nullable=true)
     */
    private $permalink;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="CREATED_BY", referencedColumnName="ID", nullable=false)
     * })
     *
     */
    private $author;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="CREATED_ON", type="datetime", nullable=false)
     */
    private $createdOn;

    /**
     * The default status of new pages is published
     *
     * @var string
     *
     * @ORM\Column(name="STATUS", type="string", nullable=false, )
     */
    private $status = 'published';

    /**
     * Page constructor.
     */
    public function __construct(  ) {
        //https://knpuniversity.com/screencast/collections/many-to-many-setup#doctrine-arraycollection
        $this->allowedRoles = new ArrayCollection();
        $this->childrenPages = new ArrayCollection();

    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param int $id
     */
    public function setId( int $id )
    {
        $this->id = $id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param string $title
     */
    public function setTitle( string $title )
    {
        $this->title = $title;
    }

    /**
     * @return string
     */
    public function getContentType()
    {
        return $this->contentType;
    }

    /**
     * @param string $contentType
     */
    public function setContentType( string $contentType )
    {
        $this->contentType = $contentType;
    }

    /**
     * @return Page
     */
    public function getParentPage()
    {
        return $this->parentPage;
    }

    /**
     * @param Page $parentPage
     */
    public function setParentPage( Page $parentPage )
    {
        $this->parentPage = $parentPage;
    }

    /**
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * @param string $content
     */
    public function setContent( string $content )
    {
        $this->content = $content;
    }

    /**
     * @return ArrayCollection|Role[]
     */
    public function getAllowedRoles()
    {
        return $this->allowedRoles;
    }

    /**
     * @param arrayCollection $allowedRoles
     */
    public function setAllowedRoles( $allowedRoles )
    {
        $this->allowedRoles = $allowedRoles;
    }

    /**
     * @return string
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * @param string $slug
     */
    public function setSlug( string $slug )
    {
        $this->slug = $slug;
    }

    /**
     * @return string
     */
    public function getPermalink()
    {
        return $this->permalink;
    }

    /**
     * @param string $permalink
     */
    public function setPermalink( string $permalink )
    {
        $this->permalink = $permalink;
    }

    /**
     * @return User
     */
    public function getAuthor()
    {
        return $this->author;
    }

    /**
     * @param FacetUser $author
     */
    public function setAuthor( User $author )
    {
        $this->author = $author;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedOn()
    {
        return $this->createdOn;
    }

    /**
     * @param \DateTime $createdOn
     */
    public function setCreatedOn( \DateTime $createdOn )
    {
        $this->createdOn = $createdOn;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param string $status
     */
    public function setStatus( string $status )
    {
        $this->status = $status;
    }

    /**
     * @return ArrayCollection
     */
    public function getChildrenPages()
    {
        return $this->childrenPages;
    }

    /**
     * @param ArrayCollection $childrenPages
     */
    public function setChildrenPages( $childrenPages )
    {
        $this->childrenPages = $childrenPages;
    }

    /**
     * Get the name of the node
     *
     * Each child of a node must have a unique name
     *
     * @return string
     */
    public function getName() {
        return $this->title;
    }

    /**
     * Get the options for the factory to create the item for this node
     *
     * @return array
     * @throws \Exception
     */
    public function getOptions() {
        if($this->contentType == 'page'){
            return [
                'route' => 'core_page_id',
                'routeParameters' => ['id'=>$this->id]
            ];
        }

        if($this->contentType == 'doc'){
            return [
                'uri'=>'/'.$this->getContent()
            ];
        }

        if($this->contentType == 'link'){
            return [
                'uri'=>$this->content
            ];
        }

        throw new \Exception('No valid options found for page type',500);
    }

    /**
     * Get the child nodes implementing NodeInterface
     *
     * @return \Traversable
     */
    public function getChildren() {
        return $this->getChildren();
    }
}

The main difference here is that it implements Knp's NodeInterface and its functions which are defined at the end of the entity, getName(), getOptions(), and getChildren().

Now on to my Builder, which basically does the same thing as your recursive function.

<?php

namespace AppBundle\Menu;

use AppBundle\Entity\Page;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class Builder implements ContainerAwareInterface
{

    use ContainerAwareTrait;

    /** @var ItemInterface */
    private $menu;

    /**
     * @param FactoryInterface $factory
     * @param array $options
     *
     * @return ItemInterface
     */
    public function mainMenu(FactoryInterface $factory, array $options)
    {
        $this->menu = $factory->createItem('root');

        $this->menu->addChild('Home', array('route' => 'core_homepage'));

        $em = $this->container->get('doctrine')->getManager();

        // get all published pages
        $pages = $em->getRepository(Page::class)->findBy(['status'=>'published']);

        // build pages
        try {
            $this->buildPageTree( $pages );
        } catch ( \Exception $e ) {
            error_log($e->getMessage());
        }

        return $this->menu;
    }

    /**
     *
     * @param array $pages
     * @param Page $parent
     * @param MenuItem $menuItem
     *
     * @throws \Exception
     */
    private function buildPageTree(array $pages, $parent = null, $menuItem = null)
    {
        /** @var Page $page */
        foreach ($pages as $page) {

            // If page doesn't have a parent, and no menuItem was passed then this is a top level add.
            if(empty($page->getParentPage()) && empty($menuItem) )
                $parentMenu = $this->menu->addChild($page->getTitle(), $page->getOptions());

            // if the current page's parent is === supplied parent, go deeper
            if ($page->getParentPage() === $parent) {

                // if a menuItem was given, then this page is a child so added it to the provided menu.
                if(!empty($menuItem))
                    $parentMenu = $menuItem->addChild($page->getTitle(), $page->getOptions());

                // go deeper
                $this->buildPageTree($pages, $page, $parentMenu);
            }
        }
    }

}

I hope this helps in some way!