How to set the locale on a Symfony CLI command? - php

I run a multi-language Symfony 4.4 website where the locale is part of the URL. So I have /it/, /fr/ and so on.
My routes look like this:
/**
* #Route({
* "it": "/it/azienda/",
* "es": "/es/empresa/"
* }, name="app_cms_about")
*/
Then I've a pre-controller event that set the correct locale:
public function onRouteRequest(ControllerEvent $event)
{
// ... checks and stuff ...
$event->getRequest()->setLocale($ln);
}
Everything works as expected via web, but now I need to manage this language difference in a CLI Command: basically it means that when I run $this->urlGenerator->generate('app_cms_about') from the CLI, I expect to get the locale-based URL.
The user must pass the locale to the CLI Command to as an argument:
protected function configure()
{
$this->addArgument(
"app_language",
InputArgument::REQUIRED,
'The site to run on: `it`, `es`, ...');
}
Now I just need to set this somehow. I'd love to it with an event, but of course there is no getRequest() on the CLI event to set the locale on.
How do I set the locale on a Symfony 4 CLI Command?

Setting the locale does not make a lot of sense for a console application. What has a locale is the user request, and there is no request during a command line run.
But you want seems to be to be able to get URLs with the appropriate locale:
Then, straight from the docs:
When a route is localized, Symfony uses by default the current request locale. Pass a different '_locale' value if you want to set the locale explicitly
E.g.:
$aboutUrlIt = $this->router->generate('app_cms_about', ['_locale' => 'it']);
You mention in comments the setting default_locale, and that since you can change this it means console applications "have a locale set". But you are reaching the wrong conclussion from this: this setting is to set a default locale in case where no locale is set in the request. Or, for example, when there is no request, as in a command line application.
You cannot change that setting during runtime, because that setting is part of the compiled container, which is compiled before the application is run.

Following Symfony calls with xdebug I was able to find the solution I was looking for. I built a listener as the one I'm using via web:
services:
App\EventListener\CommandStartListener:
tags:
- { name: kernel.event_listener, method: onCommandStart, event: console.command }
<?php
namespace App\EventListener;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Routing\RouterInterface;
class CommandStartListener
{
protected RouterInterface $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onCommandStart(ConsoleCommandEvent $event) : ?string
{
// ... checks and stuff ...
try {
$lnParam = $event->getInput()->getArgument("app_language");
} catch( \Exception $ex) {
return null;
}
$this->router->getContext()->setParameter('_locale', $lnParam);
return $lnParam;
}
}
This makes any urlGenerator->generate() behave correctly.
hat tip to #yivi for pointing me into the right direction.

Related

Configure services based on env var in Symfony 5.4

I am working on a project that uses many env vars and I used php files (rather than yaml files or xml) to deal with configurations.
I am trying to this code :
// config/may_conf_name.php
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('my_extension', [
...
]);
// CLIENT_TYPE is an env variable
if ('http' === (string) env('CLIENT_TYPE')->string()) {
$services->alias(AbstractClient::class, HttpClient::class);
} else {
$services->alias(AbstractClient::class, AwsClient::class);
}
...
}
Unfortunately, env('CLIENT_TYPE')->string()) isn’t parsed
dump(
(string) env('CLIENT_TYPE')->string())
)
// output
^ "%env(string:CNT_CLIENT_TYPE)%"
Obviously, when I do this, it works for me :
if (isset($_ENV['CLIENT_TYPE']) && 'http' === $_ENV['CLIENT_TYPE']) {
....
}
So, my question is there a way to get my ‘CLIENT_TYPE’ env var like env('CLIENT_TYPE')->string()? Is it "clean" to do this with $_ENV['CLIENT_TYPE']?
Thank you!
Look at https://symfony.com/doc/current/service_container.html#binding-arguments-by-name-or-type
In your service.yaml or php in your case you can bind variable which will be autoimported inside autowired services.
In Yaml
bind:
$clientType: '%env(CLIENT_TYPE)%'
In php
->bind('$clientType', '%env(CLIENT_TYPE)%')
Then in any service where you want to use your environment var :
public function __construct(string $clientType)
$clientType will be autowired automatically with your binded configuration.
I don't know if this is exactly what you're looking for, dont hesitate to comment i will update my answer, but i hope it will at least give you an idea of what to do.
I asked for your SF version because autowiring is automatically working on 5.4 if you did not disabled it
If CLIENT TYPE is empty, it is not related to your symfony configuration. It mean that your env var is not properly written to your current env file.

