Mocking services when testing with Mink? - symfony

I'm working on an application now that has a decent amount of javascript (dynamic forms) that I want to test. I've decided on using PHPUnit Mink + ZombieJS.
I want to mock some services that communicate with external APIs, but I'm not finding any resources on how this can be done with using a testing framework like Mink. If I was using Symfony's built in WebTestCase, I would use a bundle like the
TestDoubleBundle, but it won't work for real world requests like those in Mink.
Are there any other solutions available for this? I wonder if the best approach is to create an "test" version of my API service and configure my test environment to load that service instead of the real one. The test service would respond to various API methods with pre-determined responses. The big downside is that it's not as flexible - I can't dynamically configure the expected responses from the API methods.

I struggled a bit while trying to work out how TestDoubleBundle works but actually it is very simple. I'll show you a working example which I hope it would be helpful for you.
Case study: Let's http://api.postcodes.io/postcodes/{postcode} API in real life to get full details of given postcode. If we provide a non-existent postcode it returns null.
Example below is trimmed down version so you'll need to go to Mocking external APIs with behat in symfony for the full version.
FULL EXAMPLE
services.yml
services:
app.service.postcode:
class: AppBundle\Service\PostcodeService
arguments:
- "#app.util.guzzle"
- "%postcodes_api%"
app.util.guzzle:
class: GuzzleHttp\Client
tags:
- { name: test_double }
PostcodeService
class PostcodeService
{
private $client;
private $apiUri;
public function __construct(
ClientInterface $client,
$apiUri
) {
$this->client = $client;
$this->apiUri = $apiUri;
}
public function get($postcode)
{
return $this->getDetails($postcode);
}
private function getDetails($postcode)
{
$details = null;
try {
/** #var Response $response */
$response = $this->client->request(Request::METHOD_GET, $this->apiUri.$postcode);
/** #var Stream $body */
$body = $response->getBody();
$details = $body->getContents();
} catch (ClientException $e) {
}
return $details;
}
}
FeatureContext
/**
* #param string $postcode
*
* #Given /^the Postcode API is available for "([^"]*)"$/
*/
public function thePostcodeApiIsAvailableFor($postcode)
{
$postcodesApi = $this->kernel->getContainer()->getParameter('postcodes_api').$postcode;
$response = new Response(200, [], '{"result":{"latitude":0.12345678,"longitude":-0.1234567}}');
$this->kernel
->getContainer()
->get('app.util.guzzle.prophecy')
->request(Request::METHOD_GET, $postcodesApi)
->willReturn($response);
}
Gherkin
Scenario: I get full result for existent postcode.
Given the Postcode API is available for "DUMMY POSTCODE"
When I send a "GET" request to "/postcodes/DUMMY POSTCODE"
Then the response status code should be 200
And the response should contain json:
"""
{"result":{"latitude":0.12345678,"longitude":-0.1234567}}
"""

Related

Symdony5.3 - How to pass parameters from a Controller to a Service because the service calls a repo query?

I am working on a project where Symfony serves as API backend (with ApiPlatform) and Angular the Front End and the lead decided we will use Services and to create a function inside called updateData().
In my Service:
public function updateData(array $dates, Hotel $hotel): ?array
{
$bookings= $this->em->getRepository(Booking::class)->findAllByIdAndDate($id, $date);
foreach ($bookings as $booking) {
...
}
...
}
In my controller:
/**
* #Route("/update_data", name="update_data")
*/
public function index(UpdateData $updateData)
{
$this->em = $this->getDoctrine()
->getManager()
->getRepository(Hotel::class);
$date = new \DateTime('2021-06-13');
$id = 1;
$hotel = $this->em->find($id);
$message = $updateData->updateData([$date], $hotel);
}
My question is how can I receive the data here and pass the parameters from this controller to the service?
Thanks
In order to update the data for a specific hotel, you can use url parameters or query parameters to customize your controller.
for example, you could use a URL like this: /update_data/1?date=2021-06-13
Then your code would be using Symfony route parameters and parameter conversion.
Here is a quick example of what this would look like.
/**
* #Route("/update_data/{id<\d+>}", name="update_data")
*/
public function update_data(Hotel $hotel, Request $request): Response
{
// the $hotel variable is autoconverted using parameter conversion
$date = new \DateTime($request->query->get('date'));
$message = $updateData->updateData([$date], $hotel);
// rest of your code.
}

