Friday, July 1, 2011

ExtJS 4 REST proxy and Zend framework Zend_Rest_Route are incompatible.

I noticed a very strange thing when I worked on Ext JS 4 v4.0.2 and Zend Framework. I find that every time when Ext JS grid sends new created Record to the server through its REST proxy, the POST method was teated as PUT method in Zend_Rest_Route. This cause troublesome because my code have to know if the incoming record a new one for insert or exsiting one for update. Otherwise, I have to use some UPSERT SQL statement to UPDATE or INSERT record into Database table. Unfortunately, I use DB2 for i5/OS V5R4, which does not have built-in UPSERT command and it even does nor support MERGE statement.

I do not want to walkaround this by doing SELECT first and judging if the code need to run INSERT or UPDATE statement. So, I traced the Ext JS 4 code and Zend Framework 1.11 code. I found that these two are not compatiable. I am not sure if we can say it is BUG as this wont happen if either side change their code. Or, both side can change the code to avoid the confilicaton if we say bugs are from both sides.

The key for this issue is that Ext JS 4 append record's id at the end of base URL, which points to RESTful service. Below is a psuedo POST URL create by Ext JS 4 REST proxy. I highlight the unexpected friend in red color.

http://hostname/yourRestServiceURI/yourRecordId/?dc=randomNum


We understand that adding record id as part of URI can allow server side script easily find it. But, why this design is implemented in a general SDK like Ext JS? Especially, I do not see good reason for a POST request to have it as record in POST are suppoe not to have been existing on server side. Therefore, the ID of the new record might not have been generated if it will be generated on the server side only.

In Ext JS 4 source code \src\data\proxyRest.js, we can see what happens,
/**
     * Specialized version of buildUrl that incorporates the {@link #appendId} and {@link #format} options into the
     * generated url. Override this to provide further customizations, but remember to call the superclass buildUrl
     * so that additional parameters like the cache buster string are appended
     */
    buildUrl: function(request) {
        var me        = this,
            operation = request.operation,
            records   = operation.records || [],
            record    = records[0],
            format    = me.format,
            url       = me.getUrl(request),
            id        = record ? record.getId() : operation.id;
        
        if (me.appendId && id) {
            if (!url.match(/\/$/)) {
                url += '/';
            }
            
            url += id; //Why add ID here even for POST? Yiyu Jia
        }
        
        if (format) {
            if (!url.match(/\.$/)) {
                url += '.';
            }
            
            url += format;
        }
        
        request.url = url;
        
        return me.callParent(arguments);
    }
}

Now, people will ask why adding id in URL cause the problem in Zend Framework? Well, let's trace Zend Framework code now.

On Zend Framework API document about Zend_Rest_Route, we can see Zend clearly described their design how RESTful URI should looks like,
Zend_Rest_Route Behavior
MethodURIModule_Controller::action
GET/product/ratings/Product_RatingsController::indexAction()
GET/product/ratings/:idProduct_RatingsController::getAction()
POST/product/ratingsProduct_RatingsController::postAction()
PUT/product/ratings/:idProduct_RatingsController::putAction()
DELETE/product/ratings/:idProduct_RatingsController::deleteAction()
POST/product/ratings/:id?_method=PUTProduct_RatingsController::putAction()
POST/product/ratings/:id?_method=DELETEProduct_RatingsController::deleteAction()

As we can see, Zend Framework does not expect any thing attached to method URI when PHP programmer intend to use POST method. In Zend_Rest_Route source code, we can see a case switch statement as below. You will see when the POST method will be "magically" routed to PUT method if pathElementCount is larger than zero.

switch( $values[$this->_actionKey] ){
                    case 'post':
                        if ($pathElementCount > 0) {
                            $values[$this->_actionKey] = 'put';
                        } else {
                            $values[$this->_actionKey] = 'post';
                        }
                        break;
                    case 'put':
                        $values[$this->_actionKey] = 'put';
                        break;
                }


But, where is the $pathElementCount set? In the same source code file, we can see below statement,

//Store path count for method mapping
$pathElementCount = count($path);

So, where is value of $path populated? Still, in the same source code, there is a function call match. Part of it is cited as below.

