1

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}]

10
  • Simple question is the best question!
    – user1805543
    Commented Feb 6, 2020 at 17:51
  • 2
    Controllers should never echo data, it will only cause problems!
    – ndm
    Commented Feb 7, 2020 at 0:16
  • @ndm I can return response (instead of echoing data) but this changes nothing in my situation. But thanks for the general advice.
    – djevulen
    Commented Feb 7, 2020 at 10:21
  • 2
    @djevulen The thing is that it can not only create (additional) problems for you, it's also a problem for the people trying to help, as its unclear what might actually cause the behavior, and things might behave differently in peoples local environments when echoing as opposed to in your environment, hence even if it seemingly makes no difference for you, it is advised that you supply and use an example that is doing it the intended way, so that the possible sources of problems are being kept as small as possible.
    – ndm
    Commented Feb 7, 2020 at 10:36
  • 1
    Did you try return $this->response->withType("application/json")->withStringBody(json_encode($result)); instead of return $this->response;
    – user1805543
    Commented Feb 7, 2020 at 11:10

2 Answers 2

1
$array = ['foo'=>'bar'];

$this->set([
    'response' => $array,
    '_serialize' => 'response',
]);
$this->Request->renderAs($this, 'json');

And than I would seriliaze ajax! So, You wouldn't need to stringify the object, You could use it directly for the data property.

$.ajax({
    type: 'POST',
    url: url,                      
    data: {YourArray: YourVariables},
    success: function(data) {
      alert(data);
    }
});

You can Find more here : https://api.jquery.com/serialize/

Lets look into your ajax you need to pass values from array to ajax to get response of each value which is you are trying to do +errMsg+.

Your ajax should look like this in fail and success:

fail: function( jqXHR, Status, errMsg) { then you could show response like console.log('AJAX call to /'+errMsg+' function failed');

$.ajax({
    type: "GET",
    url: url,
    data: {
      title: $(value[0]).val(),
      description: $(value[1]).val()
    },
    success: function (data) {
        if(data === "success") {
            // do something with data or whatever other data on success
            console.log('success');
        } else if(data === "error") {
            // do something with data or whatever other data on error
            console.log('error');
        }
    }
});

To show specified error you need to pass title = $(value[0]).val() in success function.

Or use ajax serializeArray() and each() example here https://www.w3schools.com/jquery/tryit.asp?filename=tryjquery_ajax_serializearray

Since CakePHP 3.4, use should use

$content = json_encode($matchingCrops);
return $this->response->withType("application/json")->withStringBody($content);

Instead of This

$content = json_encode($matchingCrops);

$this->response = $this->response->withStringBody($content);
$this->response = $this->response->withType('json');  
$this->autoRender = false; 
return $this->response;
3
  • Well, the source of the problem in this case was between the keyboard and the chair ;) but thanks for your answer. I like your concise $this->response... and I changed my code to save lines.
    – djevulen
    Commented Feb 7, 2020 at 12:54
  • @djevulen I commented that in my comment under question 1 hour ago I think :) you wasted 1 hour :) anyway! glad to see you solved.
    – user1805543
    Commented Feb 7, 2020 at 12:56
  • Your comment under question 1 was not a solution to the problem. In fact the result I get is the same both with your code and mine. Yours is more concise, that's the main difference.
    – djevulen
    Commented Feb 7, 2020 at 13:00
0

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

  • double-check every single line and debug all possible vars!
  • double-check your TEST DATA!

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.

2
  • You should wait for two minutes :)
    – user1805543
    Commented Feb 7, 2020 at 12:35
  • @Dlk Have read your answer. Thanks for pointing out that my ajax fail method could benefit from function( jqXHR, Status, errMsg) :)
    – djevulen
    Commented Feb 7, 2020 at 12:46

Not the answer you're looking for? Browse other questions tagged or ask your own question.