Protected my annotations route for LexikJwt

I am trying to implement the LexikJwt package for my new API:
https://github.com/lexik/LexikJWTAuthenticationBundle
I just added my custom command to create a proper JWT token including my default roles for local testing. The next step would be to actually protect my routes. What i can't find however is exactly on how to do that. This is my current controller implementation using annotation routes
/**
* #Route(
* "/search",
* name="search",
* methods={"GET"},
* )
*/
class Search
{
/**
* #param AddressService $addressService
* #param Request $request
* #return JsonApiResponse
* #throws Exception
*/
public function __invoke(AddressService $addressService, Request $request)
{
$address = $addressService->createFromParams($request->query->all());
try {
$addressCollection = $addressService->search($address);
} catch (AddressNotFoundException $e) {
$addressCollection = [];
}
return new JsonApiResponse($addressCollection);
}
}
However the docs do not say anything about annotation routes and only on yml configs on security firewalls. The main thing i need is the token to be verified:
Is the token valid (matching the public key)
Is the token not expired
Does the route match the given roles
For example, i want the code above, which is an address service to match only if the token matches above and the token holds the role or scope: address:search.
Hope someone can help,
Pim
The issue was that the role should start with ROLE_. It is possible to override it though but this was the use case.

Inject service based on dynamic value in Symfony

I have 2 services, BlueWorkerService and YellowWorkerService, both implementing the same interface, WorkerServiceInterface. Each of these services use the same entities but with different required logic.
I need to inject one of, but not both, of these classes and use them in ProcessorService so that the interface methods are called using on correct Worker. Which worker service to use is dependent on which Worker is currently being processed. I'll break it down:
Class WorkerProcessor {
private $workerService;
public function __construct(WorkerServiceInterface $workerServiceInterface)
{
$this->workerService = $workerServiceInterface;
}
public function getMixedColourWithRed() {
return $this->workerService->mixWithRed();
}
}
The worker service that is being used would be based on whether the worker being processed has the colour property of Blue or Yellow.
I know I can probably use a Factory to achieve this as described here but my problem is how to tell the factory which Worker colour I am processing?
Running on Symfony 3.4
If you need more info, just ask and I will update the question.
NOTE: I'm using Symfony 4.3.1. I'll post it like that, then I'll help you to move all code from this architecture to Symfony 3.4.
I'm using a similar concept to load different classes in my project. Let me explain first, then I'll add code under this text.
Firstly, I'm loading a custom compiler pass under src/Kernel.php (your file is app/AppKernel.php):
/**
* {#inheritDoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new BannerManagerPass());
}
BannerManagerPass its created under src/DependencyInjection/Compiler (in your case should be src/BUNDLE/DependencyInjection/Compiler`).
class BannerManagerPass implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->has(BannerManager::class)) {
return;
}
$definition = $container->findDefinition(BannerManager::class);
$taggedServices = $container->findTaggedServiceIds('banner.process_banners');
foreach (array_keys($taggedServices) as $id) {
$definition->addMethodCall('addBannerType', [new Reference($id)]);
}
}
}
As you see, this class should implement CompilerPassInterface. You can observe that I'm looking for specific services tagged as banner.process_banners. I'll show how I tagged services a little bit later. Then, I'm calling addBannerType method from BannerManager.
App\Service\BannerManager.php: (in your case src/BUNDLE/Service/BannerManager.php)
class BannerManager
{
/**
* #var array
*/
private $bannerTypes = [];
/**
* #param BannerInterface $banner
*/
public function addBannerType(BannerInterface $banner)
{
$this->bannerTypes[$banner->getType()] = $banner;
}
/**
* #param string $type
*
* #return BannerInterface|null
*/
public function getBannerType(string $type)
{
if (!array_key_exists($type, $this->bannerTypes)) {
return null;
}
return $this->bannerTypes[$type];
}
/**
* Process request and return banner.
*
* #param string $type
* #param Server $server
* #param Request $request
*
* #return Response
*/
public function process(string $type, Server $server, Request $request)
{
return $this->getBannerType($type)->process($request, $server);
}
}
This class has a custom method (created by me) called process(). You can name it whatever you want it, but I think that's pretty verbose. All parameters are sent by me, so don't mind. You can send whatever you want.
Now we have our Manager and compiler pass is set. It's time to set our banner types (based on my example) and tag them!
My banner types are under src/Service/Banner/Types (in your case should be src/BUNDLE/Service/WhateverYouWant/Type. This does not matter! You can change it later from services.yaml).
These types are implementing my BannerInterface. It does not matter the code under the class in this instance. One more thing that I should warn you! You should see that under BannerManager, inside the addBannerType() I'm calling $banner->getType(). This is one method inherited from BannerInterface in my case and it has a unique string (in my example I have three banner types: small, normal, large). This method can have any name, but don't forget to update it as well in your manager.
We are almost ready! We should tag them, then we are ready to try them!
Go to your services.yaml and add these lines:
App\Service\Banner\Types\:
resource: '../src/Service/Banner/Types/'
tags: [banner.process_banners]
Please see the tag!
Whatever I want to show a custom banner, I'm using a simple URL with $_GET where I keep my banner type, then I load it like this:
public function view(?Server $server, Request $request, BannerManager $bannerManager)
{
...
return $bannerManager->getBannerType($request->query->get('slug'))->process($request, $server);
}

