CakePHP (2.4) routes json - cakephp

I have this in my routes file:
CakePlugin::routes();
Router::mapResources('api');
Router::parseExtensions('json');
Currently if I call a controller I have Api with .json as an extension as long as it's a HTTP GET (not post) it outputs json which is fine, no matter the method/function name as long as it exists in my Api controller.
If I make a post, whilst I can decode the posted JSON whatever function/method I've called, it errors saying I'm missing xxx.ctp in app/Api/Views/json/
xxx.ctp = the name of any function I've called to post.
2 Questions/problems.
Ideally I want to parse any request to the Api controller as json, but without having to specify the .json extension in the url.
Secondly, how/why can't the HTTP POST output json like the HTTP GET, do I need to map something else somewhere?
Many thanks

If you want to render everything from a specific controller (in your case, ApiController.php) as JSON without requiring the user to append the .json extension on their request, you can use renderAs and setContent in your beforeFilter.
public function beforeFilter() {
parent::beforeFilter();
$this->RequestHandler->setContent('json');
$this->RequestHandler->renderAs($this, 'json');
}
renderAs and setContent are part of RequestHandler.
This does mean that this controller will never return anything other than json. If your happy with that, you can even remove extension catcher in your routes.php file...
Router::parseExtensions('json');
Remembering that if you remove the above line from your routes.php file, a request to your ApiController of any kind will result in a 404 being thrown (not as JSON).
Developing further using the beforeFilter you can actually render as different content-types depending on the type of request. For example..
public function beforeFilter() {
parent::beforeFilter();
if ($this->RequestHandler->isGet()) {
$this->RequestHandler->setContent('json');
$this->RequestHandler->renderAs($this, 'json');
}
}

Related

Setting a header in CakePHP (MVC)

