I'm building an application with AngularJS that talks to a REST API developed in Laravel 4.
I've made a good start so far by having each request send the relevant Access-Control headers that allow Angular to talk to the API, and I've also built a UserController resource:
Route::resource('users', 'UserController');
However now I seem to be facing a wall. For the learning opportunities, I'd like to build this REST API properly using good standards, and I'm trying to work out how to do the responses.
I know I want to return JSON from the API and as far as I know I should wrap every response in a data field, and I also realize I can do that using something like this at the bottom of my controller actions:
return Response::json(array(
'data' => array(
'users' => array(
array(
'username' => 'Bob',
'email' => 'bob#gmail.com'
)
)
),
'status' => 200,
'success' => true
),
200
);
However, I don't want to repeat that kind of logic in every controller action in my application, so I'm wondering how I can make that code more DRY so that at the end of my controller I can just do something like this:
return array(
'users' => array(
array(
'username' => 'Bob',
'email' => 'bob#gmail.com'
)
)
);
and somewhere else in my application I can do the Response::json stuff. Any ideas?
So like you said, you want to return a JSON response, for example:
return Response::json(
array(
'status' => 'success',
'pages' => $modelData->toArray()
),
200
);
So, instead of repeating this everytime, you could just make a createResponse...() function, perhaps in a base controller class, that you can call in all your controllers. A response failed function could look something like:
function createResponseFailed() {
return Response::json([
'error' => [
'message' => 'Some failed message'
]
], 404);
}
That is just a simple example, but illustrates the point. Hope it helps!
Related
I have a Model called Type with a title field and a pretty_slug field.
I have a test that is checking that a user can not update a Type instance:
<?
public function test_user_cannot_put_update_page() {
$type = Type::factory()->make([
'title' => 'Original type',
]);
$type->save();
$response = $this->put(route('types.update', [
'pretty_slug' => $type->pretty_slug,
'title' => 'New type',
]));
$response->assertForbidden();
$this->assertDatabaseHas('types', [
'title' => 'Original type'
]);
}
If I do dd($response->getContent()); I can see that a redirect is happening:
Now the weird thing is that I have the exact same for another Model called Level:
<?
public function test_user_cannot_put_update_page() {
$level = Level::factory()->make([
'title' => 'Original level',
]);
$level->save();
$response = $this->put(route('levels.update', [
'pretty_slug' => $level->pretty_slug,
'title' => 'New level',
]));
$response->assertForbidden();
$this->assertDatabaseHas('levels', [
'title' => 'Original level'
]);
}
The Model Level is exactly the same as Type: the same Controller, the same Trait shared, same Policy, same Tests, same routes… I have other Models called Idea, Concept and Episode that have the exact same behavior.
All tests pass, except for my Type Model:
I have no idea why this particular types.update route is not working. It should return a 302 but is instead redirecting.
It is working when I use the webform: the Type instance updates correctly. But the test is failing.
How can I debug this test? Where do I look for an issue in my code?
Thanks for any help.
EDIT 1: added controller and routes
I think the problem is the 'levels.update' route might be protected by the auth middleware.
Since there is no user logged in, the auth middleware will attempt to redirect to the login page instead.
I like IGP's answer. But if that's not it, you might want to check your host configs. Apache or Nginx, or whatever you're using. It could be the route is being called via http and your server is redirecting to https, or visa versa.
Thanks to #Aless55, I found the issue: it was the validation of my Type model that was preventing me from updating the instance.
I looked into the StoreType file, in which I had:
'order' => 'required|numeric',
This means the order field is required. But when I tried calling the types.update route, I wasn't including that field.
One solution would have been to make that field optional. But I ended up including the order field in my test:
$response = $this->put(route('types.update', [
'pretty_slug' => $type->pretty_slug,
'title' => 'Alex new type',
'order' => 1,
]));
I am pulling order information from an external API and saving the entire JSON response into a documents table.
I obviously do not want to call this API when testing so I have setup a factory, but I am struggling with how to pull in these JSON stubs. For example:
// factory
$factory->define(App\OrderDocument::class, function (Faker $faker) {
return [
'document_id' => $faker->uuid,
'status' => $faker->randomElement(['open', 'partial', 'processed', 'updating']),
'document' => $faker->text
];
});
// Currently using it like...
foreach ($this->fullDocuments() as $id => $document){
$a = factory(\App\OrderDocument::class)->create([
'document_id' => $id,
'status' => 'full'
]);
$a->setRawAttributes(['document' => $document]);
$a->save();
}
where $this->fullDocuments() contains an array of the document_id and raw JSON response from the API.
I know a better way to do this is to factory the entire JSON document. The JSON contains about 500 lines so it would be very time consuming but also I do not own this data, so I assume I should not be trying to fake it.
Within my test, I would prefer to do something like the below, but am not sure how.
factory(OrderDocument::class, 10)->create([
'document_id' => $this->getDocumentId($i++),
'document' => $this->getDocumentStub($i++),
]);
I have this nested relation im abit unsure how i assertJson the response within the phpunit test.
FilmController
public function show(string $id)
{
$film = Film::with([
'account.user:id,account_id,location_id,name',
'account.user.location:id,city'
])->findOrFail($id);
}
FilmControllerTest
public function getFilmTest()
{
$film = factory(Film::class)->create();
$response = $this->json('GET', '/film/' . $film->id)
->assertStatus(200);
$response
->assertExactJson([
'id' => $film->id,
'description' => $film->description,
'account' => $film->account->toArray(),
'account.user' => $film->account->user->toArray(),
'account.user.location' => $film->account->user->location->toArray()
]);
}
Obviously this isnt working because its returning every column for the user im a little unfamiliar with how you test nested relations with the code you need so im unsure with a toArray can anyone help out?
Testing is a place where you throw DRY (don't repeat yourself) out and replace it with hard coded solutions. Why? simply, you want the test to always produce the same results and not be bound up on model logic, clever methods or similar. Read this amazing article.
Simply hard code the structure you expect to see. If you changed anything in your model to array approach, the test would still pass even thou your name was not in the response. Because you use the same approach for transformation as testing. I have tested a lot of Laravel apps by now and this is the approach i prefers.
$account = $film->account;
$user = $account->user;
$location = $user->location;
$response->assertExactJson([
'description' => $film->description,
'account' => [
'name' => $account->name,
'user' => [
'name' => $user->name,
'location' => [
'city' => $location->city,
],
],
],
]);
Don't test id's the database will handle those and is kinda redundant to test. If you want to check these things i would rather go with assertJsonStructure(), which does not assert the data but checks the JSON keys are properly set. I think it is fair to include both, just always check the JSON structure first as it would likely be the easiest to pass.
$response->assertJsonStructure([
'id',
'description',
'account' => [
'id',
'name',
'user' => [
'id',
'name',
'location' => [
'id',
'city',
],
],
],
]);
I've two working project communicating between each other via an API built in a Laravel. So far there is only simple POST requests made with GuzzleHttp 6.
And I am currently trying to have a new POST request made from 1 to 2, which would send a couple of simple fields along with one file.
Project 1 has a form, on the form submit I handle the data and want to send them to project 2 via a POST request to this new API endpoint.
I've tried different guzzle options 'multipart', 'form_data' etc and realised they may not be combined together. Now I understood that this options are exclusive and using only "multipart" seems the way to go.
But when I send my request to Laravel no data nor file are there.
Here is the code for my request
$options = [
'multipart' =>
[
[
'name' => 'data',
'contents' => '{"field_1":"Test","field_2":"Test","field_3":"Test"}',
'headers' =>
[
'Content-Type' => 'application/json',
],
],
[
'name' => 'file',
'filename' => 'test.pdf',
'Mime-Type' => 'application/pdf',
'contents' => file_get_contents($_FILEs['text_file']['temp_name']),
]
]
];
$this->client->request('POST', "api/test_post", $options)
I also gve this a try:
$options = [
'multipart' =>
[
[
'name' => 'field_1',
'contents' => 'Test',
],
[
'name' => 'field_2',
'contents' => 'Test',
],
[
'name' => 'file',
'filename' => 'test.pdf',
'Mime-Type' => 'application/pdf',
'contents' => fopen($_FILEs['text_file']['temp_name'],'r'),
]
]
];
$this->client->request('POST', "api/test_post", $options)
If I look the request content on the receiving end, nothing is there. No field or file.
I've seen couples posts, some say to include headers some say not too. I kinda got lost and amd now running out of ideas.
I would expecet the infos to be as if they where form post I guess:
$request->inpust('field_1') -> 'test'
$request->inpust('field_2') -> 'test'
$request->inpust('field_3') -> 'test'
$request->file('file') -> my uploaded file
Also I should point out that I am not exactly sure how multipart/form-data works, so that might not help me.
If you can point me to the right direction, that would help a lot
Well I finally figured it out. The second example from above is the way to go also be sure to check the headers of the request and the client...
As this API has been running for quite some time and was only doing json type requests, the Client was instantiated with
$options = [
headers => [ 'Content-Type' => 'application/json']
]
Which, as stated in multiple answers across the internet, prevents Guzzle to automatically set the Content-Type depending of the request options.
In my case, removing this line made Guzzle enable to set it properly when provided with 'multipart' option.
Also, as all other requests are using the 'json' options, Guzzle also works it's magic and set 'Content-Type' => 'application/json' as well.
I am using Silex 2.0 (I know - it's development version and not fully released yet) along with CNAM's JWT security provider (see: https://github.com/cnam/security-jwt-service-provider) to write an API for an open source application I am writing.
In short, there are three types of users that I care about:
Sitewide admins (ROLE_ADMIN) that have complete access
Commissioners (ROLE_COMMISH) who create objects they own, and can edit their own objects
Anonymous users who access read-only information.
As such, there are three sections of routes that go along with these "roles":
/admin/* where administrators can perform their uber actions
/commish/* where commissioners or admins can perform their actions on their objects
/* where all users can read information
The issue that I've come across is that while I can setup 3 firewalls, one for each, there are times in the 3rd route category (GET /object/1 for instance) where it needs to be accessibly anonymously, but if the user provides a valid JWT token, I need to access that user in order to perform some additional logic on the data I hand back in the response.
As I have it setup currently (more on my config below), it's all-or-nothing: I either restrict an entire firewall to only authenticated users with a certain role, or I open it up to anonymous users (and therefore cannot view user information).
Is it possible to have a route that anyone can hit, but logged in users can also be seen?
Current security configuration:
$app['users'] = function () use ($app) {
return new UserProvider($app);
};
$app['security.jwt'] = [
'secret_key' => AUTH_KEY,
'life_time' => 86400,
'algorithm' => ['HS256'],
'options' => [
'header_name' => 'X-Access-Token'
]
];
$app['security.firewalls'] = array(
'login' => [
'pattern' => 'login|register|verify|lostPassword|resetPassword',
'anonymous' => true,
],
'admin' => array(
'pattern' => '^/admin',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
),
'commish' => array(
'pattern' => '^/commish',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
)
);
$app['security.role_hierarchy'] = array(
'ROLE_ADMIN' => array('ROLE_MANAGER'),
);
$app->register(new Silex\Provider\SecurityServiceProvider());
$app->register(new Silex\Provider\SecurityJWTServiceProvider());
Additionally, I've attempted another approach where I match all routes under a single firewall, but then protect certain ones by using securty.access_rules configuration, but it does not work. An example of what I've tried:
$app['security.firewalls'] = array(
'api' => array(
'pattern' => '^/',
'logout' => array('logout_path' => '/logout'),
'anonymous' => true,
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true
)
)
);
$app['security.access_rules'] = array(
array('^/admin', 'ROLE_ADMIN'),
array('^/commish', 'ROLE_MANAGER'),
array('^/', 'IS_AUTHENTICATED_ANONYMOUSLY')
);
You can use $app['security.jwt.encoder'] to decode jwt and either create a custom trait and extending the route object or using midddlewareeeither on the route level or an easier way would be to use a middleware on the application level. I had similar issue and this is how i solved it, something like below
ex.
$app->before(function (Request $request, Application $app) {
$request->decodedJWT = $app['security.jwt.encoder']->
decode($request->headers->get('X-Access-Token'));
});
and then you can access the decoded jwt form any route by doing this
$app->get('/object/1', function(Request $request) {
$decodedJWT = $request->decodedJWT;
// do whatever logic you need here
})
So: so far I have not found this to be possible through the "normal" way, which is disappointing. I will not mark what I detail below as the "answer" for a few days, hoping that someone can chime in and offer a better, more "official" way to solve the dilemma.
TL;DR: I manually check the request headers for the access token string, then decode the token using the JWT classes in order to load the user account in routes outside of the firewall. It's incredibly hacky, it feels downright dirty, but it's the only solution to the issue that I see at the moment.
Technical Details: First, you must acquire the token value from the request header. Your controller method will have been handed a Symfony\Component\HttpFoundation\Request object, from which you can access $request->headers->get('X-Access-Token'). In most instances the user will not be authenticated, so this will be empty, and you can return null.
If not empty, you must then use Silex's instance of JWTEncoder to decode the token contents, create a new token instance of JWTToken, set the context to the decoded value from the encoder, and finally you can access the username property from said token - which can then be used to grab the corresponding user record. An example of what I came up with:
$request_token = $request->headers->get('X-Access-Token','');
if(empty($request_token)) {
return null;
}
try {
$decoded = $app['security.jwt.encoder']->decode($request_token);
$token = new \Silex\Component\Security\Http\Token\JWTToken();
$token->setTokenContext($decoded);
$userName = $token->getTokenContext()->name;
//Here, you'd use whatever "load by username" function you have at your disposal
}catch(\Exception $ex) {
return null;
}
And obviously, any code calling this function would need to know that because the request is outside of the firewall, there is zero guarantee that a user will be returned (hence the hacky try-catch that silences exceptions by just returning null).
Edit: I've updated the code here to use Silex's built-in DI container (provided by Pimple) so there's no need to create a new instance of the JWT encoder by hand. I'm also marking #user5117342 's answer as the correct one, as using some sort of Silex middleware approach is far more robust.
Edit (April 2016): Using the updated cnam/security-jwt-service 2.1.0 along with symfony/security 2.8, there's a slight update that makes the code above a little simpler:
$request_token = $request->headers->get('X-Access-Token','');
if(empty($request_token)) {
return null;
}
try {
$decodedToken = $app['security.jwt.encoder']->decode($request_token);
$userName = $decodedToken->name;
//Here, you'd use whatever "load by username" function you have at your disposal
}catch(\Exception $ex) {
return null;
}
The issue with the newer dependencies is that the JWTToken constructor requires 3 parameters which are difficult to obtain in most service layers, not to mention is quite out of place. As I was updating my Composer dependencies, I ended up finding out that I didn't actually need to create a JWTToken in order to get the username I needed.
Of course, it's to be noted I'm only using this method on public (anonymous) API routes to provide some niceties to users who are logged in - my app doesn't deal with sensitive data so I'm not overly concerned with this avenue outside of the firewalls. At worst a black hat user would end up seeing non-sensitive data that they normally wouldn't, but that's it. So YMMV.
Your are must be use regular expression e.g.
$app['security.firewalls'] = array(
'login' => [
'pattern' => 'login|register|oauth',
'anonymous' => true,
],
'secured' => array(
'pattern' => '^/api|/admin|/manager',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
),
);