Laravel cookies not available in service provider during unit testing - php

I have a service provider which instantiates a CartCookie class which generates a unique cookie for saving shopping carts. It's a singleton class and it's injected into the service container.
CartCookieServiceProvider.php
public function boot(Request $request)
{
$this->app->singleton(CartCookie::class, function ($app) use ($request) {
return new CartCookie($request);
});
}
CartCookie.php
use App\Cart;
use Illuminate\Http\Request;
class CartCookie
{
private $id;
private $request;
function __construct(Request $request)
{
$this->request = $request;
if ($request->cookie('cart_id')) {
$this->id = $request->cookie('cart_id');
} else {
$this->id = $this->generateUniqueCartId();
}
}
public function id()
{
return $this->id;
}
private function generateUniqueCartId()
{
do {
$id = md5(time() . 'cart' . rand(100000000000000, 9999999999999999));
} while (Cart::find($id));
return $id;
}
}
In the CartCookie class I check for the existence of a cart_id cookie. Works perfectly fine when using the application!
My issue is that during unit tests, the cart_id cookie is empty, but only when the Request comes from the service provider. If I obtain the Request from a Controller later on in the lifecycle for example, the cookie is present.
Here is an example of a test:
/** #test */
public function get__store_checkout__checkout_displays_database_cart_correctly()
{
$cart = $this->createDatabaseCart();
$cookie = ['cart_id' => Crypt::encrypt($this->cartCookie)];
$response = $this->call('get', route('root.store.checkout'), [
'seller_id' => $cart->seller->id,
], $cookie);
$cart->seller->items()->each(function ($item) use ($response) {
$this->assertContains($beat->item, $response->getContent());
});
}
I can tell the existence when I dd() the request cookies in both the service provider and the controller that handles the cart functionality. For some reason, only during unit tests, the request doesn't contain the cookie yet in the service provider.
Hope this makes sense.

From here: link
Try:
/** #test */
public function get__store_checkout__checkout_displays_database_cart_correctly()
{
$cart = $this->createDatabaseCart();
$cookie = ['cart_id' => Crypt::encrypt($this->cartCookie)];
//#TODO you must get the current request
//#TODO you must set $cookie to $request
//Or simply find a way to create the CartCookie you need using the $cookie from above
$cartCookie = new CartCookie($request);
//hopefully will swap the CartCookie::class instance
app()->instance(CartCookie::class, $cartCookie);
//Now that you have the CartCookie
$response = $this->call('get', route('root.store.checkout'), [
'seller_id' => $cart->seller->id,
], $cookie);
$cart->seller->items()->each(function ($item) use ($response) {
$this->assertContains($beat->item, $response->getContent());
});
}

Related

Use a request header with HTTP Client to external Api server

Consider the following request to a Symfony controller:
http http://127.0.0.1:8000/index x-token:1000
#[Route('/index', name: 'index')]
public function index(HttpClientInterface $client, Request $request): Response
{
$client->request('GET', 'http://0.0.0.0:3001', ['headers' => ['x-token' => $request->headers->get('x-token')]]);
return new JsonResponse();
}
This code snippet is a minimal example for the usage in a controller. The controller accepts a Request, and uses the x-token header for authenticating against the 3rd Party Api (here: localhost:3001).
Is there a way, to automate this process? So basically - listen to incoming requests and inject the x-token header into a specific Scoped Client or the default client in Symfony.
The goal is, not to do this in every usage of the Http Client, but have a configured client service.
The client will be used all over the codebase, not just in a controller like in this minimal example.
I know that I can use Service Decoration and extend the clients in use. I fail how to connect the dots and make this work.
Have you tried using symfony kernel events?
First of all, if you are calling some 3rd-party api, I'd suggest you to create a separate class at the infrastructure layer, for example MyApiProvider. Using HttpClient right from your controller is not smart, because you may also want to adjust something (for example api host, etc). So it's gonna look like this:
<?php
namespace App\Infrastructure\Provider;
class MyApiProvider
{
// Of course, this also be better configurable via your .env file
private const HOST = 'http://0.0.0.0:3001';
private HttpClientInterface $client;
private ?string $token = null;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function getSomething(): array
{
$response = $this->client->request(
'GET',
self::HOST,
['headers' => $this->getHeaders()]
);
return $response->toArray();
}
private function getHeaders(): array
{
$headers = [];
if ($this->token !== null) {
$headers['x-token'] = $this->token;
}
return $headers;
}
}
Then you need to use symfony's kernel.request event to inject token to your provider from the request:
<?php
namespace App\Event;
use Symfony\Component\HttpKernel\Event\KernelEvent;
class RequestTokenEventListener
{
private MyApiProvider $provider;
public function __construct(MyApiProvider $provider)
{
$this->provider = $provider;
}
public function onKernelController(KernelEvent $event): void
{
$request = $event->getRequest();
$token = $request->headers->get('x-token');
if ($token !== null) {
$this->provider->setToken($token);
}
}
}
And finally your controller:
#[Route('/index', name: 'index')]
public function index(MyApiProvider $provider): Response
{
$provider->getSomething();
return new JsonResponse();
}
So your provider is gonna have token context during each request, if the token is passed.

