anilton.junior / ext-direct-bundle
Symfony2的ExtDirect实现
Requires
- php: >=5.5
- doctrine/doctrine-bundle: ~1.2
- doctrine/orm: ~2.2
- jms/serializer-bundle: ^2.3
- knplabs/knp-paginator-bundle: >=2.3.1
- symfony/assetic-bundle: ~2.3
- symfony/monolog-bundle: ~2.4
- symfony/symfony: >=2.3
Requires (Dev)
- mockery/mockery: dev-master
- phpunit/phpunit: ~4.7
This package is not auto-updated.
Last update: 2024-09-21 20:18:06 UTC
README
DirectBundle是symfony2的ExtDirect规范的实现。
安装
使用composer
{
require: {
"ghua/ext-direct-bundle": "v2.4.0"
}
}
在AppKernel中注册DirectBundle
<?php
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Ext\DirectBundle\ExtDirectBundle(),
// ...
);
// ...
return $bundles;
}
修改app/config/routing.yml
ext_direct:
resource: "@ExtDirectBundle/Resources/config/routing.yml"
修改app/config/config.yml
ext_direct:
resource: "%kernel.root_dir%/config/extdirect_routing.yml"
配置示例
- error_template - 验证错误数组的模板
- resource - 路由配置文件,例如:
resource: "%kernel.root_dir%/config/extdirect_routing.yml"
extdirect_routing.yml
getCustomers:
defaults: { _controller: AcmeDemoBundle:Demo:getCustomers, params: true }
reader: { root: root }
getCountries:
defaults: { _controller: AcmeDemoBundle:Demo:getCountries }
getRoles:
defaults: { _controller: AcmeDemoBundle:Demo:getRoles }
updateCustomer:
defaults: { _controller: AcmeDemoBundle:Demo:updateCustomer, params: true }
createCustomer:
defaults: { _controller: AcmeDemoBundle:Demo:createCustomer, params: true, form: true }
chat:
defaults: { _controller: chat_service:chat, params: true, form: true }
此外,您还可以使用控制器注解
testClassLoader:
resource: "@AcmeTestBundle/Controller/TestController.php"
testDirectoryLoader:
resource: "@AcmeTestBundle/Controller"
AcmeController.php
namespace Acme\TestBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ext\DirectBundle\Annotation\Route;
use Ext\DirectBundle\Annotation\Reader;
use Ext\DirectBundle\Annotation\Writer;
class TestController extends Controller
{
/**
* @Route(name="acmeTest", isWithParams = true)
*/
public function testAction($_data)
{
// code
}
注解参数
- Route - 名称,isWithParams,isFormHandler
- Reader - root,successProperty,totalProperty,type
- Writer - root,type
添加到模板中
<script type="text/javascript" src="{{ url('ExtDirectBundle_api')}}"></script>
在您的ExtJS应用程序中添加extdirect提供者
Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);
使用示例
简单版本
为了考虑基本使用示例,考虑数据提取的问题,例如填充仓库(Ext.data.Store)。
控制器(Symfony2)
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DemoController extends Controller
{
public function getRolesAction()
{
$data = $this->getDoctrine()
->getRepository('AcmeDemoBundle:Role')
->createQueryBuilder('role')
->getQuery()
->getArrayResult();
return $data;
}
}
模型和仓库(ExtJS)
Ext.define('ACME.model.Role', {
extend: 'Ext.data.Model',
fields: ['id', 'code', 'name', 'customer_id'],
proxy: {
type: 'direct',
api: {
read: Actions.AcmeDemo_Demo.getRoles
}
}
});
Ext.define('ACME.store.Role', {
extend: 'Ext.data.Store',
model: 'ACME.model.Role',
autoLoad: true
});
扩展版本
AbstractQuery
您可以稍作不同,将getQuery()(AbstractQuery)的结果传递给DirectBundle
控制器(Symfony2)
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ext\DirectBundle\Response\AbstractQuery;
class DemoController extends Controller
{
public function getCountriesAction()
{
$query = $this->getDoctrine()
->getRepository('AcmeDemoBundle:Country')
->createQueryBuilder('country')
->getQuery();
return $this->get('ext_direct')
->createResponse(new AbstractQuery(), $query);
}
}
KnpPaginator和接收参数
所有数据都无差别地提取和传递,很少。分页、过滤、排序是常见的任务。
当然,分页可以独立实现,DirectBundle不会造成麻烦。但在我的项目中使用了[KnpPaginator]来执行此任务(https://github.com/KnpLabs/KnpPaginatorBundle)。
控制器(Symfony2)
<?php
namespace Acme\DemoBundle\Controller;
use Acme\DemoBundle\Direct\EventListener\CompactCustomerRolesSubscriber;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ext\DirectBundle\Response\KnpPaginator;
class DemoController extends Controller
{
public function getCustomersAction($page = 1, $limit = 10, $filter = array(), $sort = array())
{
$query = $this->getDoctrine()
->getEntityManager()
->getRepository('AcmeDemoBundle:Customer')
->findCustomers($filter, $sort);
$paginator = $this->get('knp_paginator')->paginate($query, $page, $limit);
return $this->get('ext_direct')
->createResponse(new KnpPaginator(), $paginator)
->addEventSubscriber(new CompactCustomerRolesSubscriber());
}
}
让我们仔细考虑这个方法参数。它们不是必需的,因为方法调用是通过ReflectionMethod::getParameters预先执行的。这意味着如果参数已定义并且可以发送,则将被发送。
附加说明! 从findCustomers返回的AbstractQuery应具有HydrationMode等于HYDRATE_ARRAY。这是通过调用setHydrationMode()方法完成的。
CustomerRepository
<?php
namespace Acme\DemoBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
class CustomerRepository extends EntityRepository
{
public function findCustomers($filters = array(), $sorts = array())
{
$query = $this->createQueryBuilder('customer')
// ...
->getQuery();
return $query->setHydrationMode(Query::HYDRATE_ARRAY);
}
}
ExtJS请求示例(JSON)
{
"action":"AcmeDemo_Demo",
"method":"getCustomers",
"data":[{"page":1, "start":0, "limit":28,
"sort":[
{"property":"id","direction":"ASC"}
],
"filter":[
{"property":"roles","value":[4]},
{"property":"country","value":225}
]
}],
"type":"rpc",
"tid":1
}
因此,可以发送data数组中的任何键作为方法参数。
附加参数
有几种可能的参数
- Request $request – 当前Symfony\Component\HttpFoundation\Request对象的原件;
- $_data – 所发送参数的所有原始数组;
- $_list – 与$_data相同,但用于批量处理,例如更改网格中的多行,$_list将包含来自多个$_data的数组。
事件
可以添加事件处理。目前,handler Ext\DirectBundle\Response\AbstractQuery 支持 PRE_QUERY_EXECUTE 和 POST_QUERY_EXECUTE,而基于 Ext\DirectBundle\Response\KnpPaginator 仅支持后者。有关事件的其他信息,请参阅Ext\DirectBundle\Response\AbstractQuery::execute()的源代码。
下面的示例在将数据传递到网络之前更改已提取的数据。
事件示例
<?php
namespace Acme\DemoBundle\Direct\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Ext\DirectBundle\Event\DirectEvents;
use Ext\DirectBundle\Event\ResponseEvent;
class CompactCustomerRolesSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(DirectEvents::POST_QUERY_EXECUTE => 'callbackFunction');
}
public function callbackFunction(ResponseEvent $event)
{
$data = $event->getData();
foreach($data as $n => $customer)
{
if(isset($data[$n]['role_ids']))
$data[$n]['role_ids'] = array();
foreach($customer['roles'] as $role)
{
$data[$n]['role_ids'][] = $role['id'];
}
}
$event->setData($data);
}
}
处理表单提交和返回表单的错误
让我们考虑处理Ext.form.Panel的提交的任务。在extjs的代码示例中,定义了一个显示表单的窗口以及表单本身及其元素。
表单(ExtJS)
Ext.define('ACME.view.customer.New', {
extend: 'Ext.window.Window',
alias : 'widget.customernewwindow',
autoShow: true,
title : 'New Customer',
layout: 'fit',
items: [{
xtype: 'customerform',
api: {
submit: Actions.AcmeDemo_Demo.createCustomer
},
paramsAsHash: true
}],
buttons: [{
text: 'Save',
action: 'submit'
}]
});
Ext.define('ACME.view.customer.Form', {
extend: 'Ext.form.Panel',
alias : 'widget.customerform',
layout: 'vbox',
frame: true,
items: [{
xtype: 'textfield',
name: 'name',
fieldLabel: 'Name',
},{
xtype: 'combobox',
name: 'country_id',
fieldLabel: 'Country',
valueField: 'id',
displayField: 'name',
store: 'Country',
forceSelection: true
},{
xtype: 'combobox',
name: 'role_ids',
fieldLabel: 'Roles',
valueField: 'id',
displayField: 'name',
store: 'Role',
multiSelect: true
}]
});
提交请求示例(POST)
country_id 5
extAction AcmeDemo_Demo
extMethod createCustomer
extTID 11
extType rpc
extUpload false
id
name Admin
role_ids[] 3
role_ids[] 1
控制器(Symfony2)
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ext\DirectBundle\Response\FormError;
use Acme\DemoBundle\Entity\Customer;
class DemoController extends Controller
{
public function createCustomerAction($_data)
{
$Customer = new Customer();
$form = $this->createForm($this->get('acme_demo.updatecustomer'), $Customer);
$_data = array_intersect_key($_data, $form->all());
$form->bind($_data);
if($form->isValid())
{
$em = $this->getDoctrine()
->getEntityManager();
$em->persist($Customer);
$em->flush();
} else {
return $this->get('ext_direct')
->createResponse(new FormError(), $form);
}
return $this->get('ext_direct')
->createResponse(new Response())
->setSuccess(true);
}
}
除了支持的参数之外,将发送的参数发送到 $_data。这个数组可以直接传递给 $form->bind() 来处理表单。示例中将表单定义为一个服务。这是为了操作 [Transformers] (https://symfony.com.cn/doc/current/cookbook/form/data_transformers.html)所必需的。
如果表单验证成功,则发送响应:success: true。
[
{"type":"rpc",
"tid":"11",
"action":"AcmeDemo_Demo",
"method":"createCustomer",
"result":{"success":true}}
]
在出现错误的情况下,可以发送包含 success: false 和 msg 错误文本的响应。
[
{"type":"rpc",
"tid":"18",
"action":"AcmeDemo_Demo",
"method":"createCustomer",
"result":{"success":false,
"msg":"<ul>\n<li>This value should not be blank<\/li>\n<li>This value is not valid<\/li>\n<\/ul>"}}
]
存储同步和验证服务错误返回
有一个存储同步的任务,它与一次更改多条记录有关。类似的任务也可以使用 DirectBundle 解决。
控制器(Symfony2)
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Ext\DirectBundle\Response\Response;
use Ext\DirectBundle\Response\ValidatorError;
class DemoController extends Controller
{
public function updateCustomerAction(Request $request, $_list)
{
$repository = $this->getDoctrine()
->getRepository('AcmeDemoBundle:Customer');
if($request->getMethod() === "POST")
{
foreach($_list as $customer)
{
if(!isset($customer['id']))
throw new \InvalidArgumentException();
$Customer = $repository->findOneById($customer['id']);
$form = $this->createForm($this->get('acme_demo.updatecustomer'), $Customer);
$form->bind(array_intersect_key($customer, $form->all()));
if($form->isValid())
{
$this->getDoctrine()
->getEntityManager()
->flush();
} else {
return $this->get('ext_direct')
->createResponse(new ValidatorError(), $this->get('validator')->validate($Customer));
}
}
return $this->get('ext_direct')
->createResponse(new Response())
->setSuccess(true);
}
return new Response(502);
}
在这个例子中,错误特别从验证服务中检索,响应格式将与上一节的响应类似。
异常
为了辅助开发,路由器可以捕获 symfony2 控制器的异常。示例
控制器(Symfony2)
public function testExceptionAction()
{
throw new \Exception('Exception from testExceptionAction');
}
ExtJS 应用程序
Ext.Direct.on('exception', function(e) {
Ext.Msg.show({
title: 'Exception!',
msg: e.message + ' ' + e.where,
buttons: Ext.Msg.OK,
icon: Ext.MessageBox.ERROR
});
});
调用 testException 方法的结果是抛出异常
[
{
"message":"exception 'Exception' with message 'Exception from testExceptionAction'",
"where":"in \/home\/gh\/dev\/symfony2sandbox\/vendor\/bundles\/Ext\/DirectBundle\/Controller\/ForTestingController.php: 81",
"type":"exception",
"tid":3,
"action":
"ExtDirect_ForTesting",
"method":"testException"
}
]
ExtJS 可以显示错误消息或执行其他操作。
警告!此模式只能在开发中使用。在生产模式下,异常由 symfony 处理。默认响应是 HTTP 状态码 500,消息为:内部服务器错误。
开发
测试
composer.phar install
phpunit