API Platform - Custom controller / action description annotations not working - api-platform.com

Really sorry if I've missed something here, but I have searched the issues and docs high and low, and I cannot find why this is not working. Perhaps I need another tutorial on using the internet ;)
Consider the following entity annotation, in the openapi_context section the body.description and responses.200.description have no effect at all, and its driving me slightly mad... You should know that I am using de/normalization contexts but that doesn't seem related so I have left it out.
/**
* User entity
*
* #API\ApiResource(
* collectionOperations={
* "authenticate"={
* "method"="POST",
* "status"=200,
* "path"="/authenticate",
* "controller"=UserLoginController::class,
* "openapi_context"={
* "responses"={"200"={"description"="API token and secret"}},
* "body"={"description"="Login details"},
* "summary"="User authentication",
* "description"="Provides auth tokens on success",
* },
* },
* }
* )
*
*/
class User implements UserInterface
{
...
}
The result (blue blocks as expected, red blocks not working):
I have spent way too much time on this issue as it is, I would really appreciate it if someone could help me put this to bed. I have checked/tried the following to no avail;
Creating Custom Operations and Controllers
Custom Symfony Action with API Platform bundle
[Question] Documentation API with Swagger #143
Changing Operations in the OpenAPI Documentation
Composer versions (relevant parts):
{
"require": {
"php": ">=7.2.5",
"api-platform/core": "^2.6",
"symfony/framework-bundle": "5.2.*"
}
}

After some experimenting, I found that the openapi_context annotation attribute does indeed seems to ignore the response documentation. It does however allow you to provide the request body description that you are missing:
#[ApiResource(
collectionOperations: [
'test' => [
'method' => 'POST',
'path' => '/test',
'openapi_context' => [
'summary' => 'The endpoint summary',
'description' => 'The endpoint description',
'requestBody' => [
'description' => 'The endpoint request body description', // This one
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-some.group'
],
],
],
],
],
]
],
)]
I'm using PHP 8.0.3 and API Platform 2.6.3 while writing this, changing the annotations to docblocks should result in the same outcome for you though.
In order to document the endpoint response specification however, I had to implement a custom OpenApiFactoryInterface:
<?php declare(strict_types = 1);
namespace App;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\Model\Operation;
use ApiPlatform\Core\OpenApi\Model\PathItem;
use ApiPlatform\Core\OpenApi\Model\RequestBody;
use ApiPlatform\Core\OpenApi\OpenApi;
use ArrayObject;
use UnexpectedValueException;
class MyResourceOpenApiFactory implements OpenApiFactoryInterface
{
private OpenApiFactoryInterface $openApiFactory;
public function __construct(OpenApiFactoryInterface $openApiFactory)
{
$this->openApiFactory = $openApiFactory;
}
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->openApiFactory)($context);
$components = $openApi->getComponents();
$schemas = $components->getSchemas();
if (null === $schemas) {
throw new UnexpectedValueException('Failed to obtain OpenApi schemas');
}
$pathItem = new PathItem(
'MyResource test endpoint',
'A test summary',
'A test description',
null,
null,
// Your custom post operation
new Operation(
'testMyResourceCollection', // the operation route name
[
'MyResource' // your resource name
],
[
// response specifications
'201' => [
'description' => 'test endpoint 201 response description',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-read', // your resource (read) schema
],
],
],
],
],
'A test endpoint summary',
'A test endpoint description',
null,
[],
new RequestBody(
'A test request body description',
new ArrayObject([
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-write', // your resource (write) schema
],
],
]),
),
),
);
$paths = $openApi->getPaths();
$paths->addPath('/my_resources/test', $pathItem);
return $openApi;
}
}
Have this OpenApiFactoryInterface implementation wired by the service container as a decorator to api_platform.openapi.factory:
App\MyResourceOpenApiFactory:
decorates: 'api_platform.openapi.factory'
autoconfigure: false
Change the references to the example MyResource name to a resource name of your choice (like User).
Sidenote:
This whole process of customizing OpenApi endpoint documentation in API Platform is currently quite convoluted in my opinion. Use the implementation I provided as a reference to your own implementation, as you most likely need to make a few adjustments to it in order to make it satisfy your specific use case.

Related

Paypal API Subscription Create returns 400 Bad Request response "name", though request is formed as in documentation

