I'm working with Symfony 4.4 / API Platform, and I'm trying to return the response from the DataPersister or to set up its code.
In my DataPersister, I test if the Admin->isManager() is true, So Admin can never be deleted, So in this case I want to return a custom status code in my response 414, and a message "thisAdminIsManager"
AdminDataPersister:
final class AdminDataPersister implements ContextAwareDataPersisterInterface
{
/* #var EntityManagerInterface */
private $manager;
public function __construct(
EntityManagerInterface $manager
){
$this->manager = $manager;
}
public function supports($data, array $context = []): bool
{
return $data instanceof Admin;
}
public function persist($data, array $context = [])
{
$this->manager->persist($data);
$this->manager->flush();
}
public function remove($data, array $context = [])
{
/* #var Admin $data */
#The Manager can never be deleted:
if( $data->getManager() ){
return; //here I want to return the custom response
}
$this->manager->remove($data);
$this->manager->flush();
}
You should throw an exception, then you should configurate your api_platform to handle this exception. ApiPlatform will convert exception into a response with message and the specified code.
Step1: Create dedicated exception class
<?php
// api/src/Exception/ProductNotFoundException.php
namespace App\Exception;
final class AdminNonDeletableException extends \Exception
{
}
Step2: In your data persister, throw exception:
public function remove($data, array $context = [])
{
/* #var Admin $data */
#The Manager can never be deleted:
if( $data->getManager() ){
throw new AdminNonDeletableException('thisAdminIsManager');
}
$this->manager->remove($data);
$this->manager->flush();
}
Step3: Add your exception in the config/package/api_platform.yaml file and declare the code number (414)
# config/packages/api_platform.yaml
api_platform:
# ...
exception_to_status:
# The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
ApiPlatform\Core\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
ApiPlatform\Core\Exception\FilterValidationException: 400
Doctrine\ORM\OptimisticLockException: 409
# Custom mapping
App\Exception\AdminNonDeletableException: 414 # Here is the handler for your custom exception associated to the 414 code
You can find more information in error handling chapter
Related
I'm creating a subscription-based SaaS platform in Laravel, where Laravel Cashier does not suit my needs. Therefore I need to implement the subscription-engine myself using the Stripe library.
I found it easy to implement the connection between Laravel and Stripe via hooking into the creation and deletion events of a Subscription class, and then create or cancel a Stripe subscription accordingly.
The Stripe library is unfortunately largely based on calling static methods on some predefined classes (.. like \Stripe\Charge::create()).
This makes it hard for me to test, as you normally would allow dependency injection of some custom client for mocking, but since the Stripe library is referenced statically, there is no client to inject. Is there any way of creating a Stripe client class or such, that I can mock?
Hello from the future!
I was just digging into this. All those classes extend from Stripe's ApiResource class, keep digging and you'll discover that when the library is about to make an HTTP request it calls $this->httpClient(). The httpClient method returns a static reference to a variable called $_httpClient. Conveniently, there is also a static method on the Stripe ApiRequestor class called setHttpClient which accepts an object which is supposed to implement the Stripe HttpClient\ClientInterface (this interface only describes a single method called request).
Soooooo, in your test you can make a call to ApiRequestor::setHttpClient passing it an instance of your own http client mock. Then whenever Stripe makes an HTTP request it will use your mock instead of its default CurlClient. Your responsibility is then have your mock return well-formed Stripe-esque responses and your application will be none the wiser.
Here is a very dumb fake that I've started using in my tests:
<?php
namespace Tests\Doubles;
use Stripe\HttpClient\ClientInterface;
class StripeHttpClientFake implements ClientInterface
{
private $response;
private $responseCode;
private $headers;
public function __construct($response, $code = 200, $headers = [])
{
$this->setResponse($response);
$this->setResponseCode($code);
$this->setHeaders($headers);
}
/**
* #param string $method The HTTP method being used
* #param string $absUrl The URL being requested, including domain and protocol
* #param array $headers Headers to be used in the request (full strings, not KV pairs)
* #param array $params KV pairs for parameters. Can be nested for arrays and hashes
* #param boolean $hasFile Whether or not $params references a file (via an # prefix or
* CURLFile)
*
* #return array An array whose first element is raw request body, second
* element is HTTP status code and third array of HTTP headers.
* #throws \Stripe\Exception\UnexpectedValueException
* #throws \Stripe\Exception\ApiConnectionException
*/
public function request($method, $absUrl, $headers, $params, $hasFile)
{
return [$this->response, $this->responseCode, $this->headers];
}
public function setResponseCode($code)
{
$this->responseCode = $code;
return $this;
}
public function setHeaders($headers)
{
$this->headers = $headers;
return $this;
}
public function setResponse($response)
{
$this->response = file_get_contents(base_path("tests/fixtures/stripe/{$response}.json"));
return $this;
}
}
Hope this helps :)
Based off Colin's answer, here is an example that uses a mocked interface to test creating a subscription in Laravel 8.x.
/**
* #test
*/
public function it_subscribes_to_an_initial_plan()
{
$client = \Mockery::mock(ClientInterface::class);
$paymentMethodId = Str::random();
/**
* Creates initial customer...
*/
$customerId = 'somecustomerstripeid';
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/customers";
})->andReturn([
"{\"id\": \"{$customerId}\" }", 200, []
]);
/**
* Retrieves customer
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($customerId) {
return $path === "https://api.stripe.com/v1/customers/{$customerId}";
})->andReturn([
"{\"id\": \"{$customerId}\", \"invoice_settings\": {\"default_payment_method\": \"{$paymentMethodId}\"}}", 200, [],
]);
/**
* Set payment method
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/payment_methods/{$paymentMethodId}";
})->andReturn([
"{\"id\": \"$paymentMethodId\"}", 200, [],
]);
$subscriptionId = Str::random();
$itemId = Str::random();
$productId = Str::random();
$planName = Plan::PROFESSIONAL;
$plan = Plan::withName($planName);
/**
* Subscription request
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId, $plan) {
$isSubscriptions = $path === "https://api.stripe.com/v1/subscriptions";
$isBasicPrice = $opts["items"][0]["price"] === $plan->stripe_price_id;
return $isSubscriptions && $isBasicPrice;
})->andReturn([
"{
\"object\": \"subscription\",
\"id\": \"{$subscriptionId}\",
\"status\": \"active\",
\"items\": {
\"object\": \"list\",
\"data\": [
{
\"id\": \"{$itemId}\",
\"price\": {
\"object\": \"price\",
\"id\": \"{$plan->stripe_price_id}\",
\"product\": \"{$productId}\"
},
\"quantity\": 1
}
]
}
}", 200, [],
]);
ApiRequestor::setHttpClient($client);
$this->authenticate($this->user);
$res = $this->putJson('/subscribe', [
'plan' => $planName,
'payment_method_id' => $paymentMethodId,
]);
$res->assertSuccessful();
// Actually interesting assertions go here
}
I'm creating validation (via FormRequest ) for my API and I need to change status code depending on failed validation rule (e.g. If id is string instead of int, get 400. If id doesn't exists, get 404).
I wanted to write something like that:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
$failedRules = $this->getValidatorInstance()->failed();
$statusCode = 400;
if (isset($failedRules['id']['Exists'])) $statusCode = 404;
return response($errors, $statusCode);
}
However, $this->getValidatorInstance()->failed() returns empty array
Why does $this->getValidatorInstance()->failed return empty array?
How can I fix that? Is there some other way to return status code depending on failed validation rule?
The reason you're getting an empty array when your call $this->getValidatorInstance()->failed() is because it's actually resolving a new instance of Validator.
What you can do is call passes() on the new Validator instance which will then allow you to call failed() to get the rules:
$validator = $this->getValidatorInstance();
$validator->passes();
$failedRules = $validator->failed();
Alternatively, if you don't want to have the validator run twice you could override the failedValidation method to store the Validation instance in class:
protected $currentValidator;
protected function failedValidation(Validator $validator)
{
$this->currentValidator = $validator;
throw new ValidationException($validator, $this->response(
$this->formatErrors($validator)
));
}
public function response(array $errors)
{
$failedRules = $this->currentValidator->failed();
$statusCode = 400;
if (isset($failedRules['id']['Exists'])) $statusCode = 404;
return response($errors, $statusCode);
}
Hope this helps!
From following this example I have managed to set up the below Listener/Before Filter to parse all requests to my API endpoint (ie. /api/v1) before any controller actions are processed. This is used to validate the request type and to throw an error if certain conditions are not met.
I would like to differentiate the error response based on the applications environment state. If we are in development or testing, I simply want to throw the error encountered. Alternatively, if in production mode I want to return the error as a JSON response. Is this possible? If not, how could I go about something similar?
I'm using Symfony v3.1.*, if that makes any difference.
namespace AppBundle\EventListener;
use AppBundle\Interfaces\ApiAuthenticatedController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class ApiBeforeListener
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Validates a request for API resources before serving content
*
* #param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
* #return mixed
*/
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
return;
if ($controller[0] instanceof ApiAuthenticatedController) {
$headers = $event->getRequest()->headers->all();
// only accept ajax requests
if(!isset($headers['x-requested-with'][0]) || strtolower($headers['x-requested-with'][0]) != 'xmlhttprequest') {
$error = new AccessDeniedHttpException('Unsupported request type.');
if (in_array($this->container->getParameter("kernel.environment"), ['dev', 'test'], true)) {
throw $error;
} else {
// return json response here for production environment
//
// For example:
//
// header('Content-Type: application/json');
//
// return json_encode([
// 'code' => $error->getCode(),
// 'status' => 'error',
// 'message' => $error->getMessage()
// ]);
}
}
}
}
}
Unlike most events, the FilterControllerEvent does not allow you to return a response object. Be nice if it did but oh well.
You have two basic choices.
The best one is to simply throw an exception and then add an exception listener. The exception listener can then return a JsonResponse based on the environment.
Another possibility to to create a controller which only returns your JsonResponse then use $event->setController($jsonErrorController) to point to it.
But I think throwing an exception is probably your best bet.
More details here: http://symfony.com/doc/current/reference/events.html
I'm trying to create a kind of flash-message class that will "flush" messages when they are fetched the first time. So not on the next HTTP request if, for example, forwarded from one controller action to another (of the same HTTP request)
Here is my class design:
<?php
namespace MartynBiz;
/**
* Flash messages. Slight variation in that this will store a message until it
* is accessed - whether that is a next http request, or same request. Simply
* when get method is called the message is wiped from session.
* TODO move this to martynbiz\php-flash
*/
class Flash
{
/**
* Message storage
*
* #var ArrayObject
*/
protected $storage;
/**
* #param string $storage Name to store messages in session
*/
public function __construct($storage=null)
{
// if storage is not defined, create ArrayObject (not persistent)
if (is_null($storage)) {
$storage = new \ArrayObject();
}
$this->storage = $storage;
}
/**
* Add flash message
*
* #param string $key The key to store the message under
* #param string $message Message to show on next request
*/
public function addMessage($key, $message)
{
// create entry in the session
$this->storage[$key] = $message;
}
/**
* Get flash messages, and reset storage
* #return array Messages to show for current request
*/
public function flushMessages()
{
$messages = $this->storage->getArrayCopy();
// clear storage items
foreach ($this->storage as $key => $value) {
unset($this->storage[$key]);
}
return $messages;
}
}
I've written some PHPUnit tests that also demonstrate how the Flash class might be used:
<?php
// TODO test with an ArrayAccess/Object $storage passed in
use MartynBiz\Flash;
use Zend\Session\Container;
class FlashTest extends PHPUnit_Framework_TestCase
{
protected $flash;
public function testInstantiation()
{
$flash = new Flash();
$this->assertTrue($flash instanceof Flash);
}
public function testGettingSetting()
{
$flash = new Flash();
$flash->addMessage('key1', 'value1');
$flash->addMessage('key2', 'value2');
$flash->addMessage('key2', 'value3');
$expected = array(
'key1' => 'value1',
'key2' => 'value3',
);
// assert first time to access messages
$messages = $flash->flushMessages();
$this->assertEquals($expected, $messages);
// assert messages have been cleared
$messages = $flash->flushMessages();
$this->assertEquals(array(), $messages);
}
public function testCustomStorage()
{
$container = new Container('mycontainer');
$flash = new Flash($container);
$flash->addMessage('key1', 'value1');
$expected = array(
'key1' => 'value1',
);
// assert first time to access messages
$messages = $flash->flushMessages();
$this->assertEquals($expected, $messages);
// assert messages have been cleared
$messages = $flash->flushMessages();
$this->assertEquals(array(), $messages);
}
}
You can see that I'm also passing in a custom $storage (Zend session container instance) which is one option I'd like. These tests appear to pass, however I get the following error on the other ones and I don't really understand what the issue is:
$ ./vendor/bin/phpunit tests/library/FlashTest.php 1
PHPUnit 4.8.21-4-g7f07877 by Sebastian Bergmann and contributors.
.E.
Time: 68 ms, Memory: 4.50Mb
There was 1 error:
1) FlashTest::testGettingSetting
MartynBiz\Flash::flushMessages(): ArrayIterator::next(): Array was modified outside object and internal position is no longer valid
/var/www/crsrc-slimmvc/app/library/MartynBiz/Flash/Flash.php:53
/var/www/crsrc-slimmvc/tests/library/FlashTest.php:33
FAILURES!
Tests: 3, Assertions: 3, Errors: 1.
I've searched around for this error and tried a few alternatives (e.g. $this->storage->getIterator() ) but still I get the same error. Any ideas where I'm going wrong? I guess I'm a little new to ArrayObject.
I am using Guzzle 5.3 and want to test that my client throws a TimeOutException.
Then, how can I do a mock of Guzzle Client that throw a GuzzleHttp\Exception\ConnectException?
Code to test.
public function request($namedRoute, $data = [])
{
try {
/** #noinspection PhpVoidFunctionResultUsedInspection */
/** #var \GuzzleHttp\Message\ResponseInterface $response */
$response = $this->httpClient->post($path, ['body' => $requestData]);
} catch (ConnectException $e) {
throw new \Vendor\Client\TimeOutException();
}
}
Update:
The right question was: how to throw a Exception with Guzzle 5? or, how to test a catch block with Guzzle 5?
You can test code inside a catch block with help of the addException method in the GuzzleHttp\Subscriber\Mock object.
This is the full test:
/**
* #expectedException \Vendor\Client\Exceptions\TimeOutException
*/
public function testTimeOut()
{
$mock = new \GuzzleHttp\Subscriber\Mock();
$mock->addException(
new \GuzzleHttp\Exception\ConnectException(
'Time Out',
new \GuzzleHttp\Message\Request('post', '/')
)
);
$this->httpClient
->getEmitter()
->attach($mock);
$this->client = new Client($this->config, $this->routing, $this->httpClient);
$this->client->request('any_route');
}
In the unit test, I add the GuzzleHttp\Exception\ConnectException to the mock. After, I add the mock to the emitter and, finally, I call the method I want test, request.
Reference:
Source Code
Mockito test a void method throws an exception