Symfony: Access $_POST variable from service without going scope request - php

I need to access a $_POST variable from a service and I don't want to pass request as a paramter as I think this forces me to use scope: request in the service and I have bad memories from this scope from the past, as this forces a service instance per request, instead of one instance per application.
The thing is that $_POST works all right, but as symfony best practices recommends to avoid using php primitives I ask if there's a better way to do it (avoiding scope: request in service)

If you are using 2.4+ you can use the request_stack, see here.
You would use it like..
services.yml
your.service:
class: FQCN\To\Your\Service
arguments:
- #request_stack
FQCN\To\Your\Service
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class Service
{
/**
* #var RequestStack
*/
private $requestStack;
/**
* #var Request
*/
private $request;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function yourCall()
{
$param = $this->getRequest()->request->get('your-post-parameter');
//...
}
/**
* Get current request object
*
* #return Request
*/
private function getRequest()
{
if (null === $this->request) {
$this->request = $this->requestStack->getCurrentRequest();
}
return $this->request;
}
}

I don't see any problem with doing it like below which is what I did some time ago. You're not injecting whole Request.
CONTROLLER
public function searchByGetAction(Request $request)
{
//.......
$results = $this->productSearchService->findWithSimpleArray(
$request->query
);
//.......
}
public function searchByPostAction(Request $request)
{
//.......
$results = $this->productSearchService->findkWithNestedArray(
json_decode($request->getContent(), true)
);
//.......
}
SERVICE
public function findWithSimpleArray(ParameterBag $searchParameters)
{
$name = $searchParameters->get('name');
$surname = $searchParameters->get('surname');
//.......
}
public function findWithNestedArray($searchParameters = [])
{
$name = isset($searchParameters['name']) ? $searchParameters['name'] : null;
$surname = isset($searchParameters['surname']) ? $searchParameters['surname'] : null;
//.......
}

Related

Type hinting additional class on a Laravel listener class

