loicpennamen / entity-datatables-bundle
快速在Symfony实体上实现强大的DataTables搜索引擎。
Requires
- php: >=7.4
- symfony/framework-bundle: >=6.2
- twig/intl-extra: >=3
Replaces
README
🚀 只需几分钟即可创建强大的实体研究表。
此存储库允许快速设置DataTables,用于持久化的Symfony实体,并附带一个非常强大的ajax过滤解决方案。它包括:
- Ajax过滤器用于持久化实体 - 即使是关联实体
- 在表格中智能默认显示实体属性
- 生产就绪的集成模板
- CDN选项,以实现最快集成
如何使用
使用`composer require loicpennamen/entity-datatables-bundle
`安装。
如果您正在使用Symfony Flex,则Bundle已配置。了解更多信息这里。
从服务中获取列配置
您需要两个控制器方法
- 一个用于显示包含表格的页面
- 另一个作为API以检索ajax结果。
由于两种方法都需要访问表格的配置,建议使用提供该配置的服务。您可以为项目中的每个DataTables创建此类方法。
例如,让我们有一个显示并过滤我们的用户的DataTables。让我们创建一个UserService,其中有一个返回我们的表格配置的方法
<?php
// src/Services/UserService.php
namespace App\Services;
// Helper class to configure our table's columns
use LoicPennamen\EntityDataTablesBundle\Entity\DtColumn;
class UserService
{
public function getDataTableColumns(): array
{
$columns = [];
$col = new DtColumn();
$col->setSlug('id'); // Required: must be unique in each DataTable
$col->setLabel("User ID"); // The label displayed on top of the DataTable's column
$col->setName("User ID in database"); // A longer text to display details about the column
$col->setSortingKey('user.id'); // What field will be used for sorting (see repository configuration)
// Store in the array
$columns[] = $col;
$col = new DtColumn();
$col->setSlug('email');
$col->setLabel('Email');
$col->setName("User Email");
$col->setSortingKey('user.email');
$columns[] = $col;
// You can also have columns to display arrays, dates or any object
// For instance here, you can filter by user permissions (roles)
// The cell's template will handle how to display the data
$col = new DtColumn();
$col->setSlug('roles');
$col->setLabel('Roles');
$col->setName("User permissions");
$col->setSortingKey('user.roles');
$columns[] = $col;
// Here, we define a column that is NOT linked to the User's properties
// We can use it as a placeholder for a toolbar: Read, Update, Delete...
$col = new DtColumn();
$col->setSlug('tools');
$col->setSortable(false); // Don't forget to disable sorting in this case
$columns[] = $col;
return $columns;
}
}
显示表格
现在我们已经有了检索列的方法,让我们显示一个表格。在我们的控制器中
<?php
// src/Controller/UserController.php
namespace App\Controller;
use App\Services\UserService;
// ...
class UserController extends AbstractController
{
#[Route('/user/search', name: 'app_user_search')]
public function search(UserService $userService): Response
{
// List of DtColumn objects
$columns = $userService->getDataTableColumns();
return $this->render('user/search.html.twig', [
'columns' => $columns,
]);
}
// We will already need this route for the future API
#[Route('/api/user/search', name: 'app_user_search_api')]
public function searchApi(){
// ...
}
}
并在您的模板中,在您希望表格出现的地方包含以下片段。
{% include '@LoicPennamenEntityDataTables/table.html.twig' with {config: {
columns: columns,
dataUrl: path('app_user_search_api'),
}} %}
默认表格模板使用可以在config
对象中覆盖的参数
columns
:必需的DtColumn
对象的数组。dataUrl
:API端点的路径 - 必需的。useCdn
:布尔值,用于在模板中通过CDN集成DataTables的javascript和CSS文件。默认为true
。useJQueryCdn
:布尔值,用于在模板中通过CDN集成JQuery。默认为true
。additionalData
:要发送到API端点的附加参数。tableId
:表格的DOM ID,自动生成。tableClasses
:为<table>
元素添加的附加类名。translationFile
:例如asset('./datatables.fr.json')
。有关更多信息,请参阅这里。
给技术人员的提示:您还可以复制/粘贴模板的内容,以自由定制表格:vendor/loicpennamen/entity-datatables-bundle/Resources/views/table.html.twig
获取结果
此时,您应该看到一个空白表格,其中包含列。并且可能有一个JavaScript错误,因为ajax API尚未设置。
我们需要一个方法来获取DataTables的POST值,并返回一个包含每行和单元格内容的JSON响应。为此,让我们使用一种特殊的EntityRepository类型,它基于DataTables变量处理所有过滤和分页问题。
打开您的实体存储库,并使其扩展以下类
LoicPennamen\EntityDataTablesBundle\Repository\DatatablesSearchRepository
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use LoicPennamen\EntityDataTablesBundle\Repository\DatatablesSearchRepository;
// Update the extension
class UserRepository extends DatatablesSearchRepository
{
// Configure the search options
public function __construct(ManagerRegistry $registry)
{
$this->setEntityAlias('user');
$this->addSearchField('user.id');
$this->addSearchField('user.email');
$this->addSearchField('user.roles');
parent::__construct($registry, User::class);
}
}
setEntityAlias
方法强制执行,并将用于进一步的配置。理想情况下,它是一个camelCase
字符串。请保持简单。addSearchField
方法设置了将进行检查的实体属性。它们必须是camelCase
。
更复杂的配置,如关联实体,将在本文档中进一步介绍。这很简单。
让我们回到控制器来更新API方法:它的目的是过滤我们的用户并返回表格数据。我们将准备查询配置,然后获取结果,最后将返回的内容格式化为行和单元格的数组以供显示。
<?php
// src/Controller/UserController.php
namespace App\Controller;
use App\Services\UserService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use LoicPennamen\EntityDataTablesBundle\Services\EntityDataTablesService;
// ...
class UserController extends AbstractController
{
// ...
#[Route('/api/user/search', name: 'app_user_search_api')]
public function searchApi(
Request $request,
EntityManagerInterface $em,
UserService $userService,
EntityDataTablesService $datatableService
): JsonResponse
{
// Our custom repository
$repo = $em->getRepository(User::class);
// Let's retrieve the column's configuration
$tableColumns = $userService->getDataTableColumns();
// And convert POST data to useable options in our custom repository
$options = $datatableService->getOptionsFromRequest($request, $tableColumns);
// All the magic happens here, with search, pagination and all...
$entities = $repo->search($options);
return $this->json([
// This handles all the data formatting
'data' => $datatableService->getTableData($entities, $tableColumns),
// This counts all results in the database
'recordsTotal' => $repo->countSearchTotal($options),
// This counts all results within applied search
'recordsFiltered' => $repo->countSearch($options),
]);
}
}
此时,你应该能看到一个正在工作的表格,你的用户名,以及可能一些内容不合适的单元格。在我们的例子中,“角色”列可能填充了[roles: 警告: 数组转换为字符串]
。这是因为提供的服务不知道如何格式化某些值,如数组或对象。
如何格式化单元格?
默认情况下,EntityDataTablesService->getTableData()
为每个单元格智能地创建HTML字符串。现在,你可能想以花哨的方式处理数组属性(如用户角色):例如,为管理员角色添加一些红色,或者在每个角色之间添加逗号。
为此,让我们为每个列创建一个twig模板。记住你在Repository中定义的slug
- 在我们的例子中,我们使用了user
。在你的templates
文件夹中,创建一个名为user
的文件夹(或你为实体选择的任何其他slug
)。然后添加一个名为cell-roles.html.twig
的文件。换句话说,文件命名约定是
[entitySlug]/cell-[propertyOfTheEntity].html.twig
在这个模板中,你可以通过其slug名称访问你的对象。在我们的例子中,twig变量user
包含行的用户
对象。
// templates/user/cell-roles.html.twig
{# @var user \App\Entity\User #}
{% for role in user.roles %}
<span>{{ role }}</span>{{ loop.last ? '' : ', ' }}
{% endfor %}
此时,你的roles
列应显示一个逗号分隔的用户角色列表。你可以根据喜好进行翻译和格式化。
你可以使用这种方法来显示任意列的内容。在我们的例子中,我们定义了一个tools
列。这是一种方便的方法来添加菜单。
// templates/user/cell-tools.html.twig
{# @var user \App\Entity\User #}
<a href="#">Update</a> | <a href="#">Delete<a>
如何更改单元格的模板文件夹?
如果你不想使用实体slug名称作为模板文件夹名称,你可以在`getTableData
`方法的第三个参数中定义不同的文件夹名称。
<?php
// src/Controller/UserController.php
namespace App\Controller;
// ...
class UserController extends AbstractController
{
// ...
#[Route('/api/user/search', name: 'app_user_search_api')]
public function searchApi(Request $request, EntityManagerInterface $em, UserService $userService, EntityDataTablesService $datatableService): JsonResponse
{
// ...
return $this->json([
'data' => $datatableService->getTableData(
$entities,
$tableColumns,
// Let's define an arbitrary, 2-level template folder
'user-datatables/table-cells'
),
'recordsTotal' => $repo->countSearchTotal($options),
'recordsFiltered' => $repo->countSearch($options),
]);
}
}
如何为单个列定义自定义模板?
除了默认模板命名约定和更改整个表的模板文件夹的可能性,你还可以为列设置一个特定的模板路径。`DtColumn
`对象有一个`setTemplate()
`方法,该方法接受一个自定义模板文件的路径作为参数。
<?php
// src/Services/UserService.php
namespace App\Services;
use LoicPennamen\DataTablesBundle\Entity\DtColumn;
class UserService
{
public function getDataTableColumns(): array
{
$columns = [];
$col = new DtColumn();
$col->setSlug('id');
$col->setLabel("User ID");
$col->setName("User ID in database");
$col->setSortingKey('user.id');
// Path to the file in your /templates directory (or any listed Twig directory)
$col->setTemplate('user/id-with-notifications.html.twig');
$columns[] = $col;
return $columns;
}
}
模板配置优先级
这些模板定义方法中的每一个都优先于前一个。这意味着
- 如果为列设置了特定模板,但文件未找到,则脚本在自定义文件夹中查找模板。
- 如果设置了自定义模板文件夹,但文件未找到,则脚本在以slug命名的约定中查找模板文件。
- 如果没有找到以slug命名的约定模板文件,则脚本猜测显示单元格值的最佳方式。
改进资产集成
为了快速集成,该包默认通过CDN提供jQuery和Datatables资产。这相当不可靠,因为它缺乏对资产加载的精细控制,并且可能会干扰你自己的JQuery实现。强烈建议在页面模板中禁用CDN选项。
{% include '@LoicPennamenEntityDataTables/table.html.twig' with {config: {
columns: columns,
dataUrl: path('app_user_search_api'),
useCdn: false,
useJQueryCdn: false,
}} %}
然后,以你喜欢的风味集成所需的资产(阅读更多)。
截至2023年3月,这是一种实现方式。此方法适用于使用Webpack Encore的Symfony资产!以下示例假设您已经设置了您的Webpack配置。如果没有,请阅读本指南。
Bootstrap用户注意事项:我更喜欢使用Bootstrap 5版本的DataTables。如果您也这样做,请为每个DataTables包名添加-bs5
后缀。
- 在您的控制台中要求资产。
npm install --dev jquery \ expose-loader \ datatables datatables.net \ datatables.net-fixedheader \ datatables.net-responsive \ datatables.net-select
在您的编译应用程序中要求这些资产
// assets/js/app.js // Load Jquery package import $ from "expose-loader?exposes=$,jQuery!jquery"; // Load DataTables package with plugins require('datatables.net'); require('datatables.net-fixedheader'); require('datatables.net-responsive'); require('datatables.net-select'); // Import DataTables styles import 'datatables/media/css/jquery.dataTables.css';
- 别忘了编译您的资产,例如使用
npm watch
。 - 您已准备好出发。默认模板在DOM加载后启动DataTablejavascripts,因此您的应用程序脚本在DOM中的位置不应成为问题。
如何处理关联实体
在我们的示例中,我们过滤用户对象。假设每个用户都与一个或多个地址实体相关联,并且我们想在表中显示他们的城市和国家。如果有一个ORM关联,在仓库中实现起来非常简单
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use LoicPennamen\EntityDataTablesBundle\Repository\DatatablesSearchRepository;
// Update the extension
class UserRepository extends DatatablesSearchRepository
{
// Configure the search options
public function __construct(ManagerRegistry $registry)
{
$this->setEntityAlias('user');
$this->addSearchField('user.username');
// ...
// Add a LEFT JOIN query: allow the address to be NULL
$this->addLeftJoin('user.address', 'address');
// Add a INNER JOIN query: the User will not be matched if its address is NULL
$this->addJoin('user.address', 'address');
parent::__construct($registry, User::class);
}
}
addJoin()
和addLeftJoin()
方法的第二个参数定义了连接实体的“别名”。这个别名可以用于DtColumn
对象来创建一个可排序的列。
在我们的示例中,Address实体包含一个用于国家名称和城市名称的string
属性。让我们创建列来显示它们
<?php
// src/Services/UserService.php
namespace App\Services;
use LoicPennamen\DataTablesBundle\Entity\DtColumn;
class UserService
{
public function getDataTableColumns(): array
{
$columns = [];
$col = new DtColumn();
$col->setSlug('username');
$col->setLabel('Username');
$col->setSortingKey('user.username');
$columns[] = $col;
$col = new DtColumn();
$col->setSlug('city');
$col->setLabel('City');
$col->setSortingKey('address.city');
$columns[] = $col;
$col = new DtColumn();
$col->setSlug('country');
$col->setLabel('Country');
$col->setSortingKey('address.country');
$columns[] = $col;
return $columns;
}
}
注意:这也适用于多级关系!
例如,如果您的用户在OneToMany关系中拥有多个地址,您可以将slug address
重命名为addresses
以使其更清晰。然后可以按任何城市 | 国家名称进行筛选。
如何向API添加自定义POST数据?
提供的默认模板包含一个additionalData
属性,可以向控制器的API方法添加任意POST值。在您的模板中
{% include '@LoicPennamenEntityDataTables/table.html.twig' with {config: {
columns: columns,
dataUrl: path('app_user_search_api'),
additionalData: {
propertyOne: 'value 1',
propertyTwo: 'value 2'
}
}} %}
高级配置
可以在仓库内部应用更多配置以自定义通过Datable“搜索”字段发出的每个请求的过滤行为。这些配置是仓库级的。
边界:以...开头,以...结尾,包含,精确匹配...
边界定义在实体的每个可过滤属性(即“列”)中搜索查询字符串的位置。
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use LoicPennamen\EntityDataTablesBundle\Repository\DatatablesSearchRepository;
class UserRepository extends DatatablesSearchRepository
{
public function __construct(ManagerRegistry $registry)
{
$this->setEntityAlias('user');
$this->addSearchField('user.username');
// ...
// By default: one searchable property contains the string anywhere
$this->setSearchStringBoundaries(self::SEARCH_STRING_BOUNDARIES_CONTAINS);
// one searchable property contain the exact, full string
$this->setSearchStringBoundaries(self::SEARCH_STRING_BOUNDARIES_EXACT);
// one searchable property starts with the string
$this->setSearchStringBoundaries(self::SEARCH_STRING_BOUNDARIES_STARTS_WITH);
// one searchable property ends with the string
$this->setSearchStringBoundaries(self::SEARCH_STRING_BOUNDARIES_ENDS_WITH);
parent::__construct($registry, User::class);
}
}
分割:使用整个字符串,使用每个单词,使用任何单词...
现在,如果在搜索输入中添加空格或逗号,单词是独立搜索,还是被视为单个字符串?
在仓库内部,setSearchStringDivision()
方法允许几种行为
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use LoicPennamen\EntityDataTablesBundle\Repository\DatatablesSearchRepository;
class UserRepository extends DatatablesSearchRepository
{
public function __construct(ManagerRegistry $registry)
{
$this->setEntityAlias('user');
$this->addSearchField('user.username');
// ...
// By default: return entities containing all the words in search field (in one single property)
$this->setSearchStringDivision(self::SEARCH_STRING_DIVISION_EVERY_WORD);
// Returns entities containing ANY words of the search input value (in one single property)
$this->setSearchStringDivision(self::SEARCH_STRING_DIVISION_ANY_WORD);
// Does not divide the string, and filters entities containing the whole input value
$this->setSearchStringDivision(self::SEARCH_STRING_DIVISION_FULL_STRING);
parent::__construct($registry, User::class);
}
}
假设我们有一个名为John Doe的用户,并且边界设置为"CONTAINS"。以下是根据不同配置和搜索查询的不同结果
John | Doe | John Doe | Jane Doe | Pumpkin | |
---|---|---|---|---|---|
EVERY_WORD | Match | Match | Match | ||
ANY_WORD | Match | Match | Match | Match | |
FULL_STRING | Match |
TODO
- 每列搜索字符串边界 + 搜索字符串分割 + 空值最后(已存在)+ 文档
- 允许跨属性筛选:添加一个选项以允许在多个属性中搜索分割的筛选字符串。示例:“John France”。
- API用于动态更新仓库过滤选项/列过滤选项。