.. Couldn't think of a descriptive enough title. What I'm asking for is how do I do this?
I want the following 2 API calls
GET /api/users/2/duels - returns all of the duels for user 2
GET /api/users/2 - returns the profile for user 2
Since PHP doesn't support method overloading, it isn't clear to me how to make this work.
Currently I have the function
function get($id, $action){
//returns data based on action and id
}
And I can't just make
function get($id){
//returns profile based on id
}
because of said reasons.
Any help is greatly appreciated!!!
You can use the #url phpdoc decorator to tell restler of any special invocation schemes that doesn't match the direct class->method mapping.
/**
* #url GET /api/users/:userId/duels
*/
public function getDuels($userId)
{
}
.. should probably work.
One approach is handling both cases in same function with a conditional block as shown below
function get($id, $action=null){
if(is_null($action)){
//handle it as just $id case
}else{
//handle it as $id and $action case
}
}
if you are running restler 3 and above you have to disable smart routing
/**
* #smart-auto-routing false
*/
function get($id, $action=null){
if(is_null($action)){
//handle it as just $id case
}else{
//handle it as $id and $action case
}
}
Another approach is to have multiple functions since index also maps to root, you have few options, you can name your functions as get, index, getIndex
function get($id, $action){
//returns data based on action and id
}
function index($id){
//returns profile based on id
}
If you are using Restler 2 or turning smart routing off, order of the functions is important to fight ambiguity
If you are running out of options for function names you can use #url mapping as #fiskfisk suggested but the route should only include from method level as the class route is always prepended unless you turn it off using $r->addAPIClass('MyClass','');
function get($id){
//returns data based on action and id
}
/**
* #url GET :id/duels
*/
function duels($id)
{
}
HTH
Related
I'm trying to use Symfony Voters and Controller Annotation to allow or restrict access to certain actions in my Symfony 4 Application.
As an example, My front-end provides the ability to delete a "Post", but only if the user has the "DELETE_POST" attribute set for that post.
The front end sends an HTTP "DELETE" action to my symfony endpoint, passing the id of the post in the URL (i.e. /api/post/delete/19).
I'm trying to use the #IsGranted Annotation, as described here.
Here's my symfony endpoint:
/**
* #Route("/delete/{id}")
* #Method("DELETE")
* #IsGranted("DELETE_POST", subject="post")
*/
public function deletePost($post) {
... some logic to delete post
return new Response("Deleting " . $post->getId());
}
Here's my Voter:
class PostVoter extends Voter {
private $attributes = array(
"VIEW_POST", "EDIT_POST", "DELETE_POST", "CREATE_POST"
);
protected function supports($attribute, $subject) {
return in_array($attribute, $this->attributes, true) && $subject instanceof Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
... logic to figure out if user has permissions.
return $check;
}
}
The problem I'm having is that my front end is simply sending the resource ID to my endpoint. Symfony is then resolving the #IsGranted Annotation by calling the Voters and passing in the attribute "DELETE_POST" and the post id.
The problem is, $post is just a post id, not an actual Post object. So when the Voter gets to $subject instanceof Post it returns false.
I've tried injecting Post into my controller method by changing the method signature to public function deletePost(Post $post). Of course this does not work, because javascript is sending an id in the URL, not a Post object.
(BTW: I know this type of injection should work with Doctrine, but I am not using Doctrine).
My question is how do I get #IsGranted to understand that "post" should be a post object? Is there a way to tell it to look up Post from the id passed in and evaluated based on that? Or even defer to another controller method to determine what subject="post" should represent?
Thanks.
UPDATE
Thanks to #NicolasB, I've added a ParamConverter:
class PostConverter implements ParamConverterInterface {
private $dao;
public function __construct(MySqlPostDAO $dao) {
$this->dao = $dao;
}
public function apply(Request $request, ParamConverter $configuration) {
$name = $configuration->getName();
$object = $this->dao->getById($request->get("id"));
if (!$object) {
throw new NotFoundHttpException("Post not found!");
}
$request->attributes->set($name, $object);
return true;
}
public function supports(ParamConverter $configuration) {
if ($configuration->getClass() === "App\\Model\\Objects\\Post") {
return true;
}
return false;
}
}
This appears to be working as expected. I didn't even have to use the #ParamConverter annotation to make it work. The only other change I had to make to the controller was changing the method signature of my route to public function deletePost(Post $post) (as I had tried previously - but now works due to my PostConverter).
My final two questions would be:
What exactly should I check for in the supports() method? I'm currently just checking that the class matches. Should I also be checking that $configuration->getName() == "id", to ensure I'm working with the correct field?
How might I go about making it more generic? Am I correct in assuming that anytime you inject an entity in a controller method, Symfony will call the supports method on everything that implements ParamConverterInterface?
Thanks.
What would happen if you used Doctrine is that you'd need to type-hint your $post variable. After you've done that, Doctrine's ParamConverter would take care of the rest. Right now, Symfony has no idea how about how to related your id url placeholder to your $post parameter, because it doesn't know which Entity $post refers to. By type-hinting it with something like public function deletePost(Post $post) and using a ParamConverter, Symfony would know that $post refers to the Post entity with the id from the url's id placeholder.
From the doc:
Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.
The Voter would then also know what $post is and how to treat it.
Now since you are not using Doctrine, you don't have a ParamConverter by default, and as we just saw, this is the crucial element here. So what you're going to have to do is simply to define your own ParamConverter.
This page of the Symfony documentation will tell you more about how to do that, especially the last section "Creating a Converter". You will have to tell it how to convert the string "id" into a Post object using your model's logic. At first, you can make it very specific to Post objects (and you may want to refer to that one ParamConverter explicitly in the annotation using the converter="name" option). Later on once you've got a working version, you can make it work more generic.
So I started using this little library for creating a RESTful PHP server, right here.
In the code, I noticed that it appears that the comments are actually significant, in other words, if I change the comments, it actually changes the behavior of the code. Is this normal practice? I've never seen this used before and it seems weird to me to not ignore comments.
class TestController
{
/**
* Returns a JSON string object to the browser when hitting the root of the domain
*
* #url GET /
*/
public function test()
{
return "Hello World";
}
/**
* Logs in a user with the given username and password POSTed. Though true
* REST doesn't believe in sessions, it is often desirable for an AJAX server.
*
* #url POST /login
*/
public function login()
{
$username = $_POST['username'];
$password = $_POST['password']; //#todo remove since it is not needed anywhere
return array("success" => "Logged in " . $username);
}
/**
* Gets the user by id or current user
*
* #url GET /users/$id
* #url GET /users/current
*/
public function getUser($id = null)
{
// if ($id) {
// $user = User::load($id); // possible user loading method
// } else {
// $user = $_SESSION['user'];
// }
return array("id" => $id, "name" => null); // serializes object into JSON
}
Basically, the #url blocks actually define what request types to which URLs call the function below them. What is the scope of this, does it have to be the #lines right above the function? Is this standard PHP practice?
It is PHP Doc. See https://phpdoc.org/docs/latest/guides/docblocks.html and specifically https://phpdoc.org/docs/latest/guides/docblocks.html#tags
A tag always starts on a new line with an at-sign (#) followed by the name of the tag. Between the start of the line and the tag’s name (including at-sign) there may be one or more spaces or tabs.
Erm... Yes and No!
No, in the sense that it's not a normal PHP feature. In PHP, a comment is a comment and PHP makes no attempt to parse its content.
Yes in the sense that because PHP won't parse the comment, developers sometimes use it as a place to store data for their libraries. The Symfony framework is a good example.
In this case, the library you installed is parsing the comments in the class RestServer.php itself. You can read the class yourself, although there's some pretty hardcore PHP and Regex in there.
This is a follow up question for my first question from Hiding or removing controller name in url using routes for seo purpose = codeigniter
I need to hide or remove the controller name from the url. So I followed the answer given to me by Nucleo 1985, this works perfectly fine for static pages. I know that my question is somewhat different so I got different solution.
I am using one controller.
I have a function in my controller that has a switch case on it. Every case contains url. example (http://www.sample.com/my_controller/my_function/my_case_url). The /my_case_url is the dynamic.
I created individual routes for every function and it's quite hustle and not applicable to my function that has a switch case url.
My question is.
How can I achieve a url like http://www.sample.com/my_function/ and http://www.sample.com/my_function/my_case_url/? (The function name must be removed or hide when the link is clicked and redirect to the page)
I need this for SEO purposes.
Thank you!
this will work same as your first question
// it will go to my_controller index
$route['my_function'] = 'my_controller';
// you can set specific controller method remove method will go to index.
$route['chomy_functione/(:any)'] = 'my_controller/my_function';
This should be done from project_name/application/config/routes.php this file.Add a line
$route['url_first/url_second'] = "any_controller_name/function_name";
You can add a method _remap() in your controller
https://ellislab.com/codeigniter/user-guide/general/controllers.html#remapping
Put the following code inside your controller
/**
* Intercept all calls to this class.
*
* #access private
* #param string
* #param array
* #return boolean
*/
function _remap($method, $params)
{
// If method exists, call that method.
if (method_exists($this, $method) !== false) return call_user_func_array(array($this, $method), $params);
// If method is actually an existing permalink, show permalink's content
if ($this->SomeModel->exists(array('permalink' => $method))) return $this->view($method);
// Non-existing method
show_404();
}
Does Phalcon support content negotiation out-of-the-box or is there some easy-to-implement solution? I'm scouring the 'nets and not seeing it.
Thanks!
Short answer is no and thank god for that, or we'd have another 100 bugs for a non-major component :)
You can easily plug an existing library, like Negotiation, into DI and use it later globally throughout the app.
$di->setShared('negotiator', function(){
return new \Negotiation\Negotiator();
});
$bestHeader = $di->getShared('negotiator')->getBest('en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2');
Keep in mind that with the default server config (.htaccess / Nginx) from examples static files will be served as is, without interception by Phalcon. So, to server files from the server it would be best to create a separate controller / action to handle that rather than making all request go through your app.
Edit:
If it's simply about enabling your app sending either xml or json based on the common distinction (header, param, method), then you can easily accomplish it without external frameworks. There are many strategies, the simplest would be to intercept Dispatcher::dispatch(), decide in there what content to return and configure the view and response accordingly – Phalcon will do the rest.
/**
* All controllers must extend the base class and actions must set result to `$this->responseContent` property,
* that value will be later converted to the appropriate form.
*/
abstract class AbstractController extends \Phalcon\Mvc\Controller
{
/**
* Response content in a common format that can be converted to either json or xml.
*
* #var array
*/
public $responseContent;
}
/**
* New dispatcher checks if the last dispatched controller has `$responseContent` property it will convert it
* to the right format, disable the view and direcly return the result.
*/
class Dispatcher extends \Phalcon\Mvc\Dispatcher
{
/**
* #inheritdoc
*/
public function dispatch()
{
$result = parent::dispatch();
$headerAccept = $this->request->getHeader('Accept');
$headerContentType = $this->request->getHeader('Content-Type');
$lastController = $this->getLastController();
// If controller is an "alien" or the response content is not provided, just return the original result.
if (!$lastController instanceof AbstractController || !isset($lastController->responseContent)) {
return $result;
}
// Decide what content format has been requested and prepare the response.
if ($headerAccept === 'application/json' && $headerContentType === 'application/json') {
$response = json_encode($lastController->responseContent);
$contentType = 'application/json';
} else {
$response = your_xml_convertion_method_call($lastController->responseContent);
$contentType = 'application/xml';
}
// Disable the view – we are not rendering anything (unless you've already disabled it globally).
$view->disable();
// Prepare the actual response object.
$response = $lastController->response
->setContent($response)
->setContentType($contentType);
// The returned value must also be set explicitly.
$this->setReturnedValue($response);
return $result;
}
}
// In your configuration you must insert the right dispatcher into DI.
$di->setShared('dispatcher', function(){
return new \The\Above\Dispatcher();
});
Just thought that you can probably achieve the same using dispatch loop events. The solution in theory might look more elegant but I never attempted this, so you might want to try this yourself.
I'd like my API to handle calls of the such:
/teams/colors
/teams/1/colors
The first would return all colors of all teams, the second would return colors of team 1 only.
How would I write a route rule for this in Laravel?
This should be simple using a laravel route.
Route::pattern('teamid', '[0-9]+');
Route::get('/teams/{teamid}/colors', 'controller#method');
Route::get('/teams/colors', 'controller#method');
Using the pattern, it lets you specify that a route variable must match a specific pattern. This would be possible without the pattern also.
I noticed you mentioned REST in the title. Note that my response is not using Laravel's restful routes system, but its normal routes system, but I'm sure this could be adapted to be restul, or work with the restful system.
Hope this helps.
Edit:
After a bit of looking around, you may be able to use this if you are using Route::resource or Route::controller.
Route::resource('teams', 'TeamsController');
Route::any('teams/{teamid}/colors', 'TeamsController#Method');
// Or to use a different route for post, get and so on.
Route::get('teams/{teamid}/colors', 'TeamsController#getMethod');
Route::post('teams/{teamid}/colors', 'TeamsController#postMethod');
Note: the resource word above can be replaced with ::controller.
*Note 2: I have not tested this and am unable to guarantee it would work, but it does seem possible.*
You may try something like this:
class TeamsController extends BaseController {
// GET : http://example.com/teams
public function getIndex()
{
dd('Colors of all teams');
}
// GET : http://example.com/teams/1/colors
public function getColorsById($id)
{
dd("Colors of team $id");
}
// This method will call the "getColorsById" method
public function missingMethod($parameter = array())
{
if(count($parameter) == 2) {
return call_user_func_array(array($this, 'getColorsById'), $parameter);
}
// You may throw not found exception
}
}
Declare a single route for both methods:
Route::controller('/teams', 'TeamsController');