Why am I unable to catch the Unexpected Alert Exception?

I am using a try catch block to catch an exception and I am unable to catch it as it still says:
In Exception.php line 155:
unexpected alert open: {Alert text : The form is not complete and has not been submitted yet. There is 1 problem with your submission.}
(Session info: chrome=73.0.3683.75)
(Driver info: chromedriver=2.41.578700 (2f1ed5f9343c13f73144538f15c00b370eda6706),platform=Linux 4.15.0-38-generic x86_64)
My feature file:
<?php
use Behat\Behat\Hook\Scope\AfterStepScope;
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\MinkExtension\Context\MinkContext;
use WebDriver\Exception\UnexpectedAlertOpen;
/**
* Defines application features from the specific context.
*/
class FeatureContext extends MinkContext implements Context
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
/**
* #Given I fill in the email field with :email
*/
public function iFillInTheEmailFieldWith($email)
{
dump($email);
$this->visit('/471w2222');
$page = $this->getSession()->getPage();
$page->find('xpath', '//*[#id="tfa_1111"]')->setValue($email);
}
/**
* #When I submit the form
*/
public function iSubmitTheForm()
{
try {
$page = $this->getSession()->getPage();
$page->find('xpath', '//*[#id="submit_button"]')->click();
}
catch (UnexpectedAlertOpen $e){
dd($e->getMessage());
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
}
}
}
The alert shows up :
$page->find('xpath', '//*[#id="submit_button"]')->click();
executes. But it is unable to catch it. Why?
As per the error message...
(Session info: chrome=73.0.3683.75)
(Driver info: chromedriver=2.41.578700 (2f1ed5f9343c13f73144538f15c00b370eda6706),platform=Linux 4.15.0-38-generic x86_64)
...the main issue is the incompatibility between the version of the binaries you are using as follows:
You are using chromedriver=2.41
Release Notes of chromedriver=2.41 clearly mentions the following :
Supports Chrome v67-69
You are using chrome=73.0
Release Notes of ChromeDriver v2.46 clearly mentions the following :
Supports Chrome v71-73
So there is a clear mismatch between the ChromeDriver v2.41 and the Chrome Browser v73.0
Solution
Upgrade ChromeDriver to current ChromeDriver v2.46 level.
Keep Chrome version between Chrome v73 levels. (as per ChromeDriver v2.45 release notes)
Clean your Project Workspace through your IDE and Rebuild your project with required dependencies only.
If your base Web Client version is too old, then uninstall it and install a recent GA and released version of Web Client.
Take a System Reboot.
Execute your #Test.
Always invoke driver.quit() within tearDown(){} method to close & destroy the WebDriver and Web Client instances gracefully.

Custom HandlerWrapper with MonologBundle

