Symfony QueryParam with empty value - symfony

I'm trying to use QueryParam on my symfony controller.
The problem that i dont want to accept the empty params in the query. But when i define the QueryParam with
#REST\QueryParam(name="test" , description="The test", requirements="\w+", strict=true, nullable=false)
and i try to call the web-service with mysite.com/test= it's still working.
What i have to do to refuse empty param in the request without testing in the code if the param is empty or not
Thank you

When testing weirdly enough the validation did not seem to work with the configuration param_fetcher_listener simply on true, but when I used force, everything suddenly worked:
# config.yml
fos_rest:
param_fetcher_listener: force
And in the controller:
/**
* #QueryParam(name="test" , description="The test", requirements="[a-zA-Z]+", strict=true, nullable=false, allowBlank=false)
*/
public function indexAction($test)
{
// replace this example code with whatever you need
return new JsonResponse($test);
}
Note that in my example there's multiple validations and only ?test=a will work. No test, ?test= and ?test=1 will all not work for specified reasons.
If anyone knows how to get it to work with param_fetcher_listener: true I'm curious :)
Also see the documentation (https://symfony.com/doc/master/bundles/FOSRestBundle/param_fetcher_listener.html) for the implications of using force

Related

Symfony Tactician-bundle Typehints = Missing handler method error

I've used the thephpleague/tactician-bundle with Symfony before, but this is the first time I've used it with Symfony 4.* (specifically 4.1.4) and attempted to use a single handler Class for my Application Service.
When I execute a command in the Controller
public function postAction(Request $request, CommandBus $commandBus)
{
$form = $this->createForm(VenueType::class);
$form->submit($request->request->all(), true);
$data = $form->getData();
if($form->isValid()) {
$command = new CreateVenueCommand($data);
$commandBus->handle($command);
return $form->getData();
}
return $form;
}
... I get the following error:
"error": {
"code": 500,
"message": "Internal Server Error",
"exception": [
{
"message": "Could not invoke handler for command App\\Application\\Command\\CreateVenueCommand for reason: Method 'handle' does not exist on handler",
"class": "League\\Tactician\\Exception\\CanNotInvokeHandlerException",
"trace": [
I've seemingly followed the installation documents for the tactician-bundle and installed it using Flex. As far as I can tell everything is configured correctly, so I'm unsure what I'm missing in my implementation.
Implementation
As per the thephpleague/tactician-bundle installation guide I've installed using Flex and the bundle is registered and the config package installed:
tactician:
commandbus:
default:
middleware:
- tactician.middleware.locking
- tactician.middleware.doctrine
- tactician.middleware.command_handler
After creating the DTO Command Class 'CreateVenueCommand', I created the handler Class:
use App\Infrastructure\Domain\Model\VenueRepositoryInterface;
use App\Application\Command\CreateVenueCommand;
use App\Domain\Entity\Venue;
class VenueApplicationService
{
private $venueRepository;
public function __construct(VenueRepositoryInterface $venueRepository)
{
$this->venueRepository = $venueRepository;
}
/**
* #param CreateVenueCommand $aCommand
* #throws \Exception
*/
public function createVenue(CreateVenueCommand $aCommand)
{
$aVenue = new Venue($aCommand->getData())
if ($aVenue === null) {
throw new \LogicException('Venue not created');
}
$this->venueRepository->add($aVenue);
}
Then I registered the handler Class as a Service taking advantage of Symfony's autowiring and Tacticians typehints:
App\Application\VenueApplicationService:
arguments:
- '#App\Infrastructure\Persistence\Doctrine\DoctrineVenueRepository'
tags:
- { name: tactician.handler, typehints: true }
So according to the installation documents, typehints work if:
The method must be public.
The method must accept only one parameter.
The parameter must be typehinted with a class name.
Also, and this is specific to my use case:
If you have multiple commands going into a single handler, they will all be detected, provided they follow the rules above. The actual name of the method is NOT important.
So when I invoke the commandbus in the Controller Class, I'm unsure why I'm getting the error above.
If I change the Command Handler method to:
public function handle(CreateVenueCommand $aCommand)
{
... then it works fine. This would seem to suggest that the typehints aren't working as documented.
It seems in this case that the actual name of the method IS important. ... or I've made some form of error in my implementation ... or I'm misunderstanding the multiple commands going into a single handler use case??
Any assistance would be greatly appreciated.
Solution
With a big thanks to kunicmarko20 for pointing me in the right direction.
Specifically for my use case I simply needed to use one of Tacticians MethodNameInflector classes, configured in Symfony thus:
tactician:
commandbus:
default:
middleware:
- tactician.middleware.locking
- tactician.middleware.doctrine
- tactician.middleware.command_handler
method_inflector: tactician.handler.method_name_inflector.handle_class_name
... then it was simply a matter of naming each Handler method in my Application Service class 'handle{whateverYouLike}Command
Here under 1. is explained how the naming works, if you want to use a different name than in this table you can implement MethodNameInflector Interface and provide a name of the method.

Having mapping issues when trying to save monolog logs into Elasticsearch use ElasticsearchHandler

I'm trying to start sending my logs into elastic search using monolog. (I'm using Symfony2).
I've set up monolog like this:
monolog:
handlers:
elasticsearch:
elasticsearch:
host: %logger_elastic_host%
port: %logger_elastic_port%
type: elasticsearch
level: info
It worked only few minutes until it broke with this error messages(a fatal error, I removed useless stuff):
create: /monolog/logs/AVQKYsGRPmEhlo7mDfrN caused
MapperParsingException[failed to parse [context.stack.args]]; nested:
ElasticsearchIllegalArgumentException[unknown property [class]];
I've been looking with my collegue how to fix that. What we found out is:
Elastic search receive the first logs and automatically build a mapping
We send new logs with another mapping or slightly different to what was sent before and it breaks.
In this case it's breaking here: context.stack.args.
The problem is that the context will always be very different.
What we would like is:
is anyone out there using Monolog to log to Elasticsearch
How do you guys manage to avoid this issue. (How can we manage to avoid it)?
thanks guys.
This is happening because ES creates a mapping from the first document. If any document that is inserted after has the same property but with other type/format then ES will throw an error.
A solution is to create a custom monolog formatter and register it:
config.yml:
elasticsearch:
type: elasticsearch
elasticsearch:
host: elasticsearch
ignore_error: true
formatter: my_elasticsearch_formatter
This line will make Monolog\Handler\ElasticSearchHandler ignore any other errors from Ruflin's Elastica package:
ignore_error: true
Then register a service with this name: my_elasticsearch_formatter:
<service id="my_elasticsearch_formatter" class="AppBundle\Services\MyFormatter">
<argument type="string">monolog</argument>
<argument type="string">logs</argument>
</service>
first argument is the index name, second arg is the type.
And the formatter class:
<?php
namespace AppBundle\Services;
use function json_encode;
use Monolog\Formatter\ElasticaFormatter;
use function var_dump;
class MyFormatter extends ElasticaFormatter
{
/**
* #param string $index
* #param string $type
*/
public function __construct($index, $type)
{
parent::__construct($index, $type);
}
/**
* #param array $record
* #return array|\Elastica\Document|mixed|string
*/
public function format(array $record)
{
$record['context'] = json_encode($record['context']);
return parent::format($record);
}
}
The downside of this solution is that it will json_encode the context. You will not be able to filter by inner properties of the context in ES but at least you will not lose important information about your logs.

"Cannot set session ID after the session has started" when generating csrf token within Behat test

I'm trying to test that a user with the wrong permissions sees the correct response when visting a page that has a csrf token in the path.
I've added a Behat context step to create a csrf token using the 'security.csrf.token_manager', however then visit the page with this token in the path, I get a "Cannot set session ID after the session has started" 500 error.
Can anyone advise what I'm doing wrong, or how I work around this, please?
/**
* #When /^I go to the application admin archive page for "(?P<status>[^"]*)" application (?P<number>\d+) with a valid token$/
*
* #param string $status
* #param int $number
*/
public function iGoToTheApplicationAdminArchivePageForApplicationWithAValidToken($status, $number)
{
$tokenManager = $this->kernel->getContainer()->get('security.csrf.token_manager');
$token = $tokenManager->getToken(ApplicationAdminController::CSRF_ARCHIVE);
var_dump($token);
$this->visitAdminPage('archive', $status, $number, ['token' => $token]);
}
The specific error message you get seems to be a result of a bug in symfony2. Try to apply maryo's suggestion about extending MockFileSessionStorage class and checking if the id is empty before attempting to set it like this:
public function setId($id)
{
if ($this->id !== $id) {
parent::setId($id);
}
}
You can then use this fixed class in your functional test by doing something like this after creating the client:
$client->getContainer()->set('session', new Session(new FixedMockFileSessionStorageHelper()));
I still got this error at this moment. I fixed this issue by checking your file config of env at
app/config/config_test.yml
I found this configuration under framework section
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
All what you have to do just remove this line test: ~
to be as below:
framework:
session:
storage_id: session.storage.mock_file
profiler:
collect: false
Then clear the cache by
php bin/console cache:clear --env=test --no-debug
Refresh and Enjoy ;)

Testing Symfony2 emails with Behat 3

I followed the Behat 2.5 docs to test mails. After a few tweaks to match Behat 3 I have ended with the following code (I have removed non-relevant parts):
public function getSymfonyProfile()
{
$driver = $this->mink->getSession()->getDriver();
if (!$driver instanceof KernelDriver) {
// Throw exception
}
$profile = $driver->getClient()->getProfile();
if (false === $profile) {
// Throw exception
}
return $profile;
}
/**
* #Then I should get an email with subject :subject on :email
*/
public function iShouldGetAnEmail($subject, $email)
{
$profile = $this->getSymfonyProfile();
$collector = $profile->getCollector('swiftmailer');
foreach ($collector->getMessages() as $message) {
// Assert email
}
// Throw an error if something went wrong
}
When I run this test, it throws the following error:
exception 'LogicException' with message 'Missing default data in Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector' in vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php:93
Stack trace:
#0 vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php(122): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMailerData('default')
#1 features/bootstrap/FeatureContext.php(107): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMessages()
My profiler is configured as follows:
# app/config/config_test.yml
framework:
test: ~
profiler:
enabled: true
collect: true
It seems that the Profile is correctly loaded and the MessageDataCollector from Swiftmailer does exist, but it is not doing its work as expected. Any clue to solve this?
Maybe the issue you have has been fixed as I do not have this anymore (I'm using Behat v3.0.15, BrowserKit driver 1.3.* and Symfony v2.6.6).
I managed to reproduce your error but only when I forgot to enable profiler data collecting:
profiler:
collect: false
Once this problem solved (the configuration you provided solving the problem for me) I managed to check emails in my Behat tests.
Two solutions for this:
Solution #1: Intercepting redirects globally
If it does not break all your other tests you can do so by configuring your web profiler as follows:
web_profiler:
intercept_redirects: true
Solution #2: Preventing client to follow redirections temporarily
For my part, intercepting redirections globally in the configuration broke most of my other functional tests. I therefore use this method instead.
As preventing redirections allows mainly to check data in the data collectors I decided to use a tag #collect on each scenario requiring redirect interception. I then used #BeforeScenario and #AfterScenario to enable this behaviour only for those scenarios:
/**
* Follow client redirection once
*
* #Then /^(?:|I )follow the redirection$/
*/
public function followRedirect()
{
$this->getDriver()->getClient()->followRedirect();
}
/**
* Restore the automatic following of redirections
*
* #param BeforeScenarioScope $scope
*
* #BeforeScenario #collect
*/
public static function disableFollowRedirects(BeforeScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(false);
}
/**
* Restore the automatic following of redirections
*
* #param AfterScenarioScope $scope
*
* #AfterScenario #collect
*/
public static function restoreFollowRedirects(AfterScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(true);
}
It's not the answer your are looking for, but I'm pretty sure it will suits your needs (perhaps more).
If I can suggest, try using Mailcatcher with this bundle: https://packagist.org/packages/alexandresalome/mailcatcher
You'll be able to easily tests if emails are sent, what's their subject, follow a link in the body, and so on...
Many steps are included with this bundle.

Behat authenticate Symfony2 user

I'm using Behat in Symfony2 / Doctrine2. Now, I have this scenario that boils down to the fact that "if I'm logged in and I go to /login, I shoud go to / instead":
#login
Scenario: Go to the login page while being logged in
Given I am logged in
When I go to "/login"
Then I should be on "/"
For the #login, I created the following:
/**
* #BeforeScenario #login
*/
public function loginUser()
{
$doctrine = $this->getContainer()->get('doctrine');
$userRepository = $doctrine->getRepository('MyTestBundle:User');
$user = $userRepository->find(1); // 1 = id
$token = new UsernamePasswordToken($user, NULL, 'main', $user->getRoles());
$this->getContainer()->get('security.context')->setToken($token);
}
In the "when I go to /login" code (the controller gets called), the token seems gone (not what I intended):
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$token = $this->get('security.context')->getToken();
$fd = fopen('/tmp/debug.log', 'a');
fwrite($fd, $token);
// prints 'AnonymousToken(user="anon.", authenticated=true, roles="")'
...
But in the FeatureContext, it seems to stick around (the way I hoped it would work). In the "Given I am logged in":
/**
* #Given /^I am logged in$/
*/
public function iAmLoggedIn()
{
$token = $this->getContainer()->get('security.context')->getToken();
$fd = fopen('/tmp/debug.log', 'a');
fwrite($fd, $token);
// prints 'UsernamePasswordToken(user="admin", authenticated=true, roles="ROLE_ADMIN")'
...
I run behat like this:
app/console -e=test behat
I also did this in the controller to be sure it's test:
fwrite($fd, $this->get('kernel')->getEnvironment());
// prints 'test'
Any clue how to authenticate a user? I will have to test a lot of admin pages, so it would be nice if I could hook the login into #BeforeSuite, #BeforeFeature (or #BeforeScenario ...) so that I don't get blocked.
(Suggestions on disabling the authentication mechanism for testing, or a way to stub/mock a user are also welcome.)
Oh my. It doesn't work because the DIC inside your FeatureContext isn't shared with your app - your app has separate kernel and DIC. You can get it through Mink. Or, you can simply do it right way :-)
Right way means, that every part of behavior, that is observable by the enduser, should be described inside *.feature, not inside FeatureContext. It means, that if you want to login a user, you should simply describe it with steps (like: "i am on /login", "and i fill in username ...", "i fill in password" and stuf). If you want to do it in multiple times - you should create a metastep.
Metasteps are simply steps, that describe multiple other steps, for example - "i am logged in as everzet". You could read bout them here: http://docs.behat.org/guides/2.definitions.html#step-execution-chaining
Here is an solution for login with OAuth I've used. After number of times of searching for the answer and landing on this page I thought it would be great to share the solution. Hopefully it will help someone.
Background: Symfony2 App using HWIOAuthBundle, hooked up to some OAuth2 provider.
Problem: How do I implement Given I'm logged in when Behat context in not shared with Symfony context?
Solution:
HWIOAuthBundle uses #buzz service for all API calls to OAuth providers. So all you need to do is replace Buzz client with your implementation which doesn't call external services, but returns the result straight away. This is my implementation:
<?php
namespace Acme\ExampleBundle\Mocks;
use Buzz\Client\ClientInterface;
use Buzz\Message\MessageInterface;
use Buzz\Message\RequestInterface;
class HttpClientMock implements ClientInterface
{
public function setVerifyPeer()
{
return $this;
}
public function setTimeout()
{
return $this;
}
public function setMaxRedirects()
{
return $this;
}
public function setIgnoreErrors()
{
return $this;
}
public function send(RequestInterface $request, MessageInterface $response)
{
if(preg_match('/\/oauth2\/token/', $request->getResource()))
{
$response->setContent(json_encode([
'access_token' => 'valid',
'token_type' => 'bearer',
'expires_in' => 3600
]));
}
elseif(preg_match('/\/oauth2\/me/', $request->getResource()))
{
$response->setContent(json_encode([
'id' => 1,
'username' => 'doctor',
'realname' => 'Doctor Who'
]));
}
else throw new \Exception('This Mock object doesn\'t support this resource');
}
}
Next step is to hijack the class used by HWIOAuthBundle/Buzz and replace it with the implementation above. We need to do it only for test environment.
# app/config/config_test.yml
imports:
- { resource: config_dev.yml }
parameters:
buzz.client.class: Acme\ExampleBundle\Mocks\HttpClientMock
And finally, you need to set require_previous_session to false for test environment - therefore I suggest to pass it as parameter.
# app/config/security.yml
security:
firewalls:
secured_area:
oauth:
require_previous_session: false
Now you can implement your step like this.
Specification:
Feature: Access restricted resource
Scenario: Access restricted resource
Given I'm logged in
When I go to "/secured-area"
Then I should be on "/secured-area"
And the response status code should be 200
Implementation:
<?php
/**
* #Given /^I\'m logged in$/
*/
public function iMLoggedIn()
{
$this->getSession()->visit($this->locatePath('/login/check-yourOauthProvider?code=validCode'));
}
The code you're passing is not relevant, anything you pass will be OK as it's not being checked. You can customise this behaviour in HttpClientMock::send method.
http://robinvdvleuten.nl/blog/handle-authenticated-users-in-behat-mink/ is simple, clean article on how to create a login session and set the Mink session cookie so that the Mink session is logged in. This is much better than using the login form every time to login a user.
It’s ok to call into the layer “inside” the UI layer here (in symfony: talk to the models).
And for all the symfony users out there, behat recommends using a Given step with a tables arguments to set up records instead of fixtures. This way you can read the scenario all in one place and make sense out of it without having to jump between files:
Given there are users:
| username | password | email |
| everzet | 123456 | everzet#knplabs.com |
| fabpot | 22#222 | fabpot#symfony.com |

Resources