I have my event listener defined as this:
use App\Events\LeadCreated;
use App\Services\LeadService;
class NewLeadNotifyProspectListener
{
/**
* Handle the event.
*
* #param \App\Events\LeadCreated $event
* #param App\Services\LeadService $leadService
* #return void
*/
public function handle(LeadCreated $event, LeadService $leadService)
{
$leadService->notifyProspect($event->lead);
}
}
And my event
use App\Models\Lead;
class LeadCreated
{
public Request $request;
public Lead $lead;
public function __construct(Lead $lead, Request $request)
{
$this->request = $request;
$this->lead = $lead;
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
And I'm calling it in my controller like this:
LeadCreated::dispatch($lead, $request);
The error I'm receiving:
Too few arguments to function App\Listeners\NewLeadNotifyProspectListener::handle(), 1 passed in /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php on line 424 and exactly 2 expected
I wonder why I'm not able to type-hint LeadService in my listener? What's the proper way to include that class in my listener?
You are loading a dependency, you have to follow Dependency Injection rules.
In Laravel you can use Dependency Injection in Controllers like that, because that's build by Laravel/Symfony.
Add your dependency in the constructor instead.
use App\Events\LeadCreated;
use App\Services\LeadService;
class NewLeadNotifyProspectListener
{
private LeadService $leadService;
public function __construct(LeadService $leadService)
{
$this->leadService = $leadService;
}
/**
* Handle the event.
*
* #param \App\Events\LeadCreated $event
* #return void
*/
public function handle(LeadCreated $event)
{
$this->leadService->notifyProspect($event->lead);
}
}
If you have setup the NewLeadNotifyProspectListener correctly, Dependency Injection in Laravel will inject that service in the constructor, just like it would have done in a Controller

Call to a member function on null, phone validation service

PHP with Symfony framework:
First of all before the context:
My input form is being built by form builder. Nothing is wrong there. So that is not the problem
I am making a sms validator system. I have a controller, and 2 services(validatorservice, smsapi(for api call)) Now my validatorservice looks like this:
class ValidatorService
{
public function validate($telefoonnummer)
{
$pregpatternNL = '(^\+[0-9]{2}|^\+[0-9]{2}\(0\)|^\(\+[0-9]{2}\)\(0\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\-\s]{10}$)';
if (!preg_match($pregpatternNL, $telefoonnummer)) {
return false;
} else {
return true;
}
}
}
Then my homecontroller:
use App\Service\ValidatorService;
class HomeController extends AbstractController
{
/** #var SmsApi */
private $smsApi;
/** #var validatorService */
private $validatorService;
public function __construct1(SmsApi $smsApi, Validatorservice
$validatorService)
{
$this->smsApi = $smsApi;
$this->validatorService = $validatorService;
}
/**
* #Route("/")
* #Template()
*
* #param Request $request
*
* #return array
*/
public function indexAction(Request $request)
{
$form = $this->createForm(
SmsLogFormType::class,
new SmsLog(),
[
'method' => 'post',
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var SmsLog $smslog */
$formData = $form->getData();
try {
$telefoonnummer = $formData->getTel();
$telefoonnummer = preg_replace('/[^0-9]/', '', $telefoonnummer);
$validatorservices = $this->validatorService-
>validate($telefoonnummer);
if ($validatorserviceres === false) {
$this->addFlash(
'notice',
'telefoonnummer onjuist formaat'
);
exit;
} else {
blablabla
}
Now whatever i try i get the error :
Call to a member function validate() on null
At first i thought maybe its something with the constructor names, but found online that that doesn't matter at all (also i didn't receive any code errors there)
Then i tried adding echo's to the if statement in my service. Maybe return true or false is seen as null but this doesn't work either.
I guess it's because of the number of arguments per constructor. If you define multiple constructors for a class, they should have different argument counts.
What you could do instead is to check whether or not the object you received is part of the wanted class/classes.
Or create static functions that instatiate the class with different object types.
EDIT
Use the default autowiring mechanisms:
private $smsApi;
private $validatorService;
public function __construct(SmsApi $smsApi, ValidatorService $validatorService)
{
$this->smsApi = $smsApi;
$this->validatorService = $validatorService;
}
It should work as intended if you change your Code to this :
/** #var SmsApi */
private $smsApi;
private $validatorService;
public function __construct(SmsApi $smsApi, ValidatorService $validatorService)
{
$this->validatorService = $validatorService;
$this->smsApi = $smsApi;
}
__construct1 and __construct2 are not native functions of php, so when the class is loaded, the constructors are not invoking and validatorService/smsApi are not being set (so they are null). The native function is called __construct.
/** #var SmsApi */
private $smsApi;
private $validatorService;
public function __construct(SmsApi $smsApi, ValidatorService $validatorService)
{
$this->smsApi = $smsApi;
$this->validatorService = $validatorService;
}
Or if doest not work, inject the services as arg in
public function indexAction(Request $request)
so...
public function indexAction(Request $request,SmsApi $smsApi, ValidatorService $validatorService)
and use $validatorService->validate();

Load Symfony's parameter with compiler pass from custom service

According to this question How to load Symfony's config parameters from database (Doctrine) I have a similar problem. I need to set the parameter dynamically and I want to provide data from another custom service.
So, I have Event Listener which setting current account entity (by sub-domain or currently logged user)
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use AppBundle\Manager\AccountManager;
use Palma\UserBundle\Entity\User;
/**
* Class CurrentAccountListener
*
* #package AppBundle\EventListener
*/
class CurrentAccountListener {
/**
* #var TokenStorage
*/
private $tokenStorage;
/**
* #var EntityManager
*/
private $em;
/**
* #var AccountManager
*/
private $accountManager;
private $baseHost;
public function __construct(TokenStorage $tokenStorage, EntityManager $em, AccountManager $accountManager, $baseHost) {
$this->tokenStorage = $tokenStorage;
$this->em = $em;
$this->accountManager = $accountManager;
$this->baseHost = $baseHost;
}
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$accountManager = $this->accountManager;
$accountManager->setCurrentAccount( $this->getCurrentAccount($request) );
}
private function getCurrentAccount($request){
if($this->getCurrentAccountByLoggedUser()) {
return $this->getCurrentAccountByLoggedUser();
}
if($this->getCurrentAccountBySubDomain($request) ) {
return $this->getCurrentAccountBySubDomain($request);
}
return;
}
private function getCurrentAccountBySubDomain($request) {
$host = $request->getHost();
$baseHost = $this->baseHost;
$subdomain = str_replace('.'.$baseHost, '', $host);
$account = $this->em->getRepository('AppBundle:Account')
->findOneBy([ 'urlName' => $subdomain ]);
if(!$account) return;
return $account;
}
private function getCurrentAccountByLoggedUser() {
if( is_null($token = $this->tokenStorage->getToken()) ) return;
$user = $token->getUser();
return ($user instanceof User) ? $user->getAccount() : null;
}
}
services.yml
app.eventlistener.current_account_listener:
class: AppBundle\EventListener\CurrentAccountListener
arguments:
- "#security.token_storage"
- "#doctrine.orm.default_entity_manager"
- "#app.manager.account_manager"
- "%base_host%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
And very simply account manager with setter and getter only. If I need access to current account I call
$this->get('app.manager.account_manager')->getCurrentAccount();
Everything works fine.
Now I am trying set some parameter from my service with compiler pass
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParametersCompilerPass implements CompilerPassInterface {
const ACCOUNT_MANAGER_SERVICE_ID = 'app.manager.account_manager';
public function process(ContainerBuilder $container) {
if(!$container->has(self::ACCOUNT_MANAGER_SERVICE_ID)) {
return;
}
$currentAccount = $container->get(self::ACCOUNT_MANAGER_SERVICE_ID)
->getCurrentAccount();
$container->setParameter(
'current_account', $currentAccount
);
}
}
AppBundle.php
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\ParametersCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ParametersCompilerPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}
I got current_account as null every time, no matter what PassConfig I use. Any ideas?
Thank you for your attention.
Compilation pass are executed when you run Symfony for the first time (CLI command or first http request). Once the cache is build (compiled) this code it never gets executed again.
Solution with parameters [I do not recommend this]
If your parameter can change from one to another HTTP Request you should not use a parameter as some services may be initialized before your parameter is ready and others after. Although if this is the way you want to go, you can add an event that listens to the kernel request and modify/set the parameter there. Have a look at https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-event-table
Current account in the user / session
If currentAccount depends on the user logged, why you do not store that info in the user or session and access to it from your services?

Getting route options inside ExceptionListener

I am working on an ExceptionListener and for some controllers I want errors to be formatted as json responses. I thought I would define an option in #Route annotation and then use it in ExceptionListener:
/**
* #Route("/route/path", name="route_name", options={"response_type": "json"})
*/
and:
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// ...
}
}
but GetResponseForExceptionEvent doesn't contain any info about matched route. Is there a way to get the options array inside ExceptionListener?
thanks.
You should be able to retrieve the route name from the attribute request with
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
then, if you inject the router service into your class, you can get the instance of the route with
$route = $this->router->getRouteCollection()->get($routeName);
finally
$options = $route->getOptions();
echo $options['response_type']
use Symfony\Component\Routing\RouterInterface;
class ExceptionListener
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$route = $this->router->getRouteCollection()->get(
$request->attributes->get('_route')
);
$options = $route->getOptions();
// $options['response_type'];
}
}

