anilton.junior/ext-direct-bundle

Symfony2的ExtDirect实现

安装: 1

依赖者: 0

建议者: 0

安全性: 0

星标: 0

关注者: 0

分支: 14

类型:symfony-bundle

dev-master 2024-05-17 20:32 UTC

This package is not auto-updated.

Last update: 2024-09-21 20:18:06 UTC


README

DirectBundle是symfony2的ExtDirect规范的实现。

Build Status

安装

使用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