Ext\DirectBundle — реализация ExtDirect для symfony2

в 23:38, , рубрики: extjs 4, symfony, symfony2, метки: ,

Работая над проектом использующем extjs, для связи extjs <= extdirect => symfony использовал проект NetonDirectBundle.

Этот небольшой bundle предоставляет базовые возможности связи с symfony, но в некоторых местах неоптимален и слишком прост. Всё глубже занимаясь его улучшением, накопилось достаточно много изменений.

К сожалению автор проект забросил, так как последняя активность была год назад, я решил подхватить упавшее знамя и слить свои изменения в форк этого проекта и соответственно поделиться с сообществом.

Изначально я планировал, для начала, написать краткую документацию на русском языке и пост. Увлекшись в написании документации я уже не знаю, чем можно дополнить статью.

Получилось наоборот, достопочтенную публику я не буду утруждать описанием процесса установки bundle, а перейду непосредственно к плюшкам.

Возможно будет многовато кода, но для наглядности.

Пример использования

Простой вариант

Этот метод позволяет сохранить совместимость с оригинальным кодом.
Для базового примера использования, рассмотрим задачу извлечения данных, допустим, чтобы заполнить хранилище (Ext.data.Store).

Контроллер (Symfony2)

<?php
namespace AcmeDemoBundleController;

use SymfonyBundleFrameworkBundleControllerController;

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

Можно обойтись несколько иначе и передать в DirectBundle результат из getQuery() (AbstractQuery)

Контроллер (Symfony2)

<?php
namespace AcmeDemoBundleController;

use SymfonyBundleFrameworkBundleControllerController;
use ExtDirectBundleResponseAbstractQuery;
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.

Контроллер (Symfony2)

<?php
namespace AcmeDemoBundleController;

use AcmeDemoBundleDirectEventListenerCompactCustomerRolesSubscriber;
use SymfonyBundleFrameworkBundleControllerController;
use ExtDirectBundleResponseKnpPaginator;
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. Это значит, что если параметр определен и его возможно передать, он будет передан.

Пример запроса из 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 — оригинал объекта SymfonyComponentHttpFoundationRequest, для данного запроса
  • $_data — весь оригинальный массив переданных параметров;
  • $_list — тот же самый $_data только для пакетной обработки, к примеру изменение нескольких строк в grid, $_list будет содержать массив из нескольких $_data.
События

Есть возможность добавить обработку событий. На данный момент обработчик ExtDirectBundleResponseAbstractQuery, и основанный на нем ExtDirectBundleResponseKnpPaginator, поддерживает одно событие — POST_QUERY_EXECUTE.

Ниже приведенный пример изменяет, уже извлеченные данные, перед передачей их в сеть.

Пример события

<?php
namespace AcmeDemoBundleDirectEventListener;

use SymfonyComponentEventDispatcherEventSubscriberInterface;
use ExtDirectBundleEventDirectEvents;
use ExtDirectBundleEventResponseEvent;

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);
  }
}
Обработка form submit и возврат ошибок из формы

Рассмотрим задачу обработки submit из Ext.form.Panel.

Конфигурация формы

api: {
        submit: Actions.AcmeDemo_Demo.createCustomer
}
paramsAsHash: true

Контроллер (Symfony2)

<?php
namespace AcmeDemoBundleController;

use SymfonyBundleFrameworkBundleControllerController;
use ExtDirectBundleResponseFormError;
use AcmeDemoBundleEntityCustomer;
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->getChildren());
      $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(), для обработки формы. В примере форма определена как служба. Это необходимо для работы трансформеров.

Если валидация формы прошла успешно, производится ответ передающий 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>"}}
]
Синхронизация хранилища и возврат ошибок из сервиса Validator

Существует задача синхронизации хранилища это бывает связано с изменением сразу нескольких строк. Подобную задачу тоже можно решить, используя DirectBundle.

<?php
namespace AcmeDemoBundleController;

use SymfonyBundleFrameworkBundleControllerController;
use ExtDirectBundleResponseResponse;
use ExtDirectBundleResponseValidatorError;
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->getChildren()));

            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);
}

В данном примере специально ошибки извлекаются из сервиса validator, формат ответа будет аналогичен ответу из предыдущего раздела.

Итого

Мне очень хочется, что бы кому то этот bundle был полезен.
Если кто то захочет внести свои улучшения, всегда пожалуйста! Fork & Pull Request! ;)

Автор: GHua

* - обязательные к заполнению поля