grigorygraborenko / recursive-admin
symfony/doctrine 的一个自动、基于关联的、可定制的管理面板
README
这个包自动提供了一个管理仪表板,用于管理所有的 doctrine 实体。它将每个实体展示为一个实例表,并且每个关联都可以递归地扩展到新的表中。通过注解可以指定实体、字段和动作的精细粒度权限。它有一个基于 ajax 的 React 前端,并且可以为全局和每个实体动作提供可定制的入口点。
请注意,这个包仍在开发中,因此它可能要到 2017 年中旬才会看起来不错。它目前被作者正在开发的两个项目使用:一个是商业项目,另一个是非营利项目。Sonata 管理员被发现不能满足这两个需求。
安装
可以通过 composer 安装这个库
composer require grigorygraborenko/recursive-admin
或者直接将其添加到你的 composer.json 文件中
"grigorygraborenko/recursive-admin": "master"
在 AppKernel.php 中包含这个包
$bundles = [ ... new Grigorygraborenko\RecursiveAdmin\RecursiveAdminBundle(),
然后在你的 config.yml 中某处,添加实体管理的最小默认权限
recursive_admin: default_permissions: entity: [ROLE_ADMIN] read: [ROLE_ADMIN] write: [ROLE_SUPER_ADMIN] create: [ROLE_SUPER_ADMIN] destroy: [ROLE_SUPER_ADMIN] back_route: YOUR_HOME_PAGE_ROUTE
“back_route” 属性是可选的,它将添加一个返回按钮到仪表板,链接到相应的路由名称。在你的 routing.yml 中添加仪表板的路由
recursive_admin: resource: "@RecursiveAdminBundle/Controller/" type: annotation prefix: /YOUR-URL-HERE
然后最后,确保在 security.yml 中防火墙这个路由
access_control: ... - { path: ^/YOUR-URL-HERE/, role: ROLE_ADMIN }
如果你不执行这个最后一步,你的非管理员用户可能能够访问一个没有任何实体的空白页面。
还有一个可选的“testing”属性
recursive_admin: testing: allow_fake_data_creation: "%recursive_admin.testing.fake_data%" permission: ROLE_SUPER_ADMIN
强烈建议您不要启用此功能,或者严格限制。如果“allow_fake_data_creation”设置为 true,它将显示每个实体都有一个按钮,允许用户创建任意数量的带有随机数据和关联的假实体实例。这对于测试目的很有用,特别是规模测试。
使用
点击并尝试。稍后将会完善此文档。
开发
有许多方法可以自定义开箱即用的行为。
注解
每个实体都可以有两种类型的注解:按实体和按字段。两者都使用 @Admin 注解,并且有类似选项。
具有字段注解的实体注解可能看起来像这样
use Grigorygraborenko\RecursiveAdmin\Annotations\Admin; /** * @ORM\Entity * @Admin(entity=["ROLE_ADMIN", "ROLE_TESTER"], sortBy="name", priority=1000, label="Result of Test") */ class TestResult { /** * @ORM\Column(type="string") * @Admin(read="ROLE_ADMIN", priority=10, label="Test Name") */ protected $name;
以下是注解的各种选项及其效果。所有这些都是可选的,即使实体或其字段上没有 admin 注解,它也可以显示在管理仪表板中。其中大部分是权限字符串/数组。如果您想不允许任何用户查看或编辑某个字段,将该权限设置为“none”或其他不存在于用户角色中的字符串。这可以在实体级别全面禁止权限,然后选择性地允许某些字段,反之亦然。如果既未设置实体级别的权限也未设置字段级别的权限,则使用您在配置文件中指定的默认权限。
"entity" 选项
此选项仅对实体有效,对于字段被忽略。它需要一个字符串或字符串数组,表示用户角色。这决定了哪个级别的用户可以看到实体的存在。
"read" 选项
需要一个字符串或字符串数组,表示用户角色。在字段上,它决定该字段及其数据是否对用户可见。在实体上,它决定了该实体所有字段的默认读取权限级别。按字段注解将优先于按实体注解。
"write" 选项
需要字符串或字符串数组表示的用户角色。在字段上,它决定该字段及其数据是否可由该用户编辑。在实体上,它确定该实体所有字段默认的写权限级别。每个字段的注解将优先于每个实体的注解。
"label" 选项
需要字符串。对于字段,这是数据表中列顶部的文本。对于实体,这是您点击以查看实体数据的按钮文本。
"priority" 选项
需要整数。对于字段,这决定了显示的列顺序。对于实体,它决定了顶部列出的实体顺序。数值最高的首先显示。实体具有负优先级的将默认隐藏在“显示更多”按钮后面。
"create" 选项
需要字符串或字符串数组表示的用户角色。在字段上,它决定该用户是否可以创建此实体。在字段上,它仅适用于一对一类型的关联,并确定此用户是否可以创建此子实体,同时父关联已填写。
例如,如果用户可以有多个订单,用户表将显示一个列出来自订单列表。如果此字段带有此选项的注解,则将有一个添加按钮,该按钮将弹出一个模态窗口,您可以在其中创建一个新订单,该用户已根据您点击的用户预先选择。
"destroy" 选项
此选项仅适用于实体,对字段无效。需要字符串或字符串数组表示的用户角色。它决定用户是否可以销毁此实体。
"sortBy" 选项
此选项仅适用于实体,对字段无效。它需要一个字符串,并且必须是字段名。如果另一个实体引用此实体,并且您通过该关联对该实体进行排序,则此字段将用于确定排序顺序。一个强制性的副作用是它还决定了另一个实体如何渲染关联。
例如,如果用户可以有多个订单,订单表将显示一个列来显示拥有该订单的用户。排序该列将按用户的“sortBy”字段排序。如果它是该用户的电子邮件,则订单表将显示一个可展开的电子邮件列,排序它将在订单表上强制进行字母顺序的电子邮件排序。
操作
开发人员可以指定两种自定义操作:全局和按实体。全局操作将出现在顶部栏中,可以根据实体隐藏或显示。按实体操作将出现在表中每一行。
全局操作
在recursive_admin下的config.yml文件中添加一个具有公共函数名的服务列表
global_actions: - service: SERVICE_NAME method: SERVICE_FUNCTION - service: OTHER_SERVICE_NAME method: OTHER_SERVICE_FUNCTION
这些函数将在您查看新实体、加载页面或激活全局功能时每次被调用。这些函数将接收两个参数,即服务容器和当前登录的admin用户。用户参数是为了在您希望根据用户数据创建或省略自定义操作的情况下使用。它不是权限管理所必需的,下面将进行描述。以下是一个示例服务全局操作函数
public function SERVICE_FUNCTION($container, $admin) { $user_input = array( "api" => array( "type" => "select" ,"label" => "Which API?" ,"choices" => array( array("label" => "Internal", "value" => "int_01") ,array("label" => "External", "value" => "ext_02") ,array("label" => "All of them", "value" => "*") ) ) ,"num_tests" => array( "type" => "integer" ,"label" => "How many times should the tests run?" ,"default" => 1 ,"required" => true ) ); return array( "tests" => array( "callback" => "runTests" ,"label" => "Test APIs" ,"description" => "Run automatic tests for each API" ,"permission" => "ROLE_ADMIN" ,"classes" => "btn btn-xs btn-warning" ,"visible" => array('AppBundle\Entity\ApiEntry', 'AppBundle\Entity\TestResult') ,"input" => $user_input ) ,"more_tests" => array( "callback" => "runMoreTests" ,"label" => "Additional Test APIs" ,"description" => "Run extra automatic tests for each API" ,"permission" => "ROLE_SUPER_ADMIN" ,"input" => $super_user_input ) ); }
此函数应返回一个键值数组的全局操作。每个操作都是一个键值数组,将作为管理员仪表板顶部的按钮显示。按钮可以使用可选的“classes”键使用bootstrap类进行样式化,文本由“label”键确定。
请注意,由于此函数在任何全局操作执行后被调用,因此您可以为按钮生成动态、有意义的标签和样式,使其具有上下文敏感性。
"visible" 键是可选的,它是一个数组,包含在全局操作下可见的完全限定的实体名称。 "description" 键用于当您点击按钮时弹出的模态框顶部的文本。 "input" 键相当复杂,将在下面的 "Action Input Specification" 部分进行描述。
回调函数接收三个参数:服务容器、当前登录的管理员和输入键值数组。它应该返回一个字符串,表示发生错误,或者返回一个键值数组表示成功。返回 "report" 键将在报告字符串上显示成功模态框,并格式化以处理换行。如果将 "refresh" 键设置为 true,则将重新加载模态框并清除字段。您还可以在 "file" 下返回一个字符串,该字符串将下载该字符串的内容作为文件。这对于生成 CSV 文件很有用。
public function runTests($container, $admin, $input) { ... if($some_error) { return "Tests had an error of some sort"; } ... return array("report" => "Everything went well"); }
您还可以使用以下结构在 "file" 键下返回一个键值数组
return array("file" => array("name" => "test_results.csv", "contents" => "test,result\ninternal,passed\nexternal,passed\n"));
实体操作
为了为实体定义自定义实体操作,您必须定义两个函数。第一个是在实体类中名为 adminStatic 的静态公共函数。此函数接收服务容器和管理员用户,并应返回一个键值数组。
第二个是一个非静态公共函数,它接收相同的输入并返回各种操作的键值规范。例如
public static function adminStatic($container, $admin) { return array( "headers" => array( array("label" => "Action", "permission" => "ROLE_ADMIN", "priority" => 50) ) ,"create" => array( "callback" => "adminCreate" ,"input" => $create_input ) ); } public function adminActions($container, $admin) { if(!$this->isBannable()) { return array(); // no custom actions at all } $ban_input = array( "reason" => array( "type" => "boolean" ,"label" => "Ban forever?" ,"default" => false ) ); return array( "ban" => array( "heading" => "Action" ,"callback" => "adminBan" ,"permission" => "ROLE_ADMIN" ,"class" => ($this->is_unbanned ? "btn-danger" : "btn-warning") ,"label" => ($this->is_unbanned ? "BAN" : "UNBAN") ,"input" => ($this->is_unbanned ? $ban_input : array()) ,"description" => ($is_unbanned ? "Unban this user" : "Ban this user") ) ) } public function adminBan($container, $admin, $input) { ... if($error_str) { return "Error: error_str"; } return array( "affected_fields" => array("is_unbanned", "date_banned") ,"report" => "This user was banned or unbanned" ); }
静态函数返回的数组可以有两个键:"headers" 和 "create"。 "headers" 定义出现在用户表上的列。这些列将在实体的所有实例中相同,但仍然可以是动态的。 "create" 指定实体创建的自定义输入和回调,这是可选的。每个实体当然都有默认的创建模态框,其中包含所有字段。
如果您为创建定义了回调函数,只需返回一个新对象而不进行持久化,管理员将运行持久化和刷新。如果发生错误,返回一个错误消息字符串。
非静态函数需要返回一个键值数组,表示用户可以执行的各种实体操作。没有必要动态处理用户权限 - 每个操作中都有一个字段。 "heading" 字段确定操作出现在哪个列中。如果没有指定或设置为静态函数未定义的标题,则操作将简单地不会显示。您只需要一个标题,但如果您有很多操作,可能希望定义更多。
每个回调函数都接收服务容器、当前登录的管理员用户和一个输入数组(有关从该数组中期望的内容,请参阅下面的 'Action Input Specification')。返回值将是一个字符串或键值数组。如果返回字符串,则用户将看到错误并可以使用不同的输入重新执行操作。
如果返回数组,则假定成功。此返回数组需要有一个 "affected_fields" 数组/布尔值,您需要将其填写为已修改的字段,或将它设置为 true 以始终刷新表格。如果用户按这些字段排序或过滤,则表格将重新加载。还有一个 "report" 返回值,它将保留模态框并显示文本值(换行符将显示)。
操作输入规范
全局和实体操作将触发具有自定义输入的模态框。以下是特别复杂的输入规范的一个示例,其中包括所有类型输入的示例
$user_input = array( "test_name" => array("type" => "string", "label" => "Name of New Test", "required" => true) ,"test_description" => array("type" => "string", "label" => "Description of New Test", "required" => true, "lines" => 4) // lines will enable textarea with N rows ,"test_quantity" => array("type" => "integer", "label" => "How many times it should run", "default" => 1) ,"test_fraction" => array("type" => "decimal", "label" => "Fraction of acceptable errors", "required" => true, "default" => 0.1) ,"test_critical" => array("type" => "boolean", "label" => "Is this test critical?", "default" => false) ,"test_type" => array("type" => "select", "label" => "Type of test", "default" => "internal", "choices" => array( array("label" => "Internal", "value" => "internal") ,array("label" => "External", "value" => "external") ,array("label" => "Both internal & external", "value" => "hybrid") )) ,"test_rerun" => array("type" => "select", "label" => "Re-run in case of failure?", "default" => "false", "choices" => array( array("label" => "No", "value" => "false") ,array("label" => "Yes", "value" => "true", "input" => array( "test_num_reruns" => array("type" => "integer", "label" => "How many times it should attempt to re-run", "default" => 10) ,"test_justification" => array("type" => "string", "label" => "Why should this test re-run?", "default" => "Because why not.") )) )) ,"test_user" => array("type" => "entity", "label" => "A user responsible for this test", "entity" => 'AppBundle\Entity\User') ,"test_dependencies" => array("type" => "multientity", "label" => "A list of tests that must run first", , "entity" => 'AppBundle\Entity\TestSpec') ,"test_error_descriptions" => array("type" => "array", "label" => "A list of descriptions for error codes", "input" => array( "code" => array("type" => "integer", "label" => "Error code", "required" => true) ,"description" => array("type" => "string", "label" => "Description", "required" => true) )) ,"test_file" => array("type" => "file", "label" => "Upload a file", "required" => true) );
当用户在模态框中点击 "确定" 时,将向回调函数发送一个数组,该数组包含用户为每个输入的值作为键。在此示例中,它将具有 "test_name"(一个字符串)、"test_fraction"(一个布尔值)和 "test_file"(一个 Symfony\Component\HttpFoundation\File\UploadedFile 对象)等键。以下是上述结构的示例
// the callback function will get something like this back: $input_result = array( "test_name" => "Division by zero" ,"test_quantity" => 3 ,"test_fraction" => 0.2 ,"test_type" => "internal" ,"test_rerun" => "true" // if this was "false", the array would not have the next two entries ,"test_num_reruns" => 126 // only appears if "test_rerun" was "true" ,"test_justification" => "Just in case that zero wasn't really zero, but a really small number." // only appears if "test_rerun" was "true" ,"test_user" => "ID_43tgf43mn9j38er23" // Not an object, but an ID ,"test_dependencies" => array("ID_3hj767jfgfdg", "ID_rthg54trhytj4t") // Not an array of objects, but ID's ,"test_error_descriptions" => array( array("code" => 12, "description" => "Something mysterious went wrong") ,array("code" => 46, "description" => "Negative zero encountered") ,array("code" => 1, "description" => "There was an error detected in the error detection subroutines") ) ,"test_file" => [OBJECT] );
触发输入数组的各种方法是一致的,并且它们也是递归的。对于 "test_error_descriptions" 的输入结果将是一个数组,其中每个数组项的格式与从非递归输入规范中获得的格式完全相同。这可以嵌套到任何深度。
然而,这并不适用于位于选择输入类型选项下的输入规范。您可以声明一个选择类型,它创建一个包含值的下拉框。当选择该值时,默认情况下或由用户选择,可选的 "input" 属性可以动态创建更多的输入。这可以嵌套到任何深度。与数组输入类型不同,这些值实际上会在与顶级输入相同的数组级别声明。例如,在上面的示例结果中,“test_num_reruns”与“test_rerun”处于相同的数组嵌套级别。这种奇怪的数组扁平化发生的主要原因是,没有简单、干净的方法来同时返回值和潜在输入数组。看看这个恐怖的事实,并同意。
// either this... ,"test_rerun" => array("value" => "true", "input" => array( "test_num_reruns" => 126 ,"test_justification" => "Just in case that zero wasn't really zero, but a really small number." ) // ...or this ,"test_rerun" => array("value" => "false", "input" => array()
限制
这个包是在假设每个实体都有一个单一的主键的情况下编写的。大多数功能将正常工作——但如果主键由关联组成,这将破坏此管理员。如果有问题,请打开一个问题或贡献一个修复。未来的开发将致力于通过允许您根据关联进行过滤来使过滤更有用。
开源这个管理仪表板的动力是回馈开源社区,同时也是为了让其他人参与其开发。请随时帮助解决错误、限制或功能!
许可证
递归管理是在GNU GPLv3许可证下开源的。