I recently migrate a huge project from Apiplatform 2.6 to 3.0. I'm converting my controllers into providers and processors but i encounter some problems.
I try to block an action to my users on a showcase website. My endpoint look like that (with id = showcase website id and blockOrder = boolean) :
#[ApiResource(
operations: [
new Post(
uriTemplate: '/shop/{id}/all/block-order/{blockOrder}',
status: 200,
processor: UpdateBlockOrderToAllParticipantsProcessor::class,
read: false,
deserialize: false,
serialize: false
),
],
normalizationContext: ['groups' => ['shop:read:bo', 'shop:dashboard:bo', 'shop:read:bo', 'lifecycle']],
denormalizationContext: ['groups' => ['shop:write:bo']],
provider: ShopProvider::class
)]
My problem comes from my value "$data" in my processor. I think that I get the wrong info in my endpoint and then when I call it in postman it throw me this error :
public function process(mixed $data, ApiOperation $operation, array $uriVariables = [], array $context = [])
{
assert($data instanceof Shop);
return $response;
}
Error :
{
"#context": "/bo/contexts/Error",
"#type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "Could not resolve argument $data of \"api_platform.action.placeholder::__invoke()\", maybe you forgot to register the controller as a service or missed tagging it with the \"controller.service_arguments\"?",
...
}
How can I get my shop data without this error ?
Related
I am trying to use an optional parameter in one of my symfony 6 controllers but it just does not work.
I have a controller with a method/route to create a new item and one to edit an existing item. The route for the "new" method does not accept an item parameter but the one for the "edit" method does. As such:
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new( Request $request ): Response ...
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
public function edit( Request $request, Item $item ): Response ...
Since both of the methods have a large amount of the same code in them, I decided to create a new "new_edit" method and route that is then called from the "new" and "edit" methods via "directToRoute".
#[Route('/new_edit/{id}', name: 'new_edit', defaults: ['id' => null], methods: ['GET', 'POST'])]
public function new_edit( Request $request, ?Item $item ) : Response ...
So far so good. When I select an existing item and elect to edit it, all works perfectly. However, when I elect to create a new item I constantly get and error message "Item object not found by the #ParamConverter annotation".
I have tried setting default values, etc. but I am just not able to get this working.
Does anyone have a solution to such a problem?
Many thanks,
Kristian.
It seems I have found the issue and it was not related directly to the optional parameter but to the priority in which the routes are evaluated.
I modified my route priorities and now all works as expected with the following configuration:
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(): Response
{
return $this->redirectToRoute( 'item.new_edit', [], Response::HTTP_SEE_OTHER );
}
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
public function edit( Item $item ): Response
{
return $this->redirectToRoute( 'item.new_edit', ['id' => $item->getId()], Response::HTTP_SEE_OTHER );
}
#[Route('/new_edit/{id}', name: 'new_edit', defaults: ['id' => null], methods: ['GET', 'POST'])]
public function new_edit( Request $request, ?Item $Item ) : Response
{
...
}
I hope that helps anyone who has the same issue.
Kristian
You can use several routes for one action. For the optional param you can define a default value for the PHP parameter ?Item $Item = null
#[
Route('/new', name: 'new', methods: ['GET', 'POST']),
Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])
]
public function new_edit(Request $request, ?Item $item = null) : Response
{
}
i need help with the api platform pagination in json format.
here is my api_platform.yaml
api_platform:
allow_plain_identifiers: true
mapping:
paths: ['%kernel.project_dir%/src/Entity']
formats:
json: ['application/json']
when i used the format hydra (default) i got something like this
"hydra:view": {
"#id": "/api/galleries?page=1",
"#type": "hydra:PartialCollectionView",
"hydra:first": "/api/galleries?page=1",
"hydra:last": "/api/galleries?page=6",
"hydra:next": "/api/galleries?page=2"
}
can anyone help me? if it's possible to get something like that in json format or another way to aply pagination with api platform or symfony
thank you
You shoud create an event Subscriber :
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
final class PaginateJsonSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['normalizePagination', EventPriorities::PRE_RESPOND],
];
}
public function normalizePagination(
ViewEvent $event
): void {
$method = $event->getRequest()->getMethod();
if ($method !== Request::METHOD_GET) {
return;
}
if (($data = $event->getRequest()->attributes->get('data')) && $data instanceof Paginator) {
$json = json_decode($event->getControllerResult(), true);
$pagination = [
'first' => 1,
'current' => $data->getCurrentPage(),
'last' => $data->getLastPage(),
'previous' => $data->getCurrentPage() - 1 <= 0 ? 1 : $data->getCurrentPage() - 1,
'next' => $data->getCurrentPage() + 1 > $data->getLastPage() ? $data->getLastPage() : $data->getCurrentPage() + 1,
'totalItems' => $data->getTotalItems(),
'parPage' => count($data)
];
$res = [
"data" => $json,
"pagination" => $pagination
];
$event->setControllerResult(json_encode($res));
}
}
}
In API Platform we have the jsonapi format which provides us the support for pagination. You can set the default config for number of records per page in api platform as:
# api/config/packages/api_platform.yaml
api_platform:
defaults:
pagination_items_per_page: 30 # Default value
formats:
jsonapi: ['application/vnd.api+json']
With this configuration you would require to either have an item and collection normalizer to customise the structure of the response with pagination metadata OR you can use state providers for this purpose (check here)
You would still get all the necessary pagination metadata if you just leave your default format as jsonapi
This is the very good resource to read more about API Platform pagination
The situation:
I need to send the request to the api to update the account info.
The API docs say I need to do send a PUT request to the API.
I trying to do this in Laravel 5.6, although I don't think this matters.
What I have so far:
A working constructor for the Guzzle client;
A working function to retrieve account info.
What is not working:
Upon submitting the request I get a Guzzle exception
Client error: \`PUT https://sandbox.proapi.itemize.com/api/enterprise/v1/accounts/<my account id>\` resulted in a \`400 Bad Request\` response: IOException:
This is the code I have so far:
<?php
namespace App\Http\Controllers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
class ApiController extends Controller {
private $apiKey;
private $uri;
private $client;
public function __construct() {
$this->apiKey = 'my api key';
$this->uri = 'https://sandbox.proapi.itemize.com/api/enterprise/v1/accounts/my account id';
$this->client = new Client([
'base_uri' => $this->uri,
'auth' => [null, $this->apiKey]]);
}
public function accountInfo() {
$response = $this->client->request('GET','');
echo $response->getBody()->getContents();
}
public function updateAccountInfo() {
$response = $this->client->request('PUT','',[
'headers' => [
'Content-Type' => 'application/json',
],
'body' => '{"markets":"UK"}'
]);
echo $response->getBody()->getContents();
}
}
?>
400 Bad Request means: request sent by the client due to invalid syntax.
According itemize api documentation "markets" should be passed as an array of strings. And you can also use json format.
Try this:
public function updateAccountInfo() {
$response = $this->client->request('PUT', '', [
'json' => ["markets" => ["UK"]],
]);
echo $response->getBody()->getContents();
}
I'm pretty new on unit testing and I want to try to test my login page
my Goal for this unit are :
-> if it match in database -> redirect to route '/'
-> if not -> redirect to route '/login'
<?php
namespace Tests\Feature;
use App\Domain\Core\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Session;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class userTest extends TestCase
{
use DatabaseMigrations;
/**
* A basic test example.
*
* #return void
*/
public function testLoginTrue()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'user'
];
$this->post('login',$credential)->assertRedirect('/');
}
public function testLoginFalse()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'usera'
];
$this->post('login',$credential)->assertRedirect('/login');
}
}
when I test on TestLoginTrue , its successfully return to '/' But when i try the TestLoginFalse ... it return same like TestLoginTrue, it should be stayed on '/login' route,
Any Idea?
Plus I want to try to check if when I already login I couldn't access the login page so my initial idea is :
public function testLoginTrue()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'user'
];
$this->post('login',$credential)
->assertRedirect('/')
->get('/login')
->assertRedirect('/');
}
but... it returns
1) Tests\Feature\userTest::testLoginTrue BadMethodCallException:
Method [get] does not exist on Redirect.
So how to do it correctly?
Thanks in advance
I am also a bit stuck with Laravel 5.4 testing follow redirects case.
As a workaround, you may check $response->assertSessionHasErrors(). This way it should work:
public function testLoginFalse()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'incorrectpass'
];
$response = $this->post('login',$credential);
$response->assertSessionHasErrors();
}
Also, in testLoginTrue() you may check, that session missing errors:
$response = $this->post('login',$credential);
$response->assertSessionMissing('errors');
Hope this helps!
I have a ZF 3 application. Messages I explicitly log with e.g $log->debug() Show up just fine. Exceptions don't. Errors seem to show up because that's the default php config to go to stderr. Here is the relevant lines from modules.config.php:
'service_manager' => [
'factories' => [
. . . .
'log' => \Zend\Log\LoggerServiceFactory::class,
],
],
'log' => [
'writers' => [
[
'name' => 'stream',
'options' => [ 'stream' => 'php://stderr' ]
],
],
'errorHandler' => true,
'exceptionhandler' => true,
],
The lines in the source that lead me to believe this is the correct config.
if (isset($options['exceptionhandler']) && $options['exceptionhandler'] === true) {
static::registerExceptionHandler($this);
}
if (isset($options['errorhandler']) && $options['errorhandler'] === true) {
static::registerErrorHandler($this);
}
To test it I made the following endpouints:
public function errorAction()
{
$msg = $this->params()->fromQuery('msg', 'Default Error message');
trigger_error('Index Error Action' . $msg, E_USER_ERROR);
$model = new JsonErrorModel(['msg' => $msg]);
return $model;
}
public function exceptionAction()
{
$msg = $this->params()->fromQuery('msg', 'Default Error message');
throw new \RuntimeException('Index Exception Action' . $msg);
$model = new JsonErrorModel(['msg' => $msg]);
return $model;
}
You have typo in your configuration array
'log' => [
....
'errorHandler' => true,
....
],
This index shouldn't be camelCase it should be errorhandler (all letters are lowercase). I would also add fatal_error_shutdownfunction => true to configutation so you will log fatal errors.
Zend uses set_exception_handler to handle exceptions, so keep in mind that logging exceptions will work only if they are not in try/catch block.
Sets the default exception handler if an exception is not caught within a try/catch block
Source: http://php.net/manual/en/function.set-exception-handler.php
All these functions may be set manually:
\Zend\Log\Logger::registerErrorHandler($logger);
\Zend\Log\Logger::registerFatalErrorShutdownFunction($logger);
\Zend\Log\Logger::registerExceptionHandler($logger);
If you want test it, you can do following things:
Error
public function errorAction()
{
$log = $this->getServiceLocator()->get('log'); // init logger. You shouldn't use getServiceLocator() in controller. Recommended way is injecting through factory
array_merge([], 111);
}
It should write in log:
2017-03-09T15:33:47+01:00 WARN (4): array_merge(): Argument #2 is not an array {"errno":2,"file":"[...]\\module\\Application\\src\\Application\\Controller\\IndexController.php","line":80}
Fatal error
public function fatalErrorAction()
{
$log = $this->getServiceLocator()->get('log'); // init logger. You shouldn't use getServiceLocator() in controller. Recommended way is injecting through factory
$class = new ClassWhichDoesNotExist();
}
Log:
2017-03-09T15:43:06+01:00 ERR (3): Class 'Application\Controller\ClassWhichDoesNotExist' not found {"file":"[...]\\module\\Application\\src\\Application\\Controller\\IndexController.php","line":85}
Or you could initialize logger in Module.php file if you need logger globally.
I don't think it is possible to log exception in controller's action. I'm not sure, but action is dispatched in try/catch block.