phpjsonajaxjquery-3cakephp-3.8

jQuery ajax call can't read json encoded data from CakePHP 3.8 (gets an empty array)


I have a weird problem with reading json encoded data returned by my CakePHP3 API in response to an ajax call from jQuery. I have already read over 20 posts on stackoverflow and elsewhere and the usual problems people encountered where due to wrong dataType, contentType or it was the server not getting the data from ajax. None of these cases are applicable here (I tried different settings with no impact on my problem).

The problem:

My ajax call sends some parameters to my CakePHP3 API, the API gets the parameters correctly and returns a json encoded array of CakePHP entities (each entity has an additional property 'available_yield' added before it is sent back to the ajax call). I get the correct output using a direct URL in a browser (checked it with json validators, it's all good), but my ajax call (I used console and network tabs in Chrome devtools to investigate) shows an empty array for a well-formed json.

My investigation showed that the problem occurs when I modify CakePHP entities. If I return the original data from the API json encoded, the jquery ajax gets the right data. But when I modify any entity, the array in jquery ajax is empty.

Debugging from CakePHP shows that both arrays (unmodified and modified) look exactly the same except the added property, i.e. they are well-formed and OK in all respect, both are in json, both are OK in the browser. But the modified one is not accepted by jquery as json.

A solution at the moment seems to be: don't modify your data! But that's what we do on the server before sending the relevant and processed data to the client, don't we?

Has anyone had a similar problem?

I attach my code:

CakePHP API function:

function myFunction(){
$params = $this->getRequest()->getQueryParams();
        //debug($params);
        $componentReference = $params['component_reference'];
        $componentTypeId = $params['component_type_id'];

        $matchingCrops = $this->Crops->find()->select(['id', 'grower_name', 'bulk'])->where(['reference' => $componentReference]);

        $cropsWithYieldInfo = []; //to hold modify crop
        foreach($matchingCrops as $crop){
            $availableYield = $this->Crops->calculateAvailableYield($crop->id); //returns a string
            if(isset($availableYield) && !empty($availableYield)){
                $crop->available_yield = number_format($availableYield,1);  //tried $crop['available_yield'] as well, same result
                $cropsWithYieldInfo[] = $crop;
            }
        }

//        debug($cropsWithYieldInfo);
//        debug($matchingCrops);

        //$content = json_encode($cropsWithYieldInfo);  // <<-- changing to $matchingCrops makes ajax see the array, but the array does not have my calculated data
        $content = json_encode($matchingCrops);

        $this->response = $this->response->withStringBody($content);
        $this->response = $this->response->withType('json');  
        $this->autoRender = false; 
        return $this->response;
} 

my AJAX:

function myAjax(){
 $.ajax({
                type: 'GET',
                url: url,
                //contentType: "application/json",
                dataType: "json"
            })
            .done(function (data) {
                console.log(data);  
            })
            .fail(function (data) {
                console.log('AJAX call to /'+errMsg+' function failed');
            })
}

JSON data returned from API:

EDIT: Might be important: When I access API via URL in the browser it always returns modified data; it looks like my code modifies the actual entities in $matchingCrops set. So if set $content to $matchingCrops or $cropsWithYieldInfo, the result in the browser is always the same. But it differs when accessing the API via ajax: when $content = json_encoded($matchingCrops) I get the original unmodified array of data, when $content = json_encoded($cropsWithYieldInfo) I get an empty array.

This is really weird: why the browser gets always the modified array, and ajax gets either one or the other??? I understand that if I modify $crop entity then it modifies the entity inside the resultant set, but I would expect this to be consistent for both browser and the ajax call.

EDIT: I tried a slightly modified code to see if cloning entities will make any difference but the only difference is that now the browser gets what I would expect to happen (either the original unmodified array, or modified one) and it is consistent with what ajax gets. But this does not solve the problem (ajax still gets empty array if the array was modified).

foreach($matchingCrops as $crop){
            $modCrop = clone $crop;
            $availableYield = $this->Crops->calculateAvailableYield($crop->id); //returns a string
            if(isset($availableYield) && !empty($availableYield)){
                $modCrop->available_yield = number_format($availableYield,1);  //tried $crop['available_yield'] as well, same result
                $cropsWithYieldInfo[] = $modCrop;
            }
        }

Modified (ajax gets this as empty array; browser always gets that from the API):

[{"id":12345,"grower_name":"XYZ","bulk":false,"available_yield":"4.1"},{"id":23456,"grower_name":null,"bulk":true,"available_yield":"190.0"}]

Unmodified (ajax gets this correctly):

[{"id":12345,"grower_name":"XYZ","bulk":false},{"id":23456,"grower_name":null,"bulk":true}]


Solution

  • OMG...found it! Okay, that is embarrassing but I am still gonna post it as a LESSON LEARNED and as a warning to other folks: if you have a problem in the evening that you can't solve, go home and have a good sleep, start again in the morning!

    Causes of the problem:

    1) my calculation function was actually returning a float not a string, and I was checking for emptiness, so when it returned 0, the code was not adding the 'available_yield' property to the $crop entity (because the line of code responsible for that was also in the wrong place! should have been outside of the if block)

    At this point I was still like 'alright, but I should get consistent behaviour both in the browser and in the ajax call!!!', unless...

    2) I did not notice that I used a different id for the browser check and for the ajax call, so the computer was right ... :-/

    Always learning ...

    The version of the code that works OK:

    function myFunction(){
    $params = $this->getRequest()->getQueryParams();
            //debug($params);
            $componentReference = $params['component_reference'];
            $componentTypeId = $params['component_type_id'];
    
            $matchingCrops = $this->Crops->find()->select(['id', 'grower_name', 'bulk'])->where(['reference' => $componentReference]);
    
            $cropsWithYieldInfo = []; //to hold modify crop
            $cropsWithYieldString = '';
            foreach($matchingCrops as $crop){
                $availableYield = $this->Crops->calculateAvailableYield($crop->id); //returns a float not string! 
                if(isset($availableYield)){ //<<- that was the cause of the problem; !empty(float) will ignore 0, just check if it's set
                    $crop->available_yield = number_format($availableYield,1); 
                }
                $cropsWithYieldInfo[] = $crop;
            }
    
    //        debug($cropsWithYieldInfo);
    //        debug($matchingCrops);
    
            $content = json_encode($cropsWithYieldInfo); 
    
            //$this->response = $this->response->withStringBody($content);
            //$this->response = $this->response->withType('application/json');  
            $this->autoRender = false; 
            //return $this->response;
            //more concisely
            return $this->response->withType('application/json')->withStringBody($content);
    
    }
    

    Thanks for your time guys, you kept me focused on finding the solution.