I have read the RequestHandler part in cookbook. There are isXml(), isRss(), etc. But there's no isJson().
Any other way to check whether a request is JSON?
So when the url is mysite.com/products/view/1.json it will give JSON data, but without .json it will give the HTML View.
Thanks
I dont think cakePHP has some function like isJson() for json data, you could create your custom though, like:
//may be in your app controller
function isJson($data) {
return (json_decode($data) != NULL) ? true : false;
}
//and you can use it in your controller
if( $this->isJson($your_request_data) ) {
...
}
Added:
if you want to check .json extension and process accordingly, then you could do in your controller:
$this->request->params['ext']; //which would give you 'json' if you have .json extension
CakePHP is handling this correctly, because JSON is a response type and not a type of request. The terms request and response might be causing some confusing. The request object represents the header information of the HTTP request sent to the server. A browser usually sends POST or GET requests to a server, and those requests can not be formatted as JSON. So it's not possible for a request to be of type JSON.
With that said, the server can give a response of JSON and a browser can put in the request header that it supports a JSON response. So rather than check what the request was. Check what accepted responses are supported by the browser.
So instead of writing $this->request->isJson() you should write $this->request->accepts('application/json').
This information is ambiguously shown in the document here, but there is no reference see also links in the is(..) documentation. So many people look there first. Don't see JSON and assume something is missing.
If you want to use a request detector to check if the browser supports a JSON response, then you can easily add a one liner in your beforeFilter.
$this->request->addDetector('json',array('callback'=>function($req){return $req->accepts('application/json');}));
There is a risk associated with this approach, because a browser can send multiple response types as a possible response from the server. Including a wildcard for all types. So this limits you to only requests that indicate a JSON response is supported. Since JSON is a text format a type of text/plain is a valid response type for a browser expecting JSON.
We could modify our rule to include text/plain for JSON responses like this.
$this->request->addDetector('json',array('callback'=>function($req){
return $req->accepts('application/json') || $req->accepts('text/plain');
}));
That would include text/plain requests as a JSON response type, but now we have a problem. Just because the browser supports a text/plain response doesn't mean it's expecting a JSON response.
This is why it's better to incorporate a naming convention into your URL to indicate a JSON response. You can use a .json file extension or a /json/controller/action prefix.
I prefer to use a named prefix for URLs. That allows you to create json_action methods in your controller. You can then create a detector for the prefix like this.
$this->request->addDetector('json',array('callback'=>function($req){return isset($req->params['prefix']) && $req->params['prefix'] == 'json';}));
Now that detector will always work correctly, but I argue it's an incorrect usage of detecting a JSON request. Since there is no such thing as a JSON request. Only JSON responses.
You can make your own detectors. See: http://book.cakephp.org/2.0/en/controllers/request-response.html#inspecting-the-request
For example in your AppController.php
public function beforeFilter() {
$this->request->addDetector(
'json',
[
'callback' => [$this, 'isJson']
]
);
parent::beforeFilter();
}
public function isJson() {
return $this->response->type() === 'application/json';
}
Now you can use it:
$this->request->is('json'); // or
$this->request->isJson();
Have you looked through and followed the very detailed instructions in the book?:
http://book.cakephp.org/2.0/en/views/json-and-xml-views.html
class TestController extends Controller {
public $autoRender = false;
public function beforeFilter() {
$this->request->addDetector('json', array('env' => 'CONTENT_TYPE', 'pattern' => '/application\/json/i'));
parent::beforeFilter();
}
public function index() {
App::uses('HttpSocket', 'Network/Http');
$url = 'http://localhost/myapp/test/json';
$json = json_encode(
array('foo' => 'bar'),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
);
$options = array('header' => array('Content-Type' => 'application/json'));
$request = new HttpSocket();
$body = $request->post($url, $json, $options)->body;
$this->response->body($body);
}
public function json() {
if ($this->request->isJson()) {
$data = $this->request->input('json_decode');
$value = property_exists($data, 'foo') ? $data->foo : '';
}
$body = (isset($value) && $value === 'bar') ? 'ok' : 'fail';
$this->response->body($body);
}
}
Thanks a lot Mr #Schlaefer. I read your comment and try, Wow it's working now.
//AppController.php
function beforeFilter() {
$this->request->addDetector(
'json', [
'callback' => [$this, 'isJson']
]
);
parent::beforeFilter();
...
}
public function isJson() {
return $this->response->type() === 'application/json';
}
//TasksController.php
public $components = array('Paginator', 'Flash', Session','RequestHandler');
//Get tasks function return all tasks in json format
public function getTasks() {
$limit = 20;
$conditions = array();
if (!empty($this->request->query['status'])) {
$conditions = ['Task.status' => $this->request->query['status']];
}
if (!empty($this->request->query['limit'])) {
$limit = $this->request->query['limit'];
}
$this->Paginator->settings = array('limit' => $limit, 'conditions' => $conditions);
$tasks = $this->paginate();
if ($this->request->isJson()) {
$this->set(
array(
'tasks' => $tasks,
'_serialize' => array('tasks')
));
}
}
In case anybody is reading this in the days of CakePHP 4, the correct and easy way to do this is by using $this->request->is('json').
Related
I'm building a small application to store contacts in the database, I've finished the GET/POST routes, and worked fine, now I'm on the API routes (in order to use AJAX calls). I can store the information if all fields are present in the POST request, nonetheless, If I want to send messages back to the call (to send feedback about why the contact hasn't been stored) the response is sending me to the main route www.myapp.com (with no messages) and I want to send a json back with the "reason".
At this moment I only validate if the 'nombre', 'correo', 'telefono' have information with standard Laravel's request validate method.
This is my LeadController
public function storeApi(Request $request)
{
$request -> validate([
'nombre' => 'required',
'correo' => 'required' ,
'telefono' => 'required'
]);
if(Lead::create($request->all())){
$result[] = ['saved' => true];
}else{
$result[] = ['saved' => false,
'reason' => 'Some data is missing'];
return response()-> json($result);
};
return response()-> json($result);
}
When the record is stored, it does send back the Json {'saved' : true} but when fails It just sends you back to the '/' Route: www.myapp.com
How can I send the messages back to the POST call?
It is redirecting back to "/" because $request->validate() method throws \Illuminate\Validation\ValidationException exception..
There are try ways to handle this request.
Put try catch block around your validate code
Or Handle this expection in app\Exception\Handler.php, and return the response in JSON format.
After some further reading I just change the way the information is validated using the Validator Class:
public function storeApi(Request $request)
{
$validator = \Validator::make($request->all(), ['nombre' => 'required', 'correo' => 'required', 'telefono' => 'required']);
if($validator->fails()){
return response()->json($validator->errors(), 422);
}else {
//ready to store
}
}
This way I don't let the ValidationException exception occurs before sending the feedback to the call.
I want to add some api to my Yii2 site. Api must be only in json. I don't want to set Accept: application/json headers for each request. I can set 'response' => ['format' => \yii\web\Response::FORMAT_JSON] in application configuration but it breaks all pages. Also my api function returns data in xml.
I tried to use rest\ActiveRecord for my purposes. Maybe I do it's wrong. What I want.
To have my Yii2 based site with some api acсessed through https://example.com/api/controller/action. In project I want to see folder controllers/api which contains my controllers. Controllers must use standard \yii\db\ActiveRecord based models. Also controllers input paramaters only in json body or as part url and output data only in json.
You may need to set the following code in the controller's action somewhere before return or in beforeAction() method:
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
also since Yii 2.0.11 there is a dedicated asJson() method to return a response in JSON format:
return $this->asJson($array);
The more elegant solution is to use yii\filters\ContentNegotiator.
When the Accept header is missing ContentNegotiator assumes it allows any type and send response in first format defined in its $formats property. If the requested format is not among accepted formats the content negotiator will throw yii\web\NotAcceptableHttpException and app will respond with http status 406 Not Acceptable.
You can add it in your controller in behaviors() method like this:
public function behaviors()
{
return [
[
'class' => 'yii\filters\ContentNegotiator',
'formats' => [
'application/json' => \yii\web\Response::FORMAT_JSON,
],
],
];
}
If your controller extends yii\rest\Controller it already has the ContentNegotiator filter added among its behaviors. You only need to limit allowed formats like this:
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['contentNegotiator']['formats'] = [
'application/json' => \yii\web\Response::FORMAT_JSON,
];
return $behaviors;
}
Using ContentNegotiator instead of explicitly forcing the JSON format in beforeAction() will allow for easier addition of other formats if they are needed in future.
My company uses a standard format for healthcheck responses in internal apis etc. We either return the status with a content type of application/status+json on success or application/problem+json if we have an issue (part of this proposed spec).
But if i set the content type to either of these my response becomes an emptu 406 response.
So, how can I tell the JsonOutputFormatter that it can add these Json header types to it's SupportedMediaTypes collection?
I would expect I could do something like:
services.AddMvc().AddJsonOptions(jsonOptions => {
jsonOptions.SerializerSettings.SupportedMediaTypes.Add("application/problem+json");
});
But of course I can't find a way to do that.
Okay, so here is a way to do it. I found the OutputFormatters collection and was able to pull out the JsonOutputFormatter. From there you can add a supported media type:
services.AddMvc(mvcOptions => {
//TODO: make extension method
var jFormatter = mvcOptions.OutputFormatters.FirstOrDefault(f => f.GetType() == typeof(JsonOutputFormatter)) as JsonOutputFormatter;
jFormatter?.SupportedMediaTypes.Add("application/problem+json");
jFormatter?.SupportedMediaTypes.Add("application/status+json");
});
Or, as an extension method:
public static IMvcBuilder AddStatusJsonSupport(this IMvcBuilder builder) {
builder.AddMvcOptions(options => {
var jFormatter = options.OutputFormatters.FirstOrDefault(f => f.GetType() == typeof(JsonOutputFormatter)) as JsonOutputFormatter;
jFormatter?.SupportedMediaTypes.Add("application/problem+json");
jFormatter?.SupportedMediaTypes.Add("application/status+json");
});
return builder;
}
called like so:
services.AddMvc().AddStatusJsonSupport();
I need to implement xsolla payment solution into my cakePHP 2.6 webapp.
By contract my site should communicate via REST with Xsolla.
Xsolla does all the requests to the same url (e.g. http://example.com/rest) and specifies the type of request in the JSON body,
e.g. request from Xsolla:
URL: http://example.com/rest
Accept: application/json
Content-Type: application/json
Content-Length: 78
Authorization: Signature 8189119fb35327cdee7787990df41001c4bd9122
{"data":{"notification_type":"user_validation","user":{"id":"user_id"}}}
I should return HTTP/1.1 400 Bad Request with error code if the user id is invalid or HTTP/1.1 200 OK if the user id is valid.
notification_type could be user_validation, payment and so on.
I implemented all the communication in XsollaController.php in a single function xsolla:
//XsollaController.php:
public function xsolla() {
//...
$data = $this->request->data;
if(array_key_exists("notification_type", $data) &&
$data["notification_type"]=="user_validation") {
//user_validation
}
else if(array_key_exists("notification_type", $data) &&
$data["notification_type"]=="payment") {
//payment
}
//...
}
How can I have different functions based on notification_type?
e.g. function userValidation($data), payment($data) etc.
Also what the proper way to return simple JSON with specified HTTP code?
Now I do the following:
if(userIdIsValid($userID)) {
$body = array('data' => array('user' => array(
"id"=>$data["user"]["id"]), 'message' => 'USER_IS_VALID'));
$this->response->type('json');
$this->response->statusCode(200);
$this->response->body(json_encode($body));
$this->response->send();
$this->_stop();
exit();
}
else {
$body = array('error' => array(
'code' => 'INVALID_USER',
'message' => 'INVALID_USER',
'user' => array("id"=>$data["user"]["id"])));
$this->response->type('json');
$this->response->statusCode(400);
$this->response->body(json_encode($body));
$this->response->send();
$this->_stop();
exit();
}
My code works but seems hard coded. I believe CakePHP provide a better way to do it.
For your first question of executing different functions based on the notification_type, you are almost there (unless I am oversimplifying your question).
//XsollaController.php:
public function xsolla() {
//...
$data = $this->request->data;
if(array_key_exists("notification_type", $data) &&
$data["notification_type"]=="user_validation") {
$this->userValidation($data);
}
else if(array_key_exists("notification_type", $data) &&
$data["notification_type"]=="payment") {
$this->payment($data);
}
//...
}
private function userValidation($data) {
// user validation
}
private function payment($data) {
// payment
}
As far as responding with JSON content and the correct HTTP status code, your code looks fine. There's room for some minor improvements, but if it's functional, I'd leave it. You are setting the response code correctly. If you want the truly "cake" way of responding with JSON content, read up on JSON and XML views in the CakePHP docs.
I've got a CRUD form generated via the SensioGeneratorBundle, as described here. This works great.
However, I would like to also return JSON, rather than HTML responses, if the "Accept" HTTP header contains only "application/json". I'm working on a prototype for a JSON service and this would help me jump start things.
I figured out that I can turn my entities into a JSON string like this:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new
JsonEncoder()));
$json = $serializer->serialize($entity, 'json');
However, at the end of that, $json contains a string that is my JSON data. I want to just directly output that to the requestor, rather than render the usual view. I've tried returning a new JsonResponse($json), but it re-encodes the JSON string, so it winds up double-encoded.
So I have two questions:
What is the "correct" way to inspect the HTTP requestion headers? I know I can just look in $_SERVER, but I'm thinking that there may be a better way to do this in Symfony2.
What is the "correct" way to return a JSON string, or to translate my entities into JSON that is returned straight to the requestor, without rendering the usual view.
Thanks!
This will verify if the current request is XHR and then send back properly formatted JSON data:
public function someAction(Request $request)
{
if ($request->isXmlHttpRequest()) {
$serializer = new Serializer(array(
new GetSetMethodNormalizer()
), array(
'json' => new JsonEncoder()
));
$response = $serializer->serialize(array(
'success' => true,
'data' => array(
'entity' => $entities,
)
), 'json');
return new Response($response, 200, array('Content-Type' => 'application/json'));
} else {
// Run "normal" request code, render a view
}
}
By the way, JMSSerializerBundle makes the serializing syntax more straightforward (because, let's face it, the native Symfony way for this is ugly) and also provides some additionnal features such as excluding entity fields to serialize (through annotations).
With JMS, my code looks like this:
if ($request->isXmlHttpRequest()) {
$response = array('success' => true, 'data' => array(
'entity' => $this->container->get('serializer')->serialize($entity, 'json'),
'lastPage' => $lastPage,
));
return new Response(json_encode($response), 200, array('Content-Type' => 'application/json'));
}
And finally, 'success' and 'data' are in no way required, it's just the structure I use to split status and data to be readable in JavaScript.