everlutionsk/navigation-bundle

Symfony导航组件包

v3.0.0 2022-11-15 11:37 UTC

README

Scrutinizer Code Quality Build Status

EverlutionNavigationBundle是用于渲染通过标记服务动态注册的多个导航实例的Symfony组件包。

如果您只需要导航库,请查看everlutionsk/navigation

内容

安装

下载组件包

$ composer require everlutionsk/navigation-bundle:^2

启用组件包

<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...

            new Everlution\NavigationBundle\EverlutionNavigationBundle(),
        );

        // ...
    }

    // ...
}

配置

您可以指定自定义路由服务。路由服务必须实现Symfony\Component\Routing\RouterInterface

everlution_navigation:
    router_service: router.default # this is the default value 

使用

创建导航项

您可以通过实现Everlution\Navigation\Item\ItemInterface来简单地创建导航项。接口包含用于提供在模板中显示和翻译的标签的getLabel(): string方法和用于隐藏/显示项的isHidden(): bool方法。我们提供了3个特质,您可以通过默认显示(Everlution\Navigation\Item\ShownItemTrait)或隐藏(Everlution\Navigation\Item\HiddenItemTrait)项。第三个特质(Everlution\Navigation\Item\TogglableTrait)可以与提供显示/隐藏界面的Everlution\Navigation\Item\TogglableInterface一起使用。

Everlution\Navigation\Item命名空间内还提供了一些其他接口。通过实现这些接口,您可以为导航项添加行为。

请检查此命名空间中的接口。我们将在以下部分中进一步描述其中的一些。

示例

<?php

class SampleItem implements Everlution\Navigation\Item\ItemInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.sample.label');
    }
}

创建容器并注册项

实现导航项容器最简单的方法是扩展Everlution\Navigation\MutableContainer,它提供了组件包内部使用的预定义方法。您只需将导航项添加到容器中即可。

示例

<?php

class SampleNavigation extends Everlution\Navigation\MutableContainer
{
    public function __construct()
    {
        array_map(
            [$this, 'add'],
            [
                new \SampleItem(),
                // you can specify multiple items here
            ]
        );
    }
}

通过在服务容器中注册和标记您的导航,导航将自动添加到导航注册表中。

# services.yml
services:
    \SampleNavigation:
        tags:
            - {name: 'everlution.navigation', alias: 'sample_navigation'}

如您所见,我们使用everlution.navigation名称标记了服务,我们还提供了一个别名,这将有助于我们在模板中稍后引用导航。

从任何地方注册项到多个容器

有时您想一次将项注册到多个导航实例,或者您只想通过加载不同的Symfony组件包等方式自动将项注册到某些导航中。为此,您需要做的是让您的导航项实现Everlution\Navigation\Item\RegistrableItemInterface。通过实现此接口,您提供了一系列导航别名/FQCN,您希望在项中显示。

<?php

class ProductsItem implements Everlution\Navigation\Item\ItemInterface, Everlution\Navigation\Item\RegistrableItemInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('products');
    }

    public function getRegisteredContainerNames(): array
    {
        return [
            \DefaultNavigation::class,
            'non-existent-alias-which-will-be-ignored',
        ];
    }
}

注册过程非常简单,只需将项定义为服务并使用everlution.navigation_item标记即可。然后,在构建服务容器时,组件包通过依赖注入注册项。

services:
    EshopBundle\Navigation\ProductsItem:
        tags:
            - { name: 'everlution.navigation_item', alias: 'products_item' }

渲染导航

一旦您在导航注册表中注册了导航,您就可以在Twig模板中调用render_navigation()函数。第一个参数是您在定义服务时设置的导航别名,例如sample_navigation,或者是导航的完全限定类名,例如\SampleNavigation

示例

{{ render_navigation('sample_navigation') }}

我们已提供默认模板,该模板将以Bootstrap 4样式渲染导航。您可以在@EverlutionNavigation/bootstrap_navigation.html.twig中查看标记。如果您需要更改标记,可以提供自定义模板的路径作为第二个参数,例如:render_navigation('sample_navigation', '@Path/To/custom_template.html.twig')

将带有参数的路由添加到导航项

在没有生成实际URL的情况下渲染导航将毫无意义。对于与Symfony的Router的简单使用,您只需要实现Everlution\NavigationBundle\Bridge\Item\RoutableInterface并提供路由名称和路由参数。大多数时候,您不需要任何路由参数,因此您可以使用Everlution\NavigationBundle\Bridge\Item\EmptyRouteParametersTrait

示例

<?php