I am using Symfony 3.1 and I try to configure Monolog in such a way, that requests from the Googlebot are not logged. For this I wrote a UserAgentProcessorwhich already works as intended. In the next step I tried to write BotFilter which looks like this:
<?php
namespace AppBundle\Handler;
use Monolog\Handler\HandlerWrapper;
class FilterBotsHandler extends HandlerWrapper
{
/**
* {#inheritdoc}
*/
public function isHandling(array $record)
{
if (stripos($record['extra']['userAgent'], 'bot') !== false){
return false;
} else {
return $this->handler->isHandling($record);
}
}
}
This was inspired by the comments in the HandlerWrapper abstract class (take a look here).
Now I want to add that filter to my monolog yml-configuration. I tried adding it to my services but this was not possible as HandlerWrapper needs a Handler instance for its constructor. I researched how I could use the filter without a service but as far as I can see, the monolog bundle only accepts built-in types and the generic service type.
Now the question is: How can I use the filter in my configuration?
I am using Symfony 3.1 and I try to configure Monolog in such a way, that requests from the GoogleBot are not logged...
The quick way to prevent robots visiting your site is put these two lines into the /robots.txt file on your server. Create a robots.txt file in 'web' directory and paste the follows content:
User-agent: *
Disallow: /
https://support.google.com/webmasters/answer/6062608?hl=en&visit_id=1-636097099675465769-3677253464&rd=1
It's the recommended option when you need avoid access fully, meaning your sites will not longer be index by search engines and other bots. You don't need to configure/implement anything in your application to achieve it.
Now, if you need the bot to enter, but you don't want register it in logs. Instead of writing log files somewhere, some handlers are used to filter or modify log entries before sending them to other handlers. One powerful, built-in handler called fingers_crossed is used in the prod environment by default. It stores all log messages during a request but only passes them to a second handler if one of the messages reaches an action_level:
# app/config/config.yml
monolog:
handlers:
filter_for_errors:
type: fingers_crossed
# if *one* log is error or higher, pass *all* to file_log
action_level: error
handler: file_log
# now passed *all* logs, but only if one log is error or higher
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
Thus, in your prod.log file just will register the messages/requests that contains some error, so the bots don't have effect in this level.
More details about this http://symfony.com/doc/current/logging.html
What you try to do is not advisable, because the handler will depend from http request instead of log records, which will be out of context, however you can register its own handler in Symfony easily:
Let's create the custom handler class:
namespace AppBundle\Monolog\Handler;
use Monolog\Handler\AbstractHandler;
class StopBotLogHandler extends AbstractHandler
{
public function isBotRequestDetected()
{
// here your code to detect Bot requests, return true or false
// something like this:
// return isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/bot|crawl|slurp|spider/i', $_SERVER['HTTP_USER_AGENT']);
}
/**
* Checks whether the given record will be handled by this handler.
*
* This is mostly done for performance reasons, to avoid calling processors for nothing.
*
* Handlers should still check the record levels within handle(), returning false in isHandling()
* is no guarantee that handle() will not be called, and isHandling() might not be called
* for a given record.
*
* #param array $record Partial log record containing only a level key (e.g: array('level' => 100) for DEBUG level)
*
* #return bool
*/
public function isHandling(array $record)
{
return $this->isBotRequestDetected();
}
/**
* Handles a record.
*
* All records may be passed to this method, and the handler should discard
* those that it does not want to handle.
*
* The return value of this function controls the bubbling process of the handler stack.
* Unless the bubbling is interrupted (by returning true), the Logger class will keep on
* calling further handlers in the stack with a given log record.
*
* #param array $record The record to handle
*
* #return bool true means that this handler handled the record, and that bubbling is not permitted.
* false means the record was either not processed or that this handler allows bubbling.
*/
public function handle(array $record)
{
// do nothing, just returns true whether the request is detected as "bot", this will break the handlers loop.
// else returns false and other handler will handle the record.
return $this->isBotRequestDetected();
}
}
Whenever you add a record to the logger, it traverses the handler stack. Each handler decides whether it fully handled the record, and if so, the propagation of the record ends there.
Important: Read the phpdoc from isHandling() and handle() methods for more details.
Next, let's register the class as service "without tags":
# app/config/services.yml
services:
monolog.handler.stop_bot_log:
class: AppBundle\Monolog\Handler\StopBotLogHandler
public: false
Then, add its handler to handlers list:
# app/config/config_prod.yml
monolog:
handlers:
# ...
stopbotlog:
type: service
id: monolog.handler.stop_bot_log
priority: 1
Note the type property must be equal to service, id must be the service name before defined and priority must be greater than 0 to ensure that its handler will be executed before that any other handler.
When the GoogleBot performs a request to website application the stopbotlog handler stops all handlers after him and don't register any log message.
Remember it's not the recommended way to do that! According to your needs, implementing option 1 or 2 should be enough.
If you want ignore bot requests for handlers group, you can override the monolog.handler.group.class container parameter and override the group handler behavior:
namespace AppBundle\Handler;
use Monolog\Handler\GroupHandler;
class NoBotGroupHandler extends GroupHandler
{
public function isBotRequestDetected()
{
// here your code to detect Bot requests, return true or false
}
public function handle(array $record)
{
if ($this->isBotRequestDetected()) {
// ignore bot request for handlers list
return false === $this->bubble;
}
return parent::handle($record);
}
}
in your config_prod.yml or services.yml:
parameters:
monolog.handler.group.class: AppBundle\Handler\NoBotGroupHandler
That's it! Now, you can stop bot logs for custom handles list:
# config_prod.yml
monolog:
handlers:
grouped:
type: group
members: [main, console, chromephp]
Finally, if you have difficulty to analyze your logs files I recommend using this amazing tool: https://github.com/EasyCorp/easy-log-handler
It's quite a dirty trick, but if you really need it, you may make it like this.
Supposing you want to wrap a handler with a type stream:
Add a constructor in you FilterBotsHandler:
public function __constructor($path, $level, $bubble, $permissions) {
$this->handler = new Monolog\Handler\StreamHandler($path, $level, $bubble, $permissions);
}
And then redefine a parameter monolog.handler.stream.class:
parameters:
monolog.handler.stream.class: AppBundle\Handler\FilterBotsHandler
Make sure that this parameter will be defined after it was defined by MonologBundle.
That's it. Should work.
You may write CompilerPass in your AppBundle which adds configurator to monolog service. Such configurator can be also a request event listener which can replace all handlers dynamically on request and bot detection and push empty handlers array to Logger which can be hold on configurator call.
In other words configurator added to DI by CompilerPass and added to EventDispatcher as Listener to Kernel events which onRequest check User-Agent header looking for bot and then clears Monolog\Logger (passed in configurator) all handlers (or putting an NullHandler if empty handlers array fails).
DI configurator is only way to change your services during runtime which can be applied as service definition level. Such definition can be attached or detached if not needed and it doesn't really change anything in your application.