How to access route parameters from a service tagged monolog.processor?

Using Symfony 4.4, I'd like to add on the fly to all my a route parameter if it's found in the on request. E.g. for the user Id:
PUT | DELETE | POST mywebsite.com/users/{userid}/some-action
I could add my users ids each time I log but it's kind of cumbersome.
So I created a service:
//src/
// |__Services/
// |___UserProcessor.php
use Monolog\Processor\ProcessorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
// In my config.yaml this monolog.processor
final class UserProcessor implements ProcessorInterface
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function __invoke(array $record): array
{
dd($this->requestStack->getMasterRequest()->attributes);
}
}
When I run my postman on POST /users/{userid}/some-action I get this output:
[
"media_type" => "application/json"
]
From my understanding, symfony route parameters request attributes are not built yet at the moment my processor runs.
What should I do to make my processor access the attribute userid?
Check how the Symfony\Bridge\Monolog\Processor\RouteProcessor does it:
It's implemented as an EventSubscriber
class RouteProcessor implements EventSubscriberInterface, ResetInterface
{
private $routeData;
private $includeParams;
public function __construct(bool $includeParams = true)
{
$this->includeParams = $includeParams;
$this->reset();
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['addRouteData', 1],
KernelEvents::FINISH_REQUEST => ['removeRouteData', 1],
];
}
// rest of the implementation
}
During KernelEvents::REQUEST it adds the required request data (including request parameters, if so desired) to the internal state of the object, so when the processor is run it can access the data from the internal RouteProcessor::$routeData, and not from the request directly.
public function addRouteData(RequestEvent $event)
{
if ($event->isMainRequest()) {
$this->reset();
}
$request = $event->getRequest();
if (!$request->attributes->has('_controller')) {
return;
}
$currentRequestData = [
'controller' => $request->attributes->get('_controller'),
'route' => $request->attributes->get('_route'),
];
if ($this->includeParams) {
$currentRequestData['route_params'] = $request->attributes->get('_route_params');
}
$this->routeData[spl_object_id($request)] = $currentRequestData;
}
You could modify this approach to suit yourself, or even just use this processor directly (although it adds more data than what you are looking for in your question).

How to get User in kernel response in Symfony 5?

I am making simple api wrapper, so all requests to https://example.comA/api/me must be catched on kernel response level and forwarded to https://api.example.comB/me and all was fine however I cannot get the currently logged in User in that kernel response because it returns null:
namespace App\Manager\Api\Event;
use App\Provider\Core\Api\CoreApi;
use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\Security\Core\Security;
class ApiWrapperEventListener
{
private $coreApi;
private $security;
public function __construct(CoreApi $coreApi, Security $security)
{
$this->coreApi = $coreApi;
$this->security = $security;
}
public function onKernelResponse(ResponseEvent $event)
{
if (!$event->isMasterRequest()) return;
$request = $event->getRequest();
if ('/api' === substr($request->getPathInfo(), 0, 4)) {
dump($this->security->getUser()); // returns NULL
die;
try {
$response = $this->coreApi->call($request->getMethod(), $request->getPathInfo(), json_decode($request->getContent(), true) ?? []);
$event->setResponse(new JsonResponse($response));
} catch (BadResponseException $error) {
dump($error);
die;
}
}
}
}
I guess Symfony is firing those events before I get the User, is there a way to get this right?
I have to note that in other places like controllers or services I get the User right.
Ok i know what was the problem.
The response controller event has no User when 404 or 500 given.
In my case I was catching 404, passing to listener and modifying the request to 200.
This approach wasn't good, so i decided to move this to Controller itself.
/**
* #Route("/api/v1/{uri}", name="api", requirements={"uri"=".+"})
*/
public function api(
Request $request,
CoreApi $coreApi
):Response
{
try
{
$response = $coreApi->call($request->getMethod(), $request->getPathInfo(), json_decode($request->getContent(), true) ?? []);
return new JsonResponse($response);
}
catch(BadResponseException $error)
{
return new Response($error->getResponse()->getBody()->getContents(), $error->getResponse()->getStatusCode());
}
}

Why does a container run before middleware?