class ItemWithRoute implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.item_with_route.label');
    }

    public function getRoute(): string
    {
        return 'sample_route';
    }

    public function getParameters(): array
    {
        // if route does not expect any parameters just provide empty array
        // or use Everlution\NavigationBundle\Bridge\Item\EmptyRouteParametersTrait
        return [];
    }
}

该组件将在渲染导航时自动为您生成URL。

将动态参数添加到项路由

有时,您想要为多个用户生成相同的导航,例如,您只想更改一些路由参数。由于导航项只是普通的PHP对象,您可以通过构造函数注入任何您想要的,在服务容器中创建服务并注册服务容器中的项。

示例

<?php

class EditUserItem implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    /** @var UserIdProvider */
    private $idProvider;

    public function __construct(UserIdProvider $idProvider)
    {
        $this->idProvider = $idProvider;
    }

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.edit_user.label');
    }

    public function getRoute(): string
    {
        return 'edit_user_route';
    }

    public function getParameters(): array
    {
        return [
            'id' => $this->idProvider->getId(),
        ];
    }
}

现在使用我们的SampleNavigation注册导航项

# services.yml

services:
    # register EditUserItem
    \UserIdProvider: ~
    \EditUserItem:
        arguments:
            - '@\UserIdProvider'

    # add EditUserItem to SampleNavigation
    \SampleNavigation:
        calls:
            - ['add', ['@\EditUserItem']]
        tags:
            - {name: 'everlution.navigation', alias: 'sample_navigation'}

有时,您只想重用当前请求的一些参数。为此,我们准备了Everlution\NavigationBundle\Bridge\Item\RequestAttributesTrait。它通过构造函数自动注入Everlution\NavigationBundle\Bridge\Item\RequestAttributesContainer,可以从中获取当前主请求的属性。

示例

<?php

class ItemUsingRequestAttributes implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait, Everlution\NavigationBundle\Bridge\Item\RequestAttributesTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.request_attributes.label');
    }

    public function getRoute(): string
    {
        return 'request_attributes';
    }

    public function getParameters(): array
    {
        return [
            'id' => $this->requestAttributes->get('id'),
        ];
    }
}

Everlution\NavigationBundle\Bridge\Item\RequestAttributesTrait中我们还准备了一个有用的辅助方法,允许您从请求中复制具有相同名称的参数。

示例

<?php

class CopyRequestAttributes implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait, Everlution\NavigationBundle\Bridge\Item\RequestAttributesTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.copy_request_attributes.label');
    }

    public function getRoute(): string
    {
        return 'copy_request_attributes';
    }

    public function getParameters(): array
    {
        // you can also merge parameters from request with your own parameters if you wish
        return $this->copyRequestAttributes(['id', '_token']);
    }
}

突出显示导航中的当前项

当您想要在导航中突出显示当前项时,您需要实现Everlution\Navigation\Item\MatchableInterface并提供通过getMatches(): Everlution\Navigation\Match\MatchInterface[]的匹配数组。

我们准备了3种类型的匹配

  • Everlution\Navigation\Match\Voter\ExactMatch尝试找到精确匹配
  • Everlution\Navigation\Match\Voter\PrefixMatch尝试通过提供的前缀找到匹配项
  • Everlution\Navigation\Match\Voter\RegexMatch尝试通过提供的正则表达式找到匹配项

该组件将在当前URL或路由中尝试找到任何匹配项。在找到第一个匹配项后,匹配过程结束,因此您应该首先提供最通用的模式,然后是最具体的模式。您可以提供多个或零个每种匹配类型的实例。

示例

<?php

class MatchedItem implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface, Everlution\Navigation\Item\MatchableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.matched.label');
    }

    public function getRoute(): string
    {
        return 'edit_matched_route';
    }

    public function getParameters(): array
    {
        return [];
    }

    /**
     * @return \Everlution\Navigation\Match\MatchInterface[]
     */
    public function getMatches(): array
    {
        return [
            new \Everlution\Navigation\Match\Voter\ExactMatch('edit_matched_route'),
            new \Everlution\Navigation\Match\Voter\PrefixMatch('/matched'),
            new \Everlution\Navigation\Match\Voter\RegexMatch('.php$', 'i'),
        ];
    }
}

上面的示例将在当前请求的路由或URL恰好是edit_matched_route或以/matched开头或以.php结尾时(.php不区分大小写,例如,它也会匹配.PhP)突出显示导航项。

过滤导航项

有时,您想根据某些规则过滤导航项,例如,当不同角色的用户登录时。我们已经为导航提供了添加过滤器的能力。您需要做的一切是在您的导航中实现Everlution\Navigation\FilteredContainerInterface。通过实现getFilters(): Everlution\Navigation\Filter\NavigationFilterInterface[],您可以提供任何您想要的过滤器数组。

