This is a follow-up on “Sencha Ext JS meets Symfony“. To follow the step-by-step walkthrough you need to complete the walkthrough shown there.

Sencha Ext.direct is a platform and language agnostic remote procedure call (RPC) protocol. Ext.direct allows for communication between the client side of an Ext JS application and any server platform that conforms to the specification. Because Ext.direct is handy tool for communicating between an Ext JS application and a server-side backend, and because it can easily be used from classic  (formerly known as Ext JS) and modern (formerly known as Sencha Touch) applications, we found it a perfect fit for our applications. To make this usable from a Symfony based backend we created teqneers/ext-direct-bundle and teqneers/ext-direct to provide a specification compliant PHP Ext.direct backend.

Starting with our demo application that we build in the previous article, we’re going to add the teqneers/ext-direct-bundle to our application.

composer.phar require teqneers/ext-direct-bundle:dev-master teqneers/ext-direct:dev-master

Please note that even though teqneers/ext-direct is a dependency of teqneers/ext-direct-bundle you’re required to list them both explicitly because both packages are not yet available in a stable version. Following common procedures to add bundles to Symfony applications, we’re going to modify AppKernel::registerBundles() in app/AppKernel.php:

// app/AppKernel.php
// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMS\SerializerBundle\JMSSerializerBundle(),
    new TQ\Bundle\ExtDirectBundle\TQExtDirectBundle(),
    // ...
);

You’re required to add the JMSSerializerBundle as well, because the teqneers/ext-direct-bundle currently depends on the bundle being available.

Because the teqneers/ext-direct-bundle comes with its own controller and requires some routes to access the controller actions, we need to update our routing to include the routing information provided by the bundle. Just edit app/config/routing.yml:

// app/config/routing.yml
// ...
ext_direct:
    resource: "@TQExtDirectBundle/Resources/config/routing.yml"
    prefix: /api

Finally we need to add the configuration section required by teqneers/ext-direct-bundle to app/config/config.yml.

// app/config/config.yml
// ...
tq_ext_direct:
    endpoints:
        api: ~

This is the most simple configuration possible. It just exposes one Ext.direct endpoint called api using all the default settings. Now that we have an endpoint configuration we need to add the API service description to the Ext JS page template.

//...
    <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>
// ...

But what’s a service endpoint without services? Let’s start adding a simple calculation service (I know, that’s not the most genius example ever). Using the default configuration the Ext.direct component looks up service endpoints in the ExtDirect namespace of each registered bundle. Our application is a bundle as well, so we can just add src/AppBundle/ExtDirect/CalculationService.php:

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

Let’s talk about the details later. At the moment the only things that matters is, that we added a service class to out api endpoint that exposes four methods: add, subtract, multiply and divide. To use the endpoint in our client-side application, we’re going to modify my-app/app/Application.js and change the launch() method:

// ...
launch: function () {
    if (Ext.app && Ext.app.REMOTING_API) {
        Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);
    }
},
// ...

The Ext.app.REMOTING_API is an identifier defined in the default configuration of our endpoint. To verify that everything worked correctly, reload your application in the browser, open the debugging console and type

AppBundle.ExtDirect.CalculationService.add(1, 2, function(r) { console.log(r); });

This remotely executes AppBundle\ExtDirect\CalculationService::add(1, 2) and returns the result in the given callback function. You should read 3 somewhere in the debugging console. Now let’s quickly add a stupid but simple user interface to work with our calculation service.

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

This adds a new tab to our application exposing a very sophisticated and advanced interface to our calculation service. Change the numbers and watch the results update on the fly. Using the debugging console you can trace the AJAX calls that are being made to the API endpoint. Please note that as we changed our application code, we need to run

sencha app watch 

again before reloading the page.

As the focus of this article is not on describing how Ext.direct works internally and how it can be used within an Ext JS application, but rather on how the teqneers/ext-direct-bundle integrates Ext.direct into Symfony, we’re going to close this article by looking at our calculation service once more. Exposing classes though an Ext.direct API requires some additional meta data to be associated with the class. Currently this is possible only by adding annotations to the service code. All classes exposed via the service must use the @TQ\ExtDirect\Annotation\Action() annotation on the class and the @TQ\ExtDirect\Annotation\Method() annotation on each method. To add argument validation one can add @TQ\ExtDirect\Annotation\Parameter annotations using collections of Symfony\Component\Validator\Constraints to validate the data going into a service method. Our example uses static methods only which does not require to instantiate a service object from the service class. As this is a rare use case, the bundle provides the ability to either instantiate service objects internally if they feature a parameter-less constructor or retreive a service object from the Symfony dependency injection container using an extended class annotation – something like: @TQ\ExtDirect\Annotation\Action("app.my.service.id"). This allows up-front configuration of service dependencies leveraging all the features the Symfony DI container provides.

Please feel free to contribute either by discussing with us, by testing and reporting bugs, by requesting features or even by actively working on the package itself. The package is available on Github: github.com/teqneers/ext-direct-bundle, the respective core library can be found on Github as well: github.com/teqneers/ext-direct.

Versions used: 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[/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]