In Laravel 5, why is Request::root() different when called during phpunit test?

I defined a test which tests the creation of a user. The controller is set to redirect back to the same page on error (using validation through a generated App\Http\Requests\Request). This works correctly when manually clicking in a browser, but fails during a test. Instead of being redirected to:
http://localhost/account/create
The test redirects to (missing a slash):
http://localhostaccount/create
Neither of these urls are what I have setup in the .htaccess or in the $url variable in config/app.php. Which is (On OSX Yosemite):
http://~username/laravel_projects/projectname/public
I finally pinpointed the issue to have something to do with how the result of Request::root() is generated. Making a call to this outside of a test results in the expected value defined in .htaccess and $url. Inside the test it results in:
http://localhost
What configuration needs to change in order to get this function to return the correct value in both contexts?
I should also mention I made the painful upgrade from Laravel 4 to the current version 5.0.27.
****** UPDATE *******
I was able to figure out an acceptable solution/workaround to this issue!
In Laravel 5, FormRequests were introduced to help move validation logic out of controllers. Once a request is mapped to the controller, if a FormRequest (or just Request) is specified, this is executed before hitting the controller action.
This FormRequest by default handles the response if the validation fails. It attempts to construct a redirect based on the route you posted the form data to. In my case, possibly related to an error of mine updating from Laravel 4 to 5, this default redirect was being constructed incorrectly. The Laravel System code for handling the response looks like this:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
Notice how the returned redirect is NOT the same as calling Redirect::route('some_route'). You can override this response function by including use Response in your Request class.
After using Redirect::route() to create the redirect, the logic in my tests passed with the expected results. Here is my Request code that worked:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Request;
use Response;
class AccountRequest extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:50|email|unique:users',
'password' => 'required|min:6',
'password_confirmation' => 'required|same:password'
];
}
public function response(array $errors){
return \Redirect::route('account_create');
}
}
The important part is that I called Redirect::route instead of letting the default response code execute.
Override the response function in the FormRequest validation handler to force the redirect to be constructed with Redirect::route('named_route') instead of allowing the default redirect.
You need to change config/app.php file's url value. Default value is http://localhost
Doc from config/app.php
This URL is used by the console to properly generate URLs when using the Artisan command line tool. You should set this to the root of your application so that it is used when running Artisan tasks.
I know this isn't an exact answer to your question since it is not a configuration update that solves the problem. But I was struggling with a related problem and this seems to be the only post on the internet of someone dealing with something similar - I thought I'd put in my two cents for anyone that wants a different fix.
Please note that I'm using Laravel 4.2 at the moment, so this might have changed in Laravel 5 (although I doubt it).
You can specify the HTTP_HOST header when you're testing a controller using the function:
$response = $this->call($method, $uri, $parameters, $files, $server, $content);
To specify the header just provided the $server variable as an array like so:
array('HTTP_HOST' => 'testing.mydomain.com');
When I did the above, the value produced for my Request::root() was http://testing.mydomain.com.
Again, I know this isn't a configuration update to solve you're issue, but hopefully this can help someone struggling with a semi-related issue.
If you tried changine config/app.php and it did not help.
it is better to use $_ENV - global variable in phpunit.
say, you want Request::root() to return 'my.site'
but you cannot touch phpunit.xml
you can simply set an env param like so
$_ENV['APP_URL'] = 'my.site';
and call $this->refreshApplication(); in your unittest.
viola, your request()->root() is giving you my.site now.