我们提供了Everlution\Navigation\Filter\FilterByRole,该过滤器将过滤掉支持角色提供者提供的角色的项。您可以使用Everlution\Navigation\Filter\RoleProvider或创建自定义提供者,通过实现Everlution\Navigation\Filter\RolesProviderInterface。要按角色过滤项,您需要在导航中的每个导航项上实现Everlution\Navigation\Item\HasSupportedRolesInterface

示例

<?php

class ItemFilteredByRole implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface, Everlution\Navigation\Item\HasSupportedRolesInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.item_filtered_by_role.label');
    }

    public function getRoute(): string
    {
        return 'edit_filtered_by_role_route';
    }

    public function getParameters(): array
    {
        return [];
    }

    public function getSupportedRoles(): array
    {
        return [
            'PUBLIC_ACCESS',
        ];
    }
}

上面的项仅在用户从应用程序注销时在导航中渲染。导航对象将类似于以下对象

<?php

class FilteredNavigation extends \Everlution\Navigation\MutableContainer implements Everlution\Navigation\FilteredContainerInterface
{
    /** @var \Everlution\Navigation\Filter\RolesProviderInterface */
    private $roleProvider;

    public function __construct(\Everlution\Navigation\Filter\RolesProviderInterface $rolesProvider)
    {
        $this->roleProvider = $rolesProvider;
        
        array_map(
            [$this, 'add'],
            [
                new \ItemFilteredByRole(),
                // you can specify multiple items here
            ]
        );
    }

    public function getFilters(): array
    {
        return [
            new \Everlution\Navigation\Filter\FilterByRole($this->roleProvider),
        ];
    }
}

在这种情况下,例如当您的FilteredNavigation在过滤器数组中返回FilterByRole时,所有添加到导航中的导航项都必须实现Everlution\Navigation\Item\HasSupportedRolesInterface接口。为了避免异常,您可以在执行FilterByRole过滤器之前,通过在过滤器之前添加Everlution\Navigation\Filter\RemoveNotSupportedRoleFilter来链式调用您的过滤器,这将移除所有未实现Everlution\Navigation\Item\HasSupportedRolesInterface接口的项目。由于过滤器按顺序执行,因此在FilterByRole过滤器运行时(它期望所有项目都实现Everlution\Navigation\Item\HasSupportedRolesInterface接口),所有其他项目已经被过滤掉了,因此不会抛出异常。

<?php

    // ...
    public function getFilters(): array
    {
        return [
            new Everlution\Navigation\Filter\RemoveNotSupportedRoleFilter(),
            new \Everlution\Navigation\Filter\FilterByRole($this->roleProvider),
        ];
    }
    // ...

我们还提供了非严格过滤器Everlution\Navigation\Filter\FilterByRoleNonStrictly,它将忽略未实现Everlution\Navigation\Item\HasSupportedRolesInterface接口的项目,导致无论提供什么角色,这些项目都会在最终导航中显示。

排序导航项

在渲染导航时,导航容器已转换为Everlution\Navigation\OrderedContainer。在这个容器中,所有实现Everlution\Navigation\Item\SortableInterface的项目都将通过简单的比较函数按升序排序,其他所有项目则按其原始顺序附加到排序项目的末尾。通常,项目的顺序是按它们添加到容器中的顺序来排序的。

通过实现Everlution\Navigation\OrderedContainer,您定义了一个指定有序容器中位置的数字(正数或负数)——顺序是通过从最小到最大的顺序指定这些数字来确定的。

<?php

class FirstItem implements Everlution\Navigation\Item\ItemInterface, Everlution\Navigation\Item\SortableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('first');
    }

    public function getOrder(): int
    {
        return -100;
    }
}

class LastItem implements Everlution\Navigation\Item\ItemInterface, Everlution\Navigation\Item\SortableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('last');
    }

    public function getOrder(): int
    {
        return 100;
    }
}

class MiddleItem implements Everlution\Navigation\Item\ItemInterface, Everlution\Navigation\Item\SortableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('middle');
    }

    public function getOrder(): int
    {
        return 0;
    }
}

添加嵌套项

当您需要渲染带有嵌套项目的导航时,您需要实现Everlution\Navigation\Item\NestableInterface。在您的项目提供父类的完全限定类名。没有父项的根导航项不应实现Everlution\Navigation\Item\NestableInterface

默认情况下,我们提供了将仅渲染两层导航的模板,带有Bootstrap 4样式。然而,您可以创建自己的模板并将其作为参数提供给Twig模板中的render_navigation()函数。