How to get #request_stack service in app/console context?

I have services that require the #request_stack to fetch parameters.
Now, I want to expose certain functionality to console commands callable via ./app/console//. Yet in the context of an ./app/console, there is no #request_stack, yet one can input arguments.
In order to resolve this issue, I am now creating basically two services, one basic, only waiting for the params, and one being able to use the #request_stack.
Yet I dislike that there are two ways for the data to be fetched in the request-based flow and via the app/console.
Hence I am wondering, as I am simply want the data that comes per default via the request to also be able to be inputted via console arguments:
Can I setup a custom request_stack to simulate a request during a console command?
When I was investigating this issue, I stumbled across request stack push method, where a warning was already in place in the doc block:
/**
* Pushes a Request on the stack.
*
* This method should generally not be called directly as the stack
* management should be taken care of by the application itself.
*/
public function push(Request $request)
{
$this->requests[] = $request;
}
So while it would be possible to do it this way, I decided against the approach of my original question and to refactor my application instead.
I have created a context value object which just holds the parameter data:
/**
* Context
**/
class Context
{
/**
* #var string
*/
private $countryCode;
/**
* Context constructor.
* #param string $countryCode
*/
public function __construct($countryCode = '')
{
$this->countryCode = $countryCode;
}
/**
* #return string
*/
public function getCountryCode()
{
return $this->countryCode;
}
}
And a ContextFactory that creates the context with by the request stack:
class ContextFactory extends RequestAwareService
{
/**
* ContextFactory constructor.
* #param RequestStack $stack
*/
public function __construct(RequestStack $stack)
{
$this->setRequestStack($stack);
}
/**
* #return Context
*/
public function create()
{
return new Context($this->request->getCountryCode());
}
}
(The RequestAwareService is just a helper class to more easily parse the request.)
I then defined the services in my Bundle services.yml:
context.factory:
class: Kopernikuis\MyBundle\Service\Config\ContextFactory
arguments:
- '#request_stack'
context:
class: Kopernikuis\MyBundle\Service\Config\Context
factory:
- '#context.factory'
- create
Instead of injecting the #request_stack, I am now injecting my #context value object, which also had the benefit of reducing the hierarchy as now only one service parses the request_stack once, and I also noticed that certain functionality got much simpler as I could remove parameters from method calls, as they were all provided by the context object instead.
And in my custom commands, I can just replace my context
protected function execute(InputInterface $input, OutputInterface $output)
{
// #todo: use variable from InputInterface
$context = new Context('fnordfoo');
$this->getContainer()->set('context', $context);
}
With the newly gained knowledge, I strongly disagree with my original intent of trying to manually set the #request_stack.
Refactoring the code base to not necessarily require the #request_stack was a more solid choice.