I'm trying to integrate PayPal's IPN code into CakePHP 3.
namespace App\Controller;
use PayPal\Api\PaypalIPN;
class IpnController extends AppController
{
public function index()
{
$this->autoRender = false;
$ipn = new PayPalIPN();
// Use the sandbox endpoint during testing.
$ipn->useSandbox();
$verified = $ipn->verifyIPN();
if ($verified) {
/*
* Process IPN
* A list of variables is available here:
* https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*/
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly.
header("HTTP/1.1 200 OK");
}
}
This is failing to validate on PayPal's end and I'm suspecting it has to do with setting the headers in the controller view.
Is there a way to set the header properly in CakePHP's controller.
I had this code running stand alone (in just a php file) and it seemed to work just fine.
You should not output any data in your controller action - that means you should not use echo, header() or any function or construct that would return anything to browser. If you do, you will encounter a "headers already sent" error.
If you want to set headers, you should use withHeader() or withAddedHeader() methods of Cake\Http\Response.
For status codes, you also have withStatus() method:
$response = $this->response;
$response = $response->withStatus(200,"OK");
return $response; // returning response will stop controller from rendering a view.
More about setting headers can be found in docs:
Setting response headers in CakePHP 3
Cake\Http\Response::withStatus()
Maybe that's not very Cakish, but actually one can send headers this way - it just have to be followed by die; or exit; to prevent app from further response processing.
Anyway, for sure your problem is not associated with headers. IPN seems to doesn't work properly with Paypal Sandbox. Maybe you should try it other way with ApiContext class?

CakePHP 3 - Passing parameters to application with a single controller where all requests are routed to it

CakePHP 3.x
In the Routing documentation (https://book.cakephp.org/3.0/en/development/routing.html) it says:
If you have a single controller in your application and you do not want the controller name to appear in the URL, you can map all URLs to actions in your controller. For example, to map all URLs to actions of the home controller, e.g have URLs like /demo instead of /home/demo, you can do the following:
$routes->connect('/:action', ['controller' => 'Home']);
That's fine as it means I can do stuff like this in my src/Controller/HomeController.php:
public function foo()
{
// Accessible through URL "/foo"
}
public function bar()
{
// Accessible through URL "/bar"
}
But if I need to pass parameters to a function, e.g.
public function baz($a, $b, $c)
{
}
It will give me a "Missing Controller" error if I call any of the following URLs:
/baz/2
/baz/2/17
/baz/2/17/99
It's saying that I need to create "BazController".
All of these, will work, because they include the controller name:
/home/baz/2
/home/baz/2/17
/home/baz/2/17/99
Presumably this is a routing problem?
Interestingly, calling /baz without any parameters works ok.
The correct answer, as provided in a comment by #arilia is:
$routes->connect('/:action/*', ['controller' => 'Home']);
The URL's provided in my question will now work as follows. All of these will be mapped to HomeController.php and execute the baz() function:
/baz/2
/baz/2/17
/baz/2/17/99
The * (at the end of /:action/* in app/routes.php) is key to allowing any number of parameters passed in the URL.

Cakephp 3 - CRUD plugin - Use id from auth component

Currently, I'm using the CRUD v4 plugin for Cakephp 3. For the edit function in my user controller it is important that only a user itself can alter his or her credentials. I want to make this possible by inserting the user id from the authentication component. The following controller method:
public function edit($id = null){
$this->Crud->on('beforeSave', function(\Cake\Event\Event $event) {
$event->subject()->entity->id = $this->Auth->user('id');
});
return $this->Crud->execute();
}
How can I make sure I don't need to give the id through the url? The standard implementation requires the url give like this: http://domain.com/api/users/edit/1.json through PUT request. What I want to do is that a user can just fill in http://domain.com/api/users/edit.json and send a JSON body with it.
I already tried several things under which:
$id = null when the parameter is given, like in the example above. Without giving any id in the url this will throw a 404 error which is caused by the _notFound method in the FindMethodTrait.php
Use beforeFind instead of beforeSave. This doesn't work either since this isn't the appropriate method for the edit function.
Give just a random id which doesn't exist in the database. This will through a 404 error. I think this is the most significant sign (combined with point 1) that there is something wrong. Since I try to overwrite this value, the CRUD plugin doesn't allow me to do that in a way that my inserting value is just totally ignored (overwriting the $event->subject()->entity->id).
Try to access the method with PUT through http://domain.com/api/users.json. This will try to route the action to the index method.
Just a few checks: the controllerTrait is used in my AppController and the crud edit function is not disabled.
Does anyone know what I'm doing wrong here? Is this a bug?
I personally would use the controller authorize in the Auth component to prevent anyone from updating someone else's information. That way you do not have to change up the crud code. Something like this...
Add this line to config of the Auth component (which is probably in your AppController):
'authorize' => ['Controller']
Then, inside the app controller create a function called isAuthorized:
public function isAuthorized($user) {
return true;
}
Then, inside your UsersController you can override the isAuthorized function:
public function isAuthorized($user) {
// The owner of an article can edit and delete it
if (in_array($this->request->action, ['edit'])) {
$userId = (int)$this->request->params['pass'][0];
if ($user['id'] !== $userId) {
return false;
}
}
return parent::isAuthorized($user);
}

What's the proper way to serve JSONP with CakePHP?

I want to serve JSONP content with CakePHP and was wondering what's the proper way of doing it so.
Currently I'm able to serve JSON content automatically by following this CakePHP guide.
Ok, I found a solution on this site. Basically you override the afterFilter method with:
public function afterFilter() {
parent::afterFilter();
if (empty($this->request->query['callback']) || $this->response->type() != 'application/json') {
return;
}
// jsonp response
App::uses('Sanitize', 'Utility');
$callbackFuncName = Sanitize::clean($this->request->query['callback']);
$out = $this->response->body();
$out = sprintf("%s(%s)", $callbackFuncName, $out);
$this->response->body($out);
}
I hope it helps someone else as well.
I've as yet not found a complete example of how to correctly return JSONP using CakePHP 2, so I'm going to write it down. OP asks for the correct way, but his answer doesn't use the native options available now in 2.4. For 2.4+, this is the correct method, straight from their documentation:
Set up your views to accept/use JSON (documentation):
Add Router::parseExtensions('json'); to your routes.php config file. This tells Cake to accept .json URI extensions
Add RequestHandler to the list of components in the controller you're going to be using
Cake gets smart here, and now offers you different views for normal requests and JSON/XML etc. requests, allowing you flexibility in how to return those results, if needed. You should now be able to access an action in your controller by:
using the URI /controller/action (which would use the view in /view/controller/action.ctp), OR
using the URI /controller/action.json (which would use the view in /view/controller/json/action.ctp)
If you don't want to define those views i.e. you don't need to do any further processing, and the response is ready to go, you can tell CakePHP to ignore the views and return the data immediately using _serialize. Using _serialize will tell Cake to format your response in the correct format (XML, JSON etc.), set the headers and return it as needed without you needing to do anything else (documentation). To take advantage of this magic:
Set the variables you want to return as you would a view variable i.e. $this->set('post', $post);
Tell Cake to serialize it into XML, JSON etc. by calling $this->set('_serialize', array('posts'));, where the parameter is the view variable you just set in the previous line
And that's it. All headers and responses will be taken over by Cake. This just leaves the JSONP to get working (documentation):
Tell Cake to consider the request a JSONP request by setting $this->set('_jsonp', true);, and Cake will go find the callback function name parameter, and format the response to work with that callback function name. Literally, setting that one parameter does all the work for you.
So, assuming you've set up Cake to accept .json requests, this is what your typical action could look like to work with JSONP:
public function getTheFirstPost()
$post = $this->Post->find('first');
$this->set(array(
'post' => $post, <-- Set the post in the view
'_serialize' => array('post'), <-- Tell cake to use that post
'_jsonp' => true <-- And wrap it in the callback function
)
);
And the JS:
$.ajax({
url: "/controller/get-the-first-post.json",
context: document.body,
dataType: 'jsonp'
}).done(function (data) {
console.log(data);
});
For CakePHP 2.4 and above, you can do this instead.
http://book.cakephp.org/2.0/en/views/json-and-xml-views.html#jsonp-response
So you can simply write:
$this->set('_jsonp', true);
in the relevant action.
Or you can simply write:
/**
*
* beforeRender method
*
* #return void
*/
public function beforeRender() {
parent::beforeRender();
$this->set('_jsonp', true);
}

Respond with json in CakePHP controller

My code looks something like this:
if ($this->request->is('ajax')) {
$this->Comment->Save();
$this->set('comment', $this->Comment->read());
$this->set('_serialize', array('comment');
}
Instead of responding with Ajax, I get an error that a view is missing. Is there something else that's needed to respond with json? I thought this was handled "automagically" with the response helper.
By enabling RequestHandlerComponent in your application, and enabling support for the xml
and or json extensions, you can automatically leverage the new view classes.
So you still need to enable a few things:
Add
public $components = array('RequestHandler');
and in routes.php
Router::parseExtensions(array('json'));
You may have to have your url look like controller/action.json for the automagic to work. You could just add $this->viewClass = 'Json' in the controller, though (not 100% sure on this).

Resources