/**
     * Matches a user submitted request. Assigns and returns an array of variables
     * on a successful match.
     *
     * If a request object is registered, it uses its setModuleName(),
     * setControllerName(), and setActionName() accessors to set those values.
     * Always returns the values as an array.
     *
     * @param Zend_Controller_Request_Http $request Request used to match against this routing ruleset
     * @return array An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = false)
    {
        if (!$request instanceof Zend_Controller_Request_Http) {
            $request = $this->_front->getRequest();
        }
        $this->_request = $request;
        $this->_setRequestKeys();

        $path   = $request->getPathInfo();
        $params = $request->getParams();
        $values = array();
        $path   = trim($path, self::URI_DELIMITER);

Ok, now, it is not the end of code tracing yet. Where is the getPathInfo() function? It is in Zend_Controller_Request_Http. The following code citation will show us that getPathInfo() eeturns everything between the BaseUrl and QueryString and deos something special.

/**
     * Returns everything between the BaseUrl and QueryString.
     * This value is calculated instead of reading PATH_INFO
     * directly from $_SERVER due to cross-platform differences.
     *
     * @return string
     */
    public function getPathInfo()
    {
        if (empty($this->_pathInfo)) {
            $this->setPathInfo();
        }

        return $this->_pathInfo;
    }

/**
     * Set the PATH_INFO string
     *
     * @param string|null $pathInfo
     * @return Zend_Controller_Request_Http
     */
    public function setPathInfo($pathInfo = null)
    {
        if ($pathInfo === null) {
            $baseUrl = $this->getBaseUrl(); // this actually calls setBaseUrl() & setRequestUri()
            $baseUrlRaw = $this->getBaseUrl(false);
            $baseUrlEncoded = urlencode($baseUrlRaw);
        
            if (null === ($requestUri = $this->getRequestUri())) {
                return $this;
            }
        
            // Remove the query string from REQUEST_URI
            if ($pos = strpos($requestUri, '?')) {
                $requestUri = substr($requestUri, 0, $pos);
            }
            
            if (!empty($baseUrl) || !empty($baseUrlRaw)) {
                if (strpos($requestUri, $baseUrl) === 0) {
                    $pathInfo = substr($requestUri, strlen($baseUrl));
                } elseif (strpos($requestUri, $baseUrlRaw) === 0) {
                    $pathInfo = substr($requestUri, strlen($baseUrlRaw));
                } elseif (strpos($requestUri, $baseUrlEncoded) === 0) {
                    $pathInfo = substr($requestUri, strlen($baseUrlEncoded));
                } else {
                    $pathInfo = $requestUri;
                }
            } else {
                $pathInfo = $requestUri;
            }
        
        }

        $this->_pathInfo = (string) $pathInfo;
        return $this;
    }

So, what is the conclusion? The conclusion is that both Ext JS and Zend framework has their own design of RESTful URI pattern. Unfortunately, they are not compatible with each other. They are too nice to do more than developer expected. As libraries vendor, they maybe should do less sometimes in order to make their library as general as possible. Ext JS, as a pure client side JavaScript library, should not guide server side script designer how to implement RESTful Web service. Zend Framework, as a pure server side script library, should not make design for client side application either. In fact, RESTful Web service is a set of guide lines for developer to make scalable application over HTTP methods. But, it does not define how the URI pattern must be.

Thursday, June 23, 2011

Managing Grid CRUD In ExtJS 4

I read a discussion on Sencha forum (here). Obviously, ExtJS 4 make user to be frustrating because of the lack of well organized document and lack of well commented sample code. Probably, it is just because ExtJS 4 is too new that developers have not catch up. However, since ExtJS is partially commercial product instead of a pure open source project, its quality should be well controlled before it announce final release. Here is a little bit experiece on how to make a CRUD grid with ExtJS 4.

To successfully use Grid, auto sync store, and rowediting plugin in ExtJS 4, there are several points need to be highlighted. I listed them as below,


  1. In your Model class, you have to either define a hardcoded 'id' property or use 'idProperty' to specify one column as 'id'.
  2. You server side code need to return processed records back to browser. I tried only send back "id" in "data" part. But, it is not successful.
  3. Be aware that the format of record in the "data" has JSON format.
  4. Be sure to implemented at least one Validator in your Model class because, in ExtJS source code AbstractStore.js, you can find the following code, which may always return true for a new created record in RowEditing plugin when the store is set as autoSync = true

/**
     * @private
     * Filter function for new records.
     */
    filterNew: function(item) {
        // only want phantom records that are valid
        return item.phantom === true && item.isValid();
    },

Most likely, we wont use auto sync store in real product. But, this example could be a good start.