everlutionsk / navigation-bundle
Symfony导航组件包
Requires
- php: ^8.0.2
- everlutionsk/navigation: ^2.4
- symfony/config: >=6.0
- symfony/dependency-injection: >=6.0
- symfony/framework-bundle: >=6.0
- symfony/http-foundation: >=6.0
- symfony/http-kernel: >=6.0
- symfony/routing: >=6.0
- symfony/security-core: >=6.0
- symfony/translation: >=6.0
- symfony/twig-bundle: >=6.0
README
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\MatchableInterface
、Everlution\Navigation\Item\NestableInterface
和Everlution\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') }}
故障排除
当您注册别名时,别名不会被注册
有时当您使用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 # ...
待办事项
- 动态生成导航(例如实现项提供者)