Symfony2 get to the access_control parameters located in the security.yml

I'm trying to get the access_control parameters which are located in my security.yml as an array in my custom service.
Just like with getting the role_hierarchy parameters I thought it would work with the following code:
$accessParameters = $this->container->getParameter('security.access_control');
Unfortunately this was not the case. Can someone tell how to get the parameters?
There's no way to get the access_control parameter from the container.
This is because this parameter is only used to create request matchers which will be registered as AccessMap later given in the AccessListener, and then are left over without registering it into the container.
You can try something hacky to get these matchers back by getting them like
$context = $this->get("security.firewall.map.context.main")->getContext();
$listener = $context[0][5];
// Do reflection on "map" private member
But this is kind of an ugly solution.
Another way I can see on how to get them is to parse again the security file
use Symfony\Component\Yaml\Yaml;
$file = sprintf("%s/config/security.yml", $this->container->getParameter('kernel.root_dir'));
$parsed = Yaml::parse(file_get_contents($file));
$access = $parsed['security']['access_control'];
If you want to register this configuration into a service, you can do something like
services.yml
services:
acme.config_provider:
class: Acme\FooBundle\ConfigProvider
arguments:
- "%kernel.root_dir%"
acme.my_service:
class: Acme\FooBundle\MyService
arguments:
- "#acme.config_provider"
Acme\FooBundle\ConfigProvider
use Symfony\Component\Yaml\Yaml;
class ConfigProvider
{
protected $rootDir;
public function __construct($rootDir)
{
$this->rootDir = $rootDir;
}
public function getConfiguration()
{
$file = sprintf(
"%s/config/security.yml",
$this->rootDir
);
$parsed = Yaml::parse(file_get_contents($file));
return $parsed['security']['access_control'];
}
}
Acme\FooBundle\MyService
class MyService
{
protected $provider;
public function __construct(ConfigProvider $provider)
{
$this->provider = $provider;
}
public function doAction()
{
$access = $this->provider->getConfiguration();
foreach ($access as $line) {
// ...
}
}
}
Necro, but still relevant. This is an improvement on Touki's answer above, where we don't reparse the access_control definitions, but rather use the already configured security token, firewall and access map to work out the answer.
.../services.yml
...
My\Application\AuthenticationBundle\Security\AccessControlHelper:
class: My\Application\AuthenticationBundle\Security\AccessControlHelper
arguments:
$securityContext: "#security.context"
$firewall: '#security.firewall.map'
$accessDecisionManager: '#security.access.decision_manager'
$accessMap: '#security.access_map'
...
src/My/Application/AuthenticationBundle/Security/AccessControlHelper.php
declare(strict_types=1);
namespace My\Application\AuthenticationBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\FirewallMapInterface;
class AccessControlHelper
{
/**
* #var SecurityContextInterface
*/
protected $securityContext;
/**
* #var FirewallMapInterface
*/
protected $firewallMap;
/**
* #var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
/**
* #var AccessMapInterface
*/
protected $accessMap;
public function __construct(
SecurityContextInterface $securityContext,
FirewallMapInterface $firewallMap,
AccessDecisionManagerInterface $accessDecisionManager,
AccessMapInterface $accessMap
)
{
$this->securityContext = $securityContext;
$this->firewallMap = $firewallMap;
$this->accessDecisionManager = $accessDecisionManager;
$this->accessMap = $accessMap;
}
public function isRequestAccessible(Request $request): bool
{
$token = $this->securityContext->getToken();
if (!$token || false == $token->isAuthenticated()) {
return false;
}
list($listeners) = $this->firewallMap->getListeners($request);
if ($listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof AccessListener) {
/**
* Logic here is much inspired by the AccessListener->handle(...) method.
*/
list($attributes) = $this->accessMap->getPatterns($request);
if (null === $attributes) {
continue;
}
return boolval($this->accessDecisionManager->decide($token, $attributes, $request));
}
}
}
return true;
}
public function isUriAccessible(string $uri)
{
return $this->isRequestAccessible(Request::create($uri));
}
}
Sample usage:
use My\Application\AuthenticationBundle\Security\AccessControlHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
$container = ...; // #var ContainerInterface
$accessControlHelper = $container->get(AccessControlHelper::class);
$accessControlHelper->isRequestAccessible(new Request("/foo"));
$accessControlHelper->isUriAccessible("/foo");

Categories