Enable debug mode interactively in symfony

I'm using symfony 1.4 with Doctrine.
I'm trying to find a way to enable debug mode only if the current sfUser has a special debugger credential.
I already created a filter that deactivates the symfony debug bar if the sfUser has not this credential (the web_debug is set to true in my settings.yml file):
class checkWebDebugFilter extends sfFilter
{
public function execute($filterChain)
{
if(!$this->getContext()->getUser()->hasCredential('debugger'))
{
sfConfig::set('sf_web_debug', false);
}
$filterChain->execute();
}
}
The code of my index.php file is:
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false));
sfContext::createInstance($configuration)->dispatch();
The problem is, as the debug mode is hardcoded to false in my index.php, it is also disabled for debuggers; therefore the Web debug bar does not show Doctrine statements nor timing indications.
Is there a way to enable debug mode only if the current sfUser has a precise credential?
I tried to add sfConfig::set('sf_debug', true); to my checkWebDebugFilter::execute() method but as the filter is executed after Doctrine statements, they are not recorded.
I also tried to add session_start(); in my index.php file, then browsing through the $_SESSION variable to check whether the current user has the debugger credential, but it did not work (and it was not in the spirit of symfony either).
Thanks in advance for your answers.
When you pass the debug parameter in the index.php file, it actually is passed down to the sfApplicationConfiguration class of your application. In your case it can be found in the /apps/frontend/config/frontendConfiguration.class.php file. frontendConfiguration class extends sfApplicationConfiguration, and here you can add your code.
Debug parameter is stored in a protected variable of this class, so you wont be able to change it from filter, but you can create a function for example:
setDebug($mode) {
$this->debug = $mode;
}
And call it in your filter:
$this->context->getConfiguration()->setDebug(true);
You also could override isDebug() function in frontendConfiguration class, because that is used in the initConfiguration() function to initialize timing indicators and other debugging stuff.
if ($this->isDebug() && !sfWebDebugPanelTimer::isStarted())
{
sfWebDebugPanelTimer::startTime();
}
But you won't be able to check user permissions here, as sfUser class won't be initialized in this stage yet. But you can check $_COOKIES or $_SESSION global variables for a value that you can set when user is logging in. Or you can call sfWebDebugPanelTimer::startTime() in your Filter, but will miss a few microseconds.
I have not tested this, but that's how I would do it.
Try this
if you want to enable web_debug panel (dev) mode then
http://host_url/frontend_dev.php
or
write in your index.php 'frontend','dev',true.
require_once(dirname(FILE).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'dev', true));
sfContext::createInstance($configuration)->dispatch();

Categories