Sencha Ext.direct trifft Symfony

Dies ist eine Fortsetzung von „Sencha Ext JS meets Symfony“. Um dem Schritt-für-Schritt-Walkthrough zu folgen, müssen Sie den dort gezeigten Walkthrough abgeschlossen haben.
Sencha Ext.direct ist ein plattform- und sprachunabhängiges Remote-Procedure-Call-(RPC-)Protokoll. Ext.direct ermöglicht die Kommunikation zwischen der Client-Seite einer Ext-JS-Anwendung und jeder Server-Plattform, die der Spezifikation entspricht. Da Ext.direct ein praktisches Werkzeug für die Kommunikation zwischen einer Ext-JS-Anwendung und einem serverseitigen Backend ist und sich problemlos sowohl aus classic- (früher Ext JS) als auch aus modern-Anwendungen (früher Sencha Touch) nutzen lässt, fanden wir es perfekt geeignet für unsere Anwendungen. Um dies aus einem Symfony-basierten Backend nutzbar zu machen, haben wir teqneers/ext-direct-bundle und teqneers/ext-direct erstellt, die ein spezifikationskonformes PHP-Backend für Ext.direct bereitstellen.
Ausgehend von unserer Demo-Anwendung, die wir im vorherigen Artikel gebaut haben, fügen wir unserer Anwendung nun das teqneers/ext-direct-bundle hinzu.
composer.phar require teqneers/ext-direct-bundle:dev-master teqneers/ext-direct:dev-master
Bitte beachten Sie: Auch wenn teqneers/ext-direct eine Abhängigkeit von teqneers/ext-direct-bundle ist, müssen Sie beide explizit angeben, weil beide Pakete noch nicht in einer stabilen Version verfügbar sind. Dem üblichen Vorgehen zum Hinzufügen von Bundles zu Symfony-Anwendungen folgend, ändern wir AppKernel::registerBundles() in app/AppKernel.php:
// app/AppKernel.php
// in AppKernel::registerBundles()
$bundles = array(
// ...
new JMS\SerializerBundle\JMSSerializerBundle(),
new TQ\Bundle\ExtDirectBundle\TQExtDirectBundle(),
// ...
);
Sie müssen zusätzlich das JMSSerializerBundle hinzufügen, weil das teqneers/ext-direct-bundle derzeit darauf angewiesen ist, dass dieses Bundle verfügbar ist.
Da das teqneers/ext-direct-bundle einen eigenen Controller mitbringt und einige Routen benötigt, um auf die Controller-Actions zuzugreifen, müssen wir unser Routing aktualisieren, um die vom Bundle bereitgestellten Routing-Informationen einzubinden. Bearbeiten Sie einfach app/config/routing.yml:
// app/config/routing.yml
// ...
ext_direct:
resource: "@TQExtDirectBundle/Resources/config/routing.yml"
prefix: /api
Schließlich müssen wir den vom teqneers/ext-direct-bundle benötigten Konfigurationsabschnitt in app/config/config.yml hinzufügen.
// app/config/config.yml
// ...
tq_ext_direct:
endpoints:
api: ~
Dies ist die einfachste mögliche Konfiguration. Sie stellt lediglich einen Ext.direct-Endpoint namens api mit allen Standardeinstellungen bereit. Nachdem wir nun eine Endpoint-Konfiguration haben, müssen wir die API-Service-Beschreibung zum Ext-JS-Seitentemplate hinzufügen.
//...
<script type="text/javascript">
var Ext = Ext || {};
Ext.manifest = '{{ extjsManifestPath()|e('js') }}';
</script>
<script id="microloader" data-app="{{ extjsApplicationId() }}" type="text/javascript" src="{{ extjsBootstrapPath() }}"></script>
<script id="ext-direct-api" type="text/javascript" src="{{ extDirectApiPath('api') }}"></script>
</head>
// ...
Aber was ist ein Service-Endpoint ohne Services? Fangen wir damit an, einen einfachen Berechnungsservice hinzuzufügen (ich weiß, das ist nicht das genialste Beispiel aller Zeiten). Mit der Standardkonfiguration sucht die Ext.direct-Komponente Service-Endpoints im Namespace ExtDirect jedes registrierten Bundles. Unsere Anwendung ist ebenfalls ein Bundle, also können wir einfach src/AppBundle/ExtDirect/CalculationService.php hinzufügen:
<?php
namespace AppBundle\ExtDirect;
use TQ\ExtDirect\Annotation as Direct;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class CalculationService
*
* @package AppBundle\ExtDirect
*
* @Direct\Action()
*/
class CalculationService
{
/**
* @Direct\Method()
* @Direct\Parameter("a", { @Assert\Type("numeric"), @Assert\NotNull() })
* @Direct\Parameter("b", { @Assert\Type("numeric"), @Assert\NotNull() })
*
* @param float $a
* @param float $b
* @return float
*/
public static function add($a, $b)
{
return $a + $b;
}
/**
* @Direct\Method()
* @Direct\Parameter("a", { @Assert\Type("numeric"), @Assert\NotNull() })
* @Direct\Parameter("b", { @Assert\Type("numeric"), @Assert\NotNull() })
*
* @param float $a
* @param float $b
* @return float
*/
public static function subtract($a, $b)
{
return $a - $b;
}
/**
* @Direct\Method()
* @Direct\Parameter("a", { @Assert\Type("numeric"), @Assert\NotNull() })
* @Direct\Parameter("b", { @Assert\Type("numeric"), @Assert\NotNull() })
*
* @param float $a
* @param float $b
* @return float
*/
public static function multiply($a, $b)
{
return $a * $b;
}
/**
* @Direct\Method()
* @Direct\Parameter("a", { @Assert\Type("numeric"), @Assert\NotNull() })
* @Direct\Parameter("b", { @Assert\Type("numeric"), @Assert\NotNull() })
*
* @param float $a
* @param float $b
* @return float
*/
public static function divide($a, $b)
{
return (float)$a / (float)$b;
}
}
Über die Details sprechen wir später. Im Moment zählt nur, dass wir unserem API-Endpoint eine Service-Klasse hinzugefügt haben, die vier Methoden bereitstellt: add, subtract, multiply und divide. Um den Endpoint in unserer clientseitigen Anwendung zu nutzen, ändern wir my-app/app/Application.js und passen die launch()-Methode an:
// ...
launch: function () {
if (Ext.app && Ext.app.REMOTING_API) {
Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);
}
},
// ...
Ext.app.REMOTING_API ist ein Bezeichner, der in der Standardkonfiguration unseres Endpoints definiert ist. Um zu prüfen, ob alles korrekt funktioniert hat, laden Sie Ihre Anwendung im Browser neu, öffnen die Debugging-Konsole und geben Folgendes ein:
AppBundle.ExtDirect.CalculationService.add(1, 2, function(r) { console.log(r); });
Dies führt AppBundle\ExtDirect\CalculationService::add(1, 2) entfernt aus und liefert das Ergebnis an die angegebene Callback-Funktion. Sie sollten irgendwo in der Debugging-Konsole 3 lesen. Fügen wir nun schnell eine simple, aber schlichte Benutzeroberfläche hinzu, um mit unserem Berechnungsservice zu arbeiten.
// my-app/app/view/main/MainModel.js
/**
* This class is the view model for the Main view of the application.
*/
Ext.define('MyApp.view.main.MainModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.main',
data: {
name: 'MyApp',
loremIpsum: '...',
a: 1,
b: 2,
sum: null,
difference: null,
product: null,
quotient: null
}
//TODO - add data, formulas and/or methods to support your view
});
// my-app/app/view/main/Main.js
/**
* This class is the main view for the application. It is specified in app.js as the
* "mainView" property. That setting automatically applies the "viewport"
* plugin causing this view to become the body element (i.e., the viewport).
*
* TODO - Replace this content of this view to suite the needs of your application.
*/
Ext.define('MyApp.view.main.Main', {
extend: 'Ext.tab.Panel',
xtype: 'app-main',
requires: [
'Ext.plugin.Viewport',
'Ext.window.MessageBox',
'MyApp.view.main.MainController',
'MyApp.view.main.MainModel',
'MyApp.view.main.List'
],
controller: 'main',
viewModel: 'main',
ui: 'navigation',
tabBarHeaderPosition: 1,
titleRotation: 0,
tabRotation: 0,
header: {
layout: {
align: 'stretchmax'
},
title: {
bind: {
text: '{name}'
},
flex: 0
},
iconCls: 'fa-th-list'
},
tabBar: {
flex: 1,
layout: {
align: 'stretch',
overflowHandler: 'none'
}
},
responsiveConfig: {
tall: {
headerPosition: 'top'
},
wide: {
headerPosition: 'left'
}
},
defaults: {
bodyPadding: 20,
tabConfig: {
plugins: 'responsive',
responsiveConfig: {
wide: {
iconAlign: 'left',
textAlign: 'left'
},
tall: {
iconAlign: 'top',
textAlign: 'center',
width: 120
}
}
}
},
items: [{
title: 'Home',
iconCls: 'fa-home',
// The following grid shares a store with the classic version's grid as well!
items: [{
xtype: 'mainlist'
}]
}, {
title: 'Users',
iconCls: 'fa-user',
bind: {
html: '{loremIpsum}'
}
}, {
title: 'Groups',
iconCls: 'fa-users',
bind: {
html: '{loremIpsum}'
}
}, {
title: 'Settings',
iconCls: 'fa-cog',
bind: {
html: '{loremIpsum}'
}
}, {
title: 'Calculator',
iconCls: 'fa-cog',
layout: 'anchor',
items: [{
xtype: 'numberfield',
reference: 'aField',
bind: '{a}',
fieldLabel: 'A',
listeners: {
change: 'onCalculatorFieldChange',
buffer: 250
}
}, {
xtype: 'numberfield',
reference: 'bField',
bind: '{b}',
fieldLabel: 'B',
listeners: {
change: 'onCalculatorFieldChange',
buffer: 250
}
}, {
xtype: 'displayfield',
bind: '{sum}',
fieldLabel: 'sum'
}, {
xtype: 'displayfield',
bind: '{difference}',
fieldLabel: 'difference'
}, {
xtype: 'displayfield',
bind: '{product}',
fieldLabel: 'product'
}, {
xtype: 'displayfield',
bind: '{quotient}',
fieldLabel: 'quotient'
}]
}]
});
// my-app/app/view/main/MainController.js
/**
* This class is the controller for the main view for the application. It is specified as
* the "controller" of the Main view class.
*
* TODO - Replace this content of this view to suite the needs of your application.
*/
Ext.define('MyApp.view.main.MainController', {
extend: 'Ext.app.ViewController',
alias: 'controller.main',
onItemSelected: function (sender, record) {
Ext.Msg.confirm('Confirm', 'Are you sure?', 'onConfirm', this);
},
onConfirm: function (choice) {
if (choice === 'yes') {
//
}
},
onCalculatorFieldChange: function () {
var viewModel = this.getViewModel(),
a = viewModel.get('a'),
b = viewModel.get('b');
AppBundle.ExtDirect.CalculationService.add(a, b, function(r) {
viewModel.set('sum', r);
}, this);
AppBundle.ExtDirect.CalculationService.subtract(a, b, function(r) {
viewModel.set('difference', r);
}, this);
AppBundle.ExtDirect.CalculationService.multiply(a, b, function(r) {
viewModel.set('product', r);
}, this);
AppBundle.ExtDirect.CalculationService.divide(a, b, function(r) {
viewModel.set('quotient', r);
}, this);
}
});
Dies fügt unserer Anwendung einen neuen Tab hinzu, der eine überaus ausgefeilte und fortgeschrittene Oberfläche für unseren Berechnungsservice bereitstellt. Ändern Sie die Zahlen und beobachten Sie, wie sich die Ergebnisse sofort aktualisieren. Über die Debugging-Konsole können Sie die AJAX-Aufrufe nachverfolgen, die an den API-Endpoint gemacht werden. Bitte beachten Sie: Da wir unseren Anwendungscode geändert haben, müssen wir
sencha app watch
erneut ausführen, bevor wir die Seite neu laden.
Da der Fokus dieses Artikels nicht darauf liegt, zu beschreiben, wie Ext.direct intern funktioniert und wie es innerhalb einer Ext-JS-Anwendung verwendet werden kann, sondern darauf, wie das teqneers/ext-direct-bundle Ext.direct in Symfony integriert, schließen wir diesen Artikel mit einem weiteren Blick auf unseren Berechnungsservice. Um Klassen über eine Ext.direct-API bereitzustellen, müssen der Klasse einige zusätzliche Metadaten zugeordnet werden. Derzeit ist das nur über Annotations im Service-Code möglich. Alle über den Service bereitgestellten Klassen müssen die Annotation @TQ\ExtDirect\Annotation\Action() an der Klasse und die Annotation @TQ\ExtDirect\Annotation\Method() an jeder Methode verwenden. Für die Argumentvalidierung kann man @TQ\ExtDirect\Annotation\Parameter-Annotations mit Collections von Symfony\Component\Validator\Constraints hinzufügen, um die Daten zu validieren, die in eine Service-Methode hineingehen. Unser Beispiel verwendet ausschließlich statische Methoden, wodurch kein Service-Objekt aus der Service-Klasse instanziiert werden muss. Da dies ein seltener Anwendungsfall ist, bietet das Bundle die Möglichkeit, Service-Objekte entweder intern zu instanziieren, sofern sie einen parameterlosen Konstruktor besitzen, oder ein Service-Objekt über eine erweiterte Klassen-Annotation aus dem Symfony-Dependency-Injection-Container zu beziehen – etwa so: @TQ\ExtDirect\Annotation\Action("app.my.service.id"). Das erlaubt die vorausschauende Konfiguration von Service-Abhängigkeiten unter Ausnutzung aller Features, die der Symfony-DI-Container bietet.
Beteiligen Sie sich gerne – sei es durch Diskussionen mit uns, durch Testen und das Melden von Bugs, durch Feature-Wünsche oder sogar durch aktive Mitarbeit am Paket selbst. Das Paket ist auf Github verfügbar: github.com/teqneers/ext-direct-bundle, die zugehörige Core-Bibliothek finden Sie ebenfalls auf Github: github.com/teqneers/ext-direct.
*Verwendete Versionen: Symfony 2.7.4, Sencha Cmd 6.0.1.72, Sencha Ext JS 6.0.0.640, teqneers/ext-direct 80f9bc, teqneers/ext-direct-bundle 09bd17