示例

<?php

class ItemWithParent implements Everlution\Navigation\Item\ItemInterface, Everlution\NavigationBundle\Bridge\Item\RoutableInterface, Everlution\Navigation\Item\NestableInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\Navigation\Item\ItemLabel('navigation.item_with_parent.label');
    }

    public function getRoute(): string
    {
        return 'edit_item_with_parent_route';
    }

    public function getParameters(): array
    {
        return [];
    }

    public function getParent(): string
    {
        return \SampleItem::class;
    }
}

在您的导航容器中,您不需要创建任何嵌套结构。只需按常规添加项目即可。嵌套结构将在Twig函数中为您创建。

渲染面包屑

当您将EverlutionNavigationBundle提供的所有优点结合到您的导航项中时,您可以渲染面包屑。我们准备了render_breadcrumbs()函数,该函数可以在Twig模板中使用。作为第一个参数,您需要提供导航名称。作为第二个参数,如果您不喜欢Bootstrap 4面包屑的外观,您可以提供自定义模板的路径。

面包屑将检查当前导航项并渲染其所有前身。为了使面包屑功能正常工作,您需要实现Everlution\Navigation\Item\MatchableInterfaceEverlution\Navigation\Item\NestableInterfaceEverlution\NavigationBundle\Bridge\Item\RoutableInterface,以及Everlution\Navigation\Item\ItemInterface

您应该创建尽可能描述性的结构,例如,您的项目的层次结构应该非常详尽。

翻译项标签

默认情况下,在渲染导航或面包屑时,所有标签都会被翻译。您可以在您的自定义模板中使用注入的helper变量提供的辅助方法,只需调用helper.getLabel(item),您的标签就会被翻译。

有时您可能想要在翻译字符串中添加一些额外的参数。在这种情况下,您的标签需要实现Everlution\NavigationBundle\Bridge\Item\TranslatableItemLabelInterface。然后您可以为翻译提供带有参数的数组,这些参数将被注入到翻译中。您还可以在Twig模板中的helper.getLabel()方法的第二个和第三个参数中设置翻译域和地区。

示例

<?php

class TranslatableLabelItem implements Everlution\Navigation\Item\ItemInterface
{
    use Everlution\Navigation\Item\ShownItemTrait;

    /** @var \ParameterProvider */
    private $parameterProvider;

    public function __construct(\ParameterProvider $provider)
    {
        $this->parameterProvider = $provider;
    }

    public function getLabel(): \Everlution\Navigation\Item\ItemLabelInterface
    {
        return new \Everlution\NavigationBundle\Bridge\Item\TranslatableItemLabel(
            'navigation.translatable_label_item.label',
            ['%first_parameter%' => $this->parameterProvider->getParameter()]
        );
    }
}
# messages.en.yml
navigation:
    translatable_label_item:
        label: 'Following parameter is provided by \ParameterProvider:  %first_parameter%'

渲染单个导航项

如果您只想在Twig模板中渲染单个导航项,您需要做的是将实现了Everlution\Navigation\Item\ItemInterface的项注册为服务,并使用适当的别名(如以下示例所示)标记为everlution.navigation_item

services:
    AppBundle\Navigation\LogoutItem:
        tags:
            - { name: 'everlution.navigation_item', alias: 'logout_item' }

然后您可以在提供已注册项的别名时调用预定义的Twig函数。可选地,您可以定义默认提供的Twig模板。

{{ render_item('logout_item') }}

故障排除

当您注册别名时,别名不会被注册

Alias is not registered exception

有时当您使用Symfony的自动注入功能以简化服务的注册时,可能会出现上述异常。在这种情况下,我们已在外部文件navigaiton.yml中注册了main_navigation,该文件被导入到主services.yml文件中。导航项和导航实例在AppBundle\Navigation命名空间内实现,如以下代码片段所示。

# app/config/services/navigation.yml

services:
    _defaults:
        autowire: true
        autoconfigure: true

    # navigations
    AppBundle\Navigation\MainNavigation:
        tags:
            - { name: 'everlution.navigation', alias: 'main_navigation' }
# app/config/services.yml

imports:
    - { resource: "services/navigation.yml" }

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

    # ...

这里的问题是,来自navigaiton.yml的标记服务在没有标记的情况下被主自动注入配置覆盖,这使得束无法收集标记服务。解决方案是排除AppBundle\Navigation的默认自动注入。

# app/config/services.yml

# ...

services:
    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Entity,Repository,Tests,Navigation}' # add Navigation here

    # ...

待办事项

  • 动态生成导航(例如实现项提供者)