loicpennamen/entity-datatables-bundle

快速在Symfony实体上实现强大的DataTables搜索引擎。

安装: 389

依赖: 0

建议者: 0

安全: 0

类型:symfony-bundle

v1.1.0 2023-11-01 17:01 UTC

This package is auto-updated.

Last update: 2024-09-15 11:21:30 UTC


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提供jQueryDatatables资产。这相当不可靠,因为它缺乏对资产加载的精细控制,并且可能会干扰你自己的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后缀。

  1. 在您的控制台中要求资产。
    npm install --dev jquery \
    expose-loader \
    datatables datatables.net \
    datatables.net-fixedheader \
    datatables.net-responsive \
    datatables.net-select
    
  2. 在您的编译应用程序中要求这些资产

    // 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';
    
  3. 别忘了编译您的资产,例如使用npm watch
  4. 您已准备好出发。默认模板在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"。以下是根据不同配置和搜索查询的不同结果

JohnDoeJohn DoeJane DoePumpkin
EVERY_WORDMatchMatchMatch
ANY_WORDMatchMatchMatchMatch
FULL_STRINGMatch

TODO

  • 每列搜索字符串边界 + 搜索字符串分割 + 空值最后(已存在)+ 文档
  • 允许跨属性筛选:添加一个选项以允许在多个属性中搜索分割的筛选字符串。示例:“John France”。
  • API用于动态更新仓库过滤选项/列过滤选项。