Kontaktieren Sie uns: +49 (711) 46 97 28 - 80|info@teqneers.de

Sencha Ext.direct meets Symfony

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

By | 2017-05-19T09:30:05+00:00 Oktober 1st, 2015|Development|12 Comments

12 Comments

  1. Bratok 8. Dezember 2015 at 10:44 - Reply

    Thank you, very much. It is very helpful bundle.

  2. Marius Kuhn 22. Dezember 2015 at 11:27 - Reply

    I’m unable to get this to work in production, while development works fine.
    In production, it’s trying to fetch remotingprovider.js, which yields 404.
    As a result Ext.direct.Manager.addProvider(Ext.app.REMOTING_API); returns ‚TypeError: c is not a function‘ and Ext.direct.RemotingProvider stays undefined.

    Any ideas on what I’m doing wrong?

    • Stefan Gehrig 22. Dezember 2015 at 11:34 - Reply

      Seems like you’re missing a require to Ext.direct.Manager and Ext.direct.RemotingProvider somewhere in your code (most likely in your Application.js). That would explain why it’s working in development mode and not in production mode. You should also have some debug output on the browser console in development mode that says something about a missing require.

      Did that help?

      • Marius Kuhn 22. Dezember 2015 at 12:20 - Reply

        Wow that was fast. Thank you very much Stefan, you were absolutely correct. Working like a charm now. 🙂

  3. Ronald A. 10. Januar 2016 at 14:31 - Reply

    Thank you for creating and maintaining this sencha symfony bridge.

    Recommendation: Divide by zero would be a good candidate for an example to show serverside exception handling with clientside error message.

  4. Dario G. 23. Mai 2016 at 0:01 - Reply

    Great bundle. Can you show an example of login / logout application? Thank you.

    • Stefan Gehrig 23. Mai 2016 at 9:37 - Reply

      There is no general rule on how to implement login/logout. The easiest way is to use standard Symfony procedures with a separate login page and an appropriate logout route. This can be configured using the default security configuration provided by Symfony. See http://symfony.com/doc/current/book/security.html for some details.

      The other option would be to use an Ext JS based login/logout with AJAX. This greatly depends on your application and whether you want to use sessions or a stateless approach. It’s hard to give a specific advise here because as I said: it depends on your needs.

  5. Dario G. 24. Mai 2016 at 21:10 - Reply

    The approach is session.
    The problem arises when ext.direct attempts to communicate with the backend and the session has expired.

    Your bundle in this circumstance should generate a specific exception which is then turned in response to ext.direct.
    Basically I imagine two specific applications. One that only handles the login that with a redirect calls the second.
    The second application if it finds the response received from ext.direct indicates an expired session performs a redirect to the first.
    I would like to use your bundle in a pilot project.
    Forgive my bad English. I’m using google to translate.

    Thank you for your reply.

    • Stefan Gehrig 24. Mai 2016 at 21:18 - Reply

      In single page applications or other applications where a user remains on a single page very long, you have to deal with session expiration. To circumvent these problems you could use a remote call that regularly checks and prolongates the session lifetime. Implementing such a „heartbeat“ functionality is out of scope for this bundle and library however.

  6. Dario G. 24. Mai 2016 at 22:20 - Reply

    The component “ heartbeat “ would solve the problem of taking in life the session . But if hypothetically a user were to duplicate the page , keeping the same session , and on one of these pages performs a logout , on the other when the user makes any request which leads to query the backend your bundle would return an exception to ext.direct ?

    • Stefan Gehrig 25. Mai 2016 at 9:37 - Reply

      Don’t really get what you mean exactly. Perhaps it’s best to move this discussion to stackoverflow.com. Just open an issue there an post the link here as a comment. I’ll try to answer as best as I can.

  7. Dario G. 26. Mai 2016 at 9:06 - Reply

    Thank you for the answer.

Leave A Comment