I try to implement Paypal subscription service according to: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
This is my first try.
In sandbox business account I have created two test subscriptions: monthly and yearly and configured application with their id's.
This is the method:
public function createSubscription($planSlug, $name, $email) {
return $this->makeRequest(
'POST',
'v1/billing/subscriptions',
[],
[
'plan_id' => $this->plans[$planSlug],
'subscriber' => [
'name' => [
'given_name' => $name,
],
'email_address' => $email
],
'application_context'=> [
'brand_name' => config('app.name'),
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'SUBSCRIBE_NOW',
'return_url' => route('subscribe.approval', ['plan' => $planSlug]),
'cancel_url'=> route('subscribe.cancelled')
],
],
[],
$isJsonRequest = true
);
}
However, when I make a call to API, to create a test subscription, I get weird response that 'name' parameter is formed incorrectly:
php artisan tinker
>>> $paypal = resolve(App\Services\PaypalService::class);
=> App\Services\PaypalService {#3413}
>>> $paypal->createSubscription('monthly', 'Test', 'test#test.com');
GuzzleHttp\Exception\ClientException with message 'Client error: `POST https://api-
m.sandbox.paypal.com/v1/billing/subscriptions` resulted in a `400 Bad Request` response:
{"name":"INVALID_REQUEST","message":"Request is not well-formed, syntactically incorrect, or
violates schema.","debug_id (truncated...)
This is strange, because in Paypal API doc (see above), the 'name' param is described exactly like that!
Do I miss something or it is Paypal API is acting funky?
try this :
try {
// your code here
} catch(\Throwable $th) {
if ($th instanceof ClientException) {
$r = $th->getResponse();
$responseBodyAsString = json_decode($r->getBody()->getContents());
dd($responseBodyAsString);
}
}
I've faced this too before and it was not easy for me to figure out how to show an explicit error message.
'return_url' => route('subscribe.approval', ['plan' => $planSlug]),
'cancel_url'=> route('subscribe.cancelled')
the problem is in this two url, may be you have changed the APP_URL in the .env
APP_URL=http://127.0.0.1:8000
put app url that and try

laravel custom validator messages translation from another location

How can I load translation from other location than resources/lang/*/validation.php to vendor/package/src/translation/*/validation.php?
I have created translation file on path vendor/package/src/translation/*/validation.php:
<?php
return [
'custom' => [
'search_text' => [
'string' => 'A nice message.',
'not_regex' => 'Regex failed.',
],
],
'attributes' => [
'search_text' => 'Search text',
],
];
I have booted my own Validator in service provider:
$this->app->validator->resolver( function( $translator, $data, $rules, $messages = array(), $customAttributes = array() ) {
return new MyValidator( $translator, $data, $rules, $messages, $customAttributes );
} );
and I have created ofc the validator class. But I have no idea how i can concate validator and translation from custom location to work. The output should be overloaded by the custom file if any intersection will appear in both files.
Thanks for help. :)
If your package contains translation files, you may use the loadTranslationsFrom method to inform Laravel how to load them, and should add the following to your service provider's boot method:
public function boot()
{
$this->loadTranslationsFrom(__DIR__.'/path/to/translations', 'name');
}
Package translations are referenced using the package::file.line syntax convention.
echo trans('name::file.line');

Yii2 PageCache invalidation

Is there any possibility to invalidate or delete PageCache for a particular action.
Consider this:
class SiteController extends Controller
{
public function behaviors()
{
return [
'pageCache' => [
'class' => PageCache::className(),
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
],
];
}
public function actionIndex( $id )
{
// action code
}
}
And now I want to remove/invalidate cache for
action en/site/index?id=1
Currently I am thinking to write some code in a console app but do not know how to achieve this.
EDIT1: I try to rebuild-invalidate cache manually for a specific action. The code can't relay on 'dependency' because it is almost impossible to implement for that action.
EDIT2: The task is to rebuild cache only for the specific action (page) leave other cache intact.
You can use TagDependency for more granular invalidation:
public function behaviors()
{
return [
'pageCache' => [
'class' => PageCache::className(),
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
'dependency' => new \yii\caching\TagDependency([
'tags' => [
Yii::$app->requestedRoute,
Yii::$app->request->get('id'),
],
]),
],
];
}
To invalidate cache:
TagDependency::invalidate(Yii::$app->cache, [
'site/index', // route of action
123, // ID of page
]);
If someone else needs ...
Yii2 does not provide a native function to invalidate the cache of a specific page, however there is the delete function of the cache component. It would however be necessary to know the generated key for the requested page but the function that generates this key is protected (calculateCacheKey ()). In this way, the best way would be to create your own class extending \yii\filters\PageCache.
'pageCache' => function () {
return new class extends \yii\filters\PageCache{
public function init(){
parent::init();
$this->except = ['index'];
$this->duration = Yii::$app->params['cacheTime'], // seconds;
$this->variations = [
Yii::$app->language,
Yii::$app->request->get('id'),
];
if(Yii::$app->request->get('IC') == 1)
Yii::$app->cache->delete($this->calculateCacheKey());
}
public function beforeCacheResponse(){
return Yii::$app->request->get('IC') != 1;
}
};
},
In the provided code, for simplicity, I am using an anonymous class (PHP 7).
Instead you can create your class as you wish and inform its path as the 'class' parameter, as seen in the configuration displayed in the question.
Note that I am using a simple logic to invalidate the cache, checking if there is a GET parameter IC == 1, you can use whatever logic you want.
If after invalidating the cache you do not want a new cache to be created, simply return false in beforeCacheResponse, it is from \yii\filters\PageCache.
You can invalidate the cache by using dependencies
'pageCache' => [
...
'dependency' => [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT COUNT(*) FROM post',
],
http://www.yiiframework.com/doc-2.0/yii-filters-pagecache.html#$dependency-detail
If I understand correctly you are trying to disable caching only for a specific action and according to the DOCS you can use the following options to explicitly identify which action IDs to apply the cache filter OR which action IDs it should not.
$except array List of action IDs that this filter should not apply to. yii\base\ActionFilter
$only array List of action IDs that this filter should apply to.
The following should work for you
return [
'pageCache' => [
'class' => PageCache::className(),
'except'=>['index']
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
],
];

use Guzzle 6 to request Ruckus public API, but get Argument 3 passed to GuzzleHttp\Client::request() must be of the type array, boolean given

I am using Guzzle 6 to retrieve data from Ruckus public API, but keeps getting the following error
Argument 3 passed to GuzzleHttp\Client::request() must be of the type
array, boolean given
I have googled and searched for similar questions. The only working around is the second answer in this post which is to downgrade guzzle version to 5. However, other team members doing other features are using Guzzle 6, so downgrading to version 5 could also be an issue for the team.
Since I am not using any packages like in that post, I don't think the Guzzle version could be the culprit here, so can anyone give me some idea what I have done wrong? Thanks.
By the way, I am using Laravel Command. The code in the handle function is as below:
use Illuminate\Console\Command;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Client;
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$client = new Client();
//to bypass local ssl certificate issuer
$client->setDefaultOption('verify', false);
$res = $client->request(
'POST',
$this->baseUrl . '/v4_0/session',
[
"headers" => [
"Content-Type" => "application/json;charset=UTF-8"
],
"json" => [
"username" => "admin",
"password" => "admin"
]
]
);
$headers = explode(';', $res->getHeader('Set-Cookie'));
return current($headers);
}
The Ruckus public API: http://docs.ruckuswireless.com/vscg-enterprise/vsz-e-public-api-reference-guide-3-5.html#header-overview. I am using version 4, but even I use version 5 I still get the same error.
At the end I logged an issue on the github account of Guzzle. It turns out that $client->setDefaultOption('verify', false); is no longer a supported function call. Instead it should be passed to the third params like below:
$res = $client->request(
'POST',
$this->baseUrl . '/v4_0/session',
[
"verify" => false,
"headers" => [
"Content-Type" => "application/json;charset=UTF-8"
],
"json" => [
"username" => "admin",
"password" => "admin"
]
]
);
Thank you for everyone who have given suggestions and tried to help.

How to send Log event from Laravel to Loggly?

I want to send Monolog logs from my Laravel 5.1 application to Loggly.com online log management service. From all possible environment, including local development.
I have found some outdated libs and complicated ways to do this. So I ended up with very simple solution. Actually, Laravel Monolog Handler already have Loggly Handler out of the box.
Add config info to config/services.php:
'loggly' => array(
'key' => 'ENTER_YOUR_LOGGLY_TOKEN_HERE',
'tag' => 'ProjectName_' .strtolower(env('APP_ENV')),
),
Than add Monolog handler in bootstrap/app.php, before $app is returned:
/*
|--------------------------------------------------------------------------
| Setup Loggly Handler
|--------------------------------------------------------------------------
*/
$app->configureMonologUsing(function($monolog) {
$handler = new \Monolog\Handler\LogglyHandler(config('services.loggly.key'),\Monolog\Logger::DEBUG);
$handler->setTag(config('services.loggly.tag'));
$monolog->pushHandler($handler);
});
Voila! You are getting your Monolog Logs in Loggly dashboard.
UPDATE: (thanks #thitami)
Based on laravel.com/docs/5.6/upgrade
The configureMonologUsing Method
If you were using the configureMonologUsing method to customize the Monolog instance for your application, you should now create a custom Log channel. For more information on how to create custom channels, check out the full logging documentation.
I was able to manage having Laravel's default local log behaviour, and pushing to Loggly in the same time, by tweaking mladen-janjetovic's code a bit. Tested on Laravel 5.3
config/services.php:
'loggly' => [
'key' => 'ENTER_YOUR_LOGGLY_TOKEN_HERE',
'tag' => 'ProjectName_' .strtolower(env('APP_ENV')),
],
bootstrap/app.php:
/*
|--------------------------------------------------------------------------
| Push to Loggly, and save locally.
|--------------------------------------------------------------------------
*/
$app->configureMonologUsing(function($monolog) use ($app) {
$log = $app->make(Illuminate\Log\Writer::class);
$logglyHandler = new \Monolog\Handler\LogglyHandler(config('services.loggly.key'));
$logglyHandler->setTag(config('services.loggly.tag'));
if (config('app.env') == 'production')
{
// Push to Loggly and save local if in production
$log->getMonolog()->pushHandler($logglyHandler);
$log->useFiles(storage_path('/logs/laravel.log'));
}
else
{
// Otherwise, save only locally
$log->useFiles(storage_path('/logs/laravel.log'));
}
});
Alternatively, you may use Monolog-Cascade to do this.
Monolog-Cascade is a Monolog extension that allows you to set up and configure multiple loggers and handlers from a single config file.
Here is a sample config file for Monolog-Cascade using Loggly. This would log to you stdOut and to Loggly:
---
handlers:
console:
class: Monolog\Handler\StreamHandler
level: DEBUG
stream: php://stdout
error_loggly_handler:
class: Monolog\Handler\LogglyHandler
level: ERROR
token: xxxx-xxxx-xxxxxxxx
tags: [cascade, waterfall]
loggers:
my_logger:
handlers: [console, error_loggly_handler]
If you're interested, here is a blog post on Cascade => https://medium.com/orchard-technology/enhancing-monolog-699efff1051d
[Disclaimer]: I am the main contributor of Monolog-Cascade.
Got mine working with little configuration with Laravel 8.
Just use the built-in monolog handler for Loggly.
Edit your app/config/logging.php
use Monolog\Handler\LogglyHandler;
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'loggly'],
'ignore_exceptions' => false,
],
'loggly' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => LogglyHandler::class,
'with' => [
'token' => env('LOGGLY_TOKEN'),
],
],
]
For more advanced logging (for my case I need to set the tag as it was missing in the built-in handler's constructor.
Copy the built-in handler where you can find it within vendor folder
(e.g: vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php) into your app folder of choice (e.g: app/Logging/CustomLogglyHandler.php).
Modify the constructor to set the tags, and you need to change some of the imports as we're on different namespaces.
// app/Logging/CustomLogglyHandler.php
namespace App\Logging;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\MissingExtensionException;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LogglyFormatter;
use function array_key_exists;
use CurlHandle;
use Monolog\Handler\Curl\Util as CurlUtil;
public function __construct(string $token, array|string $tag = [], $level = Logger::DEBUG, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler');
}
$this->token = $token;
if (is_array($tag)) {
$this->tag = $tag;
} else {
$this->tag = [$tag];
}
parent::__construct($level, $bubble);
}
// config/logging.php
'loggly' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => CustomLogglyHandler::class,
'with' => [
'token' => env('LOGGLY_TOKEN'),
'tag' => strtolower(env('APP_NAME', 'Laravel')) . '_' . strtolower(env('APP_ENV', 'production'))
],
],
To expand on Hassan's contribution (posting as an answer, as I still don't have enough reputation to post a comment).
If you have a need to use daily logs locally, you could use following code:
$logFile = 'laravel'.'.txt';
$log->useDailyFiles(storage_path().'/logs/'.$logFile);
Of course, logfile name is totally arbitrary. In this example, format will be as such:
laravel-YYYY-MM-DD.txt
Edit:
with an upgrade to 5.4 this line does not work anymore:
$log = $app->make(Illuminate\Log\Writer::class);
As a workaround, you can create Writer instance manually, injecting $monolog available from configureMonologUsing closure:
$log = new Illuminate\Log\Writer($monolog);

Resources