I'm use L5-Swagger 5.7.* package (wrapper of Swagger-php) and tried describe Laravel REST API. So, my code like this:
/**
* #OA\Post(path="/subscribers",
* #OA\RequestBody(
* #OA\MediaType(
* mediaType="application/json",
* #OA\Schema(
* type="object",
* #OA\Property(property="email", type="string")
* )
* )
* ),
* #OA\Response(response=201,description="Successful created"),
* #OA\Response(response=422, description="Error: Unprocessable Entity")
* )
*/
public function publicStore(SaveSubscriber $request)
{
$subscriber = Subscriber::create($request->all());
return new SubscriberResource($subscriber);
}
But when I try send request via swagger panel I get code:
curl -X POST "https://examile.com/api/subscribers" -H "accept: */*" -H "Content-Type: application/json" -H "X-CSRF-TOKEN: " -d "{\"email\":\"bademail\"}"
As you can see, accept is not application/json and Laravel doesn't identify this as an AJAX request. So, when I send wrong data and expect to get 422 with errors in real I get 200 code with errors in "session". Request (XHR) through the swagger panel is also processed incorrectly, CURL code just for clarity.
Also, I found that in the previous version was used something like:
* #SWG\Post(
* ...
* consumes={"multipart/form-data"},
* produces={"text/plain, application/json"},
* ...)
But now it's already out of date.
So, how get 422 code without redirect if validation fails? Or maybe add 'XMLHttpRequest' header? What is the best thing to do here?
The response(s) didn't specify a mimetype.
#OA\Response(response=201, description="Successful created"),
If you specify a json response, swagger-ui will send an Accept: application/json header.
PS. Because json is so common swagger-php has a #OA\JsonContent shorthand, this works for the response:
#OA\Response(response=201, description="Successful created", #OA\JsonContent()),
and the requestbody:
#OA\RequestBody(
#OA\JsonContent(
type="object",
#OA\Property(property="email", type="string")
)
),
you can use this, i use Request class,
on the file Request
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
public function rules()
{
return [
'currentPassword' => 'required',
'newPassword' => 'required|same:confirmPassword',
'confirmPassword' => 'required'
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'errors' => $validator->errors(),
'status' => true
], 422));
}
Related
I have an endpoint, that allows file upload, everything works fine.
Next thing is to cover the endpoint with proper functional test.
And here's the problem - I can't pass the file to the client making the request.
My test class extends \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase.
static::createClient() method creates an instance of ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client and these Client does not support file uploads.
Beacuse of implementing the Symfony\Contracts\HttpClient\HttpClientInterface which defines public function request(string $method, string $url, array $options = []): ResponseInterface; there's no place for passing files argument.
The allowed options in Client does not support files array.
Internaly it looks like this:
ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client::request passes to the internal kernelBrowser an empty array in place of files params (2nd array): $this->kernelBrowser->request($method, $resolvedUrl, [], [], $server, $options['body'] ?? null)
How do you test endpoints with file upload by extending Base class for functional API tests which is ApiTestCase?
Here's some code, to help you visualize the problem:
ApiResource definition in entity:
/**
* #ApiResource(
* collectionOperations={
* "file_upload"={
* "method"="post",
* "controller"=FileUpload::class,
* "path"="/api/file-upload-endpoint",
* "deserialize"=false,
* "openapi_context"={
* "requestBody"={
* "content"={
* "multipart/form-data"={
* "schema"={
* "type"="object",
* "properties"={
* "file"={
* "type"="string",
* "format"="binary"
* }
* }
* }
* }
* }
* }
* }
* },
* },
* )
*/
Test class (don't mind the instance of UploadedFile, it's just there, to show you, that it cannot be passed anywhere):
<?php
declare(strict_types=1);
namespace App\Tests\Api;
use \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
final class FileUploadTest extends ApiTestCase
{
public function testFileUploadSuccessfully():void
{
$file = new UploadedFile(
TESTS_PROJECT_DIR.'/tests/files/Small_sample_of_jet.jpg',
'Small_sample_of_jet.jpg',
'image/jpeg',
);
static::createClient()->request(
'POST',
'/api/file-upload-endpoint',
[
'headers' => [
'Content-Type' => 'multipart/form-data',
],
],
);
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
}
}
And here is what i'm looking for:
<?php
declare(strict_types=1);
namespace App\Tests\Api;
use \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
final class FileUploadTest extends ApiTestCase
{
public function testFileUploadSuccessfully():void
{
$file = new UploadedFile(
TESTS_PROJECT_DIR.'/tests/files/Small_sample_of_jet.jpg',
'Small_sample_of_jet.jpg',
'image/jpeg',
);
static::createClient()->request(
'POST',
'/api/file-upload-endpoint',
[
'headers' => [
'Content-Type' => 'multipart/form-data',
],
],
[
'file'=>$file
]
);
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
}
}
When modyfing the vendor itself and passing the files to the Client::request and then to the kernelBrowser in place of 2nd empty array, everything works fine (I'm aware of breaking the contract, that's not the issue here ;)).
I'm thinking if there's missing feature of uploading files in ApiTestCase or I just can't find the solution.
Pls halp!
Api Platform version: 2.5.6
PS: I know i can use different client - test.client
$client = static::$kernel->getContainer()->get('test.client');
which is an instance of Symfony\Bundle\FrameworkBundle\KernelBrowser, the same that is used internally by the Api Platform's Client and that supports files array, but that's not the point of my question. I'd like to know how to do file upload with ApiTestCase.
Since the current latest release of api-platform/core (2.5.8) we are able to pass more parameters to kernelBrowser->request via the extra key. This also now includes files!
Here is a very basic example of testing an image upload (implemented based on the official API Platform documentation):
$file = new UploadedFile(
'path/to/images/my_image.png',
'my_image.png',
'image/png',
);
$response = static::createClient()->request('POST', '/upload_image',
[
'headers' => ['Content-Type' => 'multipart/form-data'],
'extra' => [
'files' => [
'file' => $file,
],
],
],
);
I have a symfony 2 backend and I have installed it on a server. My frontend is an ionic PWA, so it runs in browser and it is also installed on that server, but with an other subdomain. When I send a request from the web app to the backend, I'm getting this error:
OPTIONS https://api.example.com/api/login.json
XMLHttpRequest cannot load https://api.example.com/api/login.json. Response
for preflight has invalid HTTP status code 405
This is my code for the login action:
/**
* Login via username and password or via API key
*
* #Doc\ApiDoc(
* section="Security",
* description="Login",
* views={"default", "security"}
* )
*
* #param ParamFetcher $params
*
* #Rest\RequestParam(name="username", nullable=true, description="Username")
* #Rest\RequestParam(name="password", nullable=true, description="Password")
* #Rest\RequestParam(name="apikey", nullable=true, description="API key (alternative to username + password)")
*
* #Rest\Post("/login", name="api_security_login", options={"method_prefix" = false})
*
* #return Response
*/
public function loginAction(ParamFetcher $params)
{
//...do some stuff here...//
$data = array(
'user' => $userValue,
'apikey' => $user->getApiKey(),
);
$groups = array('default', 'private');
return $this->createAPIResponse(self::STATUS_OK, $data, $groups);
}
This is header from the response:
Access-Control-Allow-Methods:GET,POST,OPTIONS,DELETE,PUT
Access-Control-Allow-Origin:*
Allow:POST
Cache-Control:no-cache
Connection:Keep-Alive
Content-Length:54
Content-Type:application/json
Date:Tue, 29 Aug 2017 08:33:26 GMT
Keep-Alive:timeout=5, max=100
Server:Apache
This is the error message in the prod.log file:
request.ERROR: Uncaught PHP Exception
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException:
"No route found for "OPTIONS /api/login.json": Method Not Allowed
(Allow: POST)" at
/var/www/example.com/api/htdocs/symfony/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php
line 163 {"exception":"[object]
(Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException(code:
0): No route found for \"OPTIONS /api/login.json\": Method Not Allowed
(Allow: POST) at
/var/www/example.com/api/htdocs/symfony/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php:163,
Symfony\Component\Routing\Exception\MethodNotAllowedException(code:
0): at
/var/www/example.com/api/htdocs/symfony/app/cache/prod/appProdUrlMatcher.php:855)"}
[]
So it seems like the OPTIONS request, which is send because CORS, is not allowed, because of the "Allow" header, where only "POST" is allowed. So what is the best way to fix that for all routes?
You only have a route on your action for the POST method:
#Rest\Post("/login", name="api_security_login", options={"method_prefix" = false})
If you want to answer OPTIONS requests, you have to define an Options route using the #Rest\Options annotation.
You can either have several annotations on the same action and test the method inside the action (typically the case with GET and POST for a form action), or define two distinct actions with the same path but different methods.
http://symfony.com/doc/master/bundles/FOSRestBundle/7-manual-route-definition.html
There is no route that accepts the options http method.
No route found for "OPTIONS /api/login.json"
You need to define it first:
/**
* #Rest\Route(
* "/login",
* name="api_security_login",
* methods = {
* Request::METHOD_POST,
* Request::METHOD_OPTIONS,
* }
* )
*/
Clear your cache afterwards and verify that the route is active i.e. using:
bin/console debug:router | grep -i api_security_login
For my rest api I use yii\rest\Controller
class TweetController extends Controller
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
'auth' => [$this, 'auth']
];
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application\json' => Response::FORMAT_JSON,
]
];
return $behaviors;
}
// not solved yet
public function auth($pass)
{}
/**
* #param int $count
*
* #return array
* #throws \yii\base\InvalidConfigException
*/
public function actionLastTweets($count = 10)
{
/**
* #var TweetLastfinder $tweetLastFinder
*/
$tweetLastFinder = Yii::$app->get('tweetlastfinder');
return $tweetLastFinder->findLastTweets($count);
}
also i use prettyUrls 'GET tweet/last-tweets/<count>' => 'tweet/last-tweets'
actionLastTweets return array which one convert into json.
Idea is made simply authentication. In docs is example how to implements IdentityInterface in model. But i don`t work with AR directly. As i understand. I need to properly write auth() method.
I dont get it, whith value should be returned from auth() when authentification is passed? And how it will be send by request? (i mean http://localhost/index.php/tweet/last-tweets/50 without authentication, how it change?)
For simplicity there is a string value $password = 'qwerty' and i want to check if parameter $pass equal $password - authentication passed
some kinda:
public function auth($pass)
{
$password = 'qwerty';
if ($pass == $password) {
authentication passed
} else {
authentication failed
}
}
The subject of the authentication is your Identity (aka user) that has to implements the funtion findIdentityByAccessToken
see
\yii\web\IdentityInterface
to try your API on command line you can use cUrl as follows ( the token: XXXXXXXX_accessTokenString_XXXXXX )
note, the ":" divides user from password in the basic autentication, the token is used as user
POST a JSON to MessageController (the pluralization is not an error)
curl --user "XXXXXXXX_accessTokenString_XXXXXX:" -H "Accept:application/json" -H "Content-Type:application/json" -XPOST "http://rest.my-domain.com/messages" -d '{"email": "me#example.com"}'
POST a JSON to UserController enabling xdebug
curl --user "XXXXXXXX_accessTokenString_XXXXXX:" --cookie 'XDEBUG_SESSION=1221221' -H "Accept:application/json" -H "Content-Type:application/json" -XPOST "http://rest.my-domain.com/messages" -d '{"email": "me#example.com"}'
GET a JSON to UserController enabling xdebug
curl --user "XXXXXXXX_accessTokenString_XXXXXX:" -H "Accept:application/json" -H "Content-Type:application/json" -XGET "http://rest.my-domain.com/messages/123"
see also the Yii2 guide to REST services
http://www.yiiframework.com/doc-2.0/guide-rest-quick-start.html
I`m trying to write some functional tests for a REST API, created using FOS Rest Bundle.
The problem is that when I use the Symfony\Component\BrowserKit, symfony throws me the following error:
{"message":"Unable to find template \"AccountBundle:Account:list.html.twig\". .. }
The code that I run is:
$client = static::createClient();
$client->request('GET','/account');
When I run the request from the browser, it works fine.
Here is the controller:
/**
* Get channel by ID
* #Secure(roles="ROLE_USER")
* #RestView()
* #ApiDoc(
* resource=true,
* description="Get channel by id",
* section="Channel",
* output="Channel"
* )
*/
public function getAction(Channel $channel)
{
return array('channel' => $channel);
}
So when in test scenario, instead of returning the JSON tries to load the template.
You should use the $server parameter of the $client-request() method to set the Accept header to application/json. FOSRestBundle has a listener that returns JSON only if the corresponding Accept header is received, otherwise it will search for the template corresponding to the controller.
$client->request('GET', '/account', array(), array(), array('HTTP_ACCEPT' => 'application/json'));
Okay so I have a following situation:
The system I am building is retrieving data from a REST api and saving that data into a database. What I am wondering is how could this be implemented and where would behaviour like this go in sense of Laravels structure (controller, model etc.)? Does Laravel have a built in mechanism to retrieve data from external sources?
Edit:
Buzz hasn't been updated for over a year, it's recomended to now use Guzzle, see Mohammed Safeer's answer.
I have used Buzz package in order to make API requests.
You can add this package by adding it to the require section in your composer.json file.
{
require: {
"kriswallsmith/buzz": "dev-master"
}
}
Then run composer update to get it installed.
Then in Laravel you can wrap it in a class (perhaps a repository-like class) that handles making API request and returning data for your app to use.
<?php namespace My\App\Service;
class SomeApi {
public function __construct($buzz)
{
$this->client = $buzz;
}
public function getAllWidgets()
{
$data = $this->client->get('http://api.example.com/all.json');
// Do things with data, etc etc
}
}
Note: This is pseudocode. You'll need to create a class that works for your needs, and do any fancy dependency injection or code architecture that you want/need.
As #Netbulae pointed out, a Repository might help you. The article he linked is a great place to start. The only difference between the article and what your code will do is that instead of using an Eloquent model to get your data from your database, you're making an API request and transforming the result into a set of arrays/objects that your application can use (Essentially, just the data storage is different, which is one of the benefits of bothering with a repository class in the first place).
We can use package Guzzle in Laravel, it is a PHP HTTP client to send HTTP requests.
You can install Guzzle through composer
composer require guzzlehttp/guzzle:~6.0
Or you can specify Guzzle as a dependency in your project's existing composer.json
{
"require": {
"guzzlehttp/guzzle": "~6.0"
}
}
Example code in laravel 5 using Guzzle as shown below,
use GuzzleHttp\Client;
class yourController extends Controller {
public function saveApiData()
{
$client = new Client();
$res = $client->request('POST', 'https://url_to_the_api', [
'form_params' => [
'client_id' => 'test_id',
'secret' => 'test_secret',
]
]);
$result= $res->getBody();
dd($result);
}
First you have to make routes in your app/routes.php
/*
API Routes
*/
Route::group(array('prefix' => 'api/v1', 'before' => 'auth.basic'), function()
{
Route::resource('pages', 'PagesController', array('only' => array('index', 'store', 'show', 'update', 'destroy')));
Route::resource('users', 'UsersController');
});
Note: If you are not required authentication for API call, you can remove 'before' => 'auth.basic'
Here you can access index, store, show, update and destroy methods from your PagesController.
And the request urls will be,
GET http://localhost/project/api/v1/pages // this will call index function
POST http://localhost/project/api/v1/pages // this will call store function
GET http://localhost/project/api/v1/pages/1 // this will call show method with 1 as arg
PUT http://localhost/project/api/v1/pages/1 // this will call update with 1 as arg
DELETE http://localhost/project/api/v1/pages/1 // this will call destroy with 1 as arg
The command line CURL request will be like this (here the username and password are admin) and assumes that you have .htaccess file to remove index.php from url,
curl --user admin:admin localhost/project/api/v1/pages
curl --user admin:admin -d 'title=sample&slug=abc' localhost/project/api/v1/pages
curl --user admin:admin localhost/project/api/v1/pages/2
curl -i -X PUT --user admin:admin -d 'title=Updated Title' localhost/project/api/v1/pages/2
curl -i -X DELETE --user admin:admin localhost/project/api/v1/pages/1
Next, you have two controllers named PagesController.php and UsersController.php in your app/controllers folder.
The PagesController.php,
<?php
class PagesController extends BaseController {
/**
* Display a listing of the resource.
*
* #return Response
* curl --user admin:admin localhost/project/api/v1/pages
*/
public function index() {
$pages = Page::all();;
return Response::json(array(
'status' => 'success',
'pages' => $pages->toArray()),
200
);
}
/**
* Store a newly created resource in storage.
*
* #return Response
* curl --user admin:admin -d 'title=sample&slug=abc' localhost/project/api/v1/pages
*/
public function store() {
// add some validation also
$input = Input::all();
$page = new Page;
if ( $input['title'] ) {
$page->title =$input['title'];
}
if ( $input['slug'] ) {
$page->slug =$input['slug'];
}
$page->save();
return Response::json(array(
'error' => false,
'pages' => $page->toArray()),
200
);
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
* curl --user admin:admin localhost/project/api/v1/pages/2
*/
public function show($id) {
$page = Page::where('id', $id)
->take(1)
->get();
return Response::json(array(
'status' => 'success',
'pages' => $page->toArray()),
200
);
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
* curl -i -X PUT --user admin:admin -d 'title=Updated Title' localhost/project/api/v1/pages/2
*/
public function update($id) {
$input = Input::all();
$page = Page::find($id);
if ( $input['title'] ) {
$page->title =$input['title'];
}
if ( $input['slug'] ) {
$page->slug =$input['slug'];
}
$page->save();
return Response::json(array(
'error' => false,
'message' => 'Page Updated'),
200
);
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
* curl -i -X DELETE --user admin:admin localhost/project/api/v1/pages/1
*/
public function destroy($id) {
$page = Page::find($id);
$page->delete();
return Response::json(array(
'error' => false,
'message' => 'Page Deleted'),
200
);
}
}
Then you have model named Page which will use table named pages.
<?php
class Page extends Eloquent {
}
You can use Laravel4 Generators to create these resources using php artisan generator command. Read here.
So using this route grouping you can use the same application to make API request and as a front-end.
You can choose what to use:
Guzzle
CURL
file_get_contents :
$json = json_decode(file_get_contents('http://host.com/api/v1/users/1'), true);
Referrer
Try looking into the external API's manuals. There you will find info on how to retrieve information.
Then the best plan is to build an Interface.
Check this out:
http://culttt.com/2013/07/08/creating-flexible-controllers-in-laravel-4-using-repositories/
It's up to you how you use php to solve this.