If we look at the Middleware concept published on the slim4 website and elsewhere.
It should be executed before a request reaches the application or when sending the response to the user.
The question is this, because even if a Middleware is executed before, a container is called before by the application:
Show me the code.
config
'providers' => [,
App\ServiceProviders\Flash::class => 'http'
],
'middleware' => [
App\Middleware\Session::class => 'http,console',
],
Session Middleware
class Session
{
public function __invoke(Request $request, RequestHandler $handler)
{
if (session_status() !== PHP_SESSION_ACTIVE) {
$settings = app()->getConfig('settings.session');
if (!is_dir($settings['filesPath'])) {
mkdir($settings['filesPath'], 0777, true);
}
$current = session_get_cookie_params();
$lifetime = (int)($settings['lifetime'] ?: $current['lifetime']);
$path = $settings['path'] ?: $current['path'];
$domain = $settings['domain'] ?: $current['domain'];
$secure = (bool)$settings['secure'];
$httponly = (bool)$settings['httponly'];
session_save_path($settings['filesPath']);
session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly);
session_name($settings['name']);
session_cache_limiter($settings['cache_limiter']);
session_start();
}
return $handler->handle($request);
}
}
Flash Message Container
class Flash implements ProviderInterface
{
public static function register()
{
$flash = new Messages();
return app()->getContainer()->set(Messages::class, $flash);
}
}
Execution app
...
// Instantiate PHP-DI ContainerBuilder
$containerBuilder = new ContainerBuilder();
AppFactory::setContainer($containerBuilder->build());
$app = AppFactory::create();
$providers = (array)$this->getConfig('providers');
array_walk($providers, function ($appName, $provider) {
if (strpos($appName, $this->appType) !== false) {
/** #var $provider ProviderInterface */
$provider::register();
}
});
$middlewares = array_reverse((array)$this->getConfig('middleware'));
array_walk($middlewares, function ($appType, $middleware) {
if (strpos($appType, $this->appType) !== false) {
$this->app->add(new $middleware);
}
});
....
$app->run();
Result
`Fatal error: Uncaught RuntimeException: Flash messages middleware failed. Session not found.`
Flash message needs a session started to work this already I know, and Middleware should be responsible for doing this, but it is always executed after the container
First, you are using dependency and container terms as if they are the same thing, which they are not.
About the problem with your code, in Flash::register() method, you are creating a new object from Messages class and putting this in the DI container. You are calling this method and forcing creation of the Message object, which needs the session to be already started, before letting the middleware start the session. You really should avoid storing objects in DIC, instead of storing their definition (how they are built). The following change is what I mean:
class Flash implements ProviderInterface
{
public static function register()
{
return app()->getContainer()->set(Messages::class, function() {
return new Messages();
});
}
}

Slim 3 Middleware Redirect

I want to check if a user is logged in. Therefor I have an Class witch returns true or false. Now I want a middleware which checks if the user is logged in.
$app->get('/login', '\Controller\AccountController:loginGet')->add(Auth::class)->setName('login');
$app->post('/login', '\Controller\AccountController:loginPost')->add(Auth::class);
Auth Class
class Auth {
protected $ci;
private $account;
//Constructor
public function __construct(ContainerInterface $ci) {
$this->ci = $ci;
$this->account = new \Account($this->ci);
}
public function __invoke($request, \Slim\Http\Response $response, $next) {
if($this->account->login_check()) {
$response = $next($request, $response);
return $response;
} else {
//Redirect to Homepage
}
}
}
So when the user is logged in the page will render correctly. But when the user is not autoriesed I want to redirect to the homepage. But how?!
$response->withRedirect($router->pathFor('home');
This doesn't work!
You need to return the response. Don't forget that the request and response objects are immutable.
return $response = $response->withRedirect(...);
I have a similar auth middleware and this is how I do it which also adds a 403 (unauthorized) header.
$uri = $request->getUri()->withPath($this->router->pathFor('home'));
return $response = $response->withRedirect($uri, 403);
Building off of tflight's answer, you will need to do the following to make everything work as intended. I tried to submit this as a revision, given that the code provided in tflight's answer would not work on the framework out of the box, but it was declined, so providing it in a separate answer:
You will need the following addition to your middleware:
protected $router;
public function __construct($router)
{
$this->router = $router;
}
Additionally, when declaring the middleware, you would need to add the following the constructor:
$app->getContainer()->get('router')
Something similar to:
$app->add(new YourMiddleware($app->getContainer()->get('router')));
Without these changes, the solution will not work and you will get an error that $this->router does not exist.
With those changes in place you can then utilize the code provided by tflight
$uri = $request->getUri()->withPath($this->router->pathFor('home'));
return $response = $response->withRedirect($uri, 403);
make basic Middleware and inject $container into it so all your middleware can extends it.
Class Middleware
{
protected $container;
public function __construct($container)
{
$this->container = $container;
}
public function __get($property)
{
if (isset($this->container->{$property})) {
return $this->container->{$property};
}
// error
}
}
make sure your Auth middleware on the same folder with basic middleware or you can use namespacing.
class Auth extends Middleware
{
public function __invoke($request, $response, $next)
{
if (!$this->account->login_check()) {
return $response->withRedirect($this->router->pathFor('home'));
}
return $next($request, $response);
}
}
Use:
http_response_code(303);
header('Location: ' . $url);
exit;

Categories