Symfony services --> correct usage?

first of all I am trying to use services for the first time... (Actually if s.o. could give a short info about how and when and why to use it.. nice ;-) )
But now to my specific case:
I wrote two controllers:
One for uploading a xlsx file to the server
One for importing the xlsx data to the DB
What I now want to do is to pass the (uploaded)path from the uploading controller to the import controller. am I correct to use the import as a service?
Code looks as the following...
class FileUploadController extends Controller
/**
* #Route("/upload", name="upload")
* #Security("has_role('ROLE_ADMIN')")
*/
public function uploadAction(Request $request){
$companyid = $this->getUser()->getCompany();
if ($request->getMethod() == 'POST'){
$file = $request->files->get('xls');
$uploadedURL = '';
if(($file instanceof UploadedFile) && $file->getError()=='0'){
if(!($file->getSize()<20000)){
$originalName = $file->getClientOriginalName();
$name_array = explode('.',$originalName );
$file_type = $name_array[(sizeof($name_array)-1)];
$valid_filetypes = array('xls', 'xlsx');
if(in_array(strtolower($file_type), $valid_filetypes)){
$document = new Document();
$document->setFile($file);
$document->setSubDirectory('uploads');
$document->processFile();
$uploadedURL=$uploadedURL=$document->getUploadDirectory().DIRECTORY_SEPARATOR.$document->getSubDirectory().DIRECTORY_SEPARATOR.$file->getBasename();
}else{
echo "Wrong File Ending";
}
}else {
echo "File to big";
}
}else{
print_r('File Error');
die;;
}
$this->get("dataimport.service")->importIndexAction($uploadedURL);
}else{
return $this->render(bla)
DataImportController as:
class DataImportController extends Controller
/**
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #Security("has_role('ROLE_ADMIN')")
*/
public function importIndexAction($path)
{
$companyid = $this->getUser()->getCompany();
$em = $this->getDoctrine()->getManager();
$file = $this->defineFilePathAction($path);
$reader = $this->readExcelAction($file);
$accountarray = $this->getAccountsArrayAction($companyid);
$this->importAccountsAction($companyid, $reader, $accountarray, $em);
}
....
/**
* Get a service from the container
*
* #param string The service to get
*/
public function get($service)
{
return $this->container->get($service);
}
services.yml
services:
dataimport.service:
class: AppBundle\Controller\DataHandling\DataImportController
arguments: [#service_container]
Thanks for your help!
In Symfony a controller and service is at first just a class. A controller's public method is meant to take an input and generates an Response (output) (by the way injecting the Request is deprecated, you have to use the current request from the request_stack). A service is an object out of the DI container with no constraints at all.
Since the controller's method has to generate and return a response, it's mostly not a good idea to invoke a controller from another controller, because you maybe don't need that response, but only the implementation of the method.
That's also the reason why you should move reusable code to services. A controller should actually only:
extract data from the request
call some services
render a template with the result
Same for Commands. The services are the core of your application. Horizontal communication between controllers or commands is mostly a bad idea (only of course some proxies or wrapper).
Here are some ideas for your code:
The action itself is too much unreadable code. If you get the uploaded file via symfony form read this https://stackoverflow.com/a/28754907/4469738
Don't access the request directly if you use forms. The reason is, that only your builder or Type class which creates the form (and the data class), knows the name of the input fields and maps them to a data class. You should just use the data class. Then you get a nice UploadedFile object to check everything, but also move the checks to services.

Resources