Symfony dependency injection in twig extension - symfony

Ok, I was trying to create twig extension with dependencies on other service (security.context) and got some troubles. So, here is my service declaration:
acme.twig.user_extension:
class: Acme\BaseBundle\Twig\UserExtension
arguments: ["#security.context"]
tags:
- { name: twig.extension }
and here's my class
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
protected $context;
public function __construct(SecurityContext $context){
$this->context = $context;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->context->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}
Finally, I have an error:
FatalErrorException: Error: Call to a member function getUser() on a non-object in \src\Acme\BaseBundle\Twig\UserExtension.php line 27
I guess that security.context service is not initialized yet, then i get an error.
Could anyone tell, please, is there are ways to load service manually, or any better solutions for an issue?
Thanks a lot.
I use Symfony 2.5.*
UPD:
I've also found this notice in symfony docs
Keep in mind that Twig Extensions are not lazily loaded. This means that there's a higher chance that you'll get a CircularReferenceException or a ScopeWideningInjectionException if any services (or your Twig Extension in this case) are dependent on the request service. For more information take a look at How to Work with Scopes.
Actually, I have no idea about how to do it correct..

You are calling $this->getAbcData() when constructing Twig_SimpleFilter. But you have to pass a callable as argument.
public function getFunctions() {
return array (
'getAbcData' => new \Twig_SimpleFunction( 'getAbcData', array( $this, 'getAbcData' ))
);
}
Leo is also right. You should check first if getToken() is returning an object before trying getToken()->getUser().
You can also pass the user to the function as a parameter in twig: {{ getAbcData(app.user) }}. This way the function is more generic and could be used for any user, not just the currently logged in one.

This should probably work. The error message means that getToken() is not an object so you have to test if getToken() is an object before testing if getUser() is also is an object.
public function getAbcData()
{
$token = $this->context->getToken();
if (!is_object($token) || !is_object($token->getUser())) {
return null;
}
return array(
'data_array' => $user->getData(),
);
}

You need to change your twig extension to have the container not the security context passed into the constructor.
Twig_Extensions are special in that the normal rule of don't pass in the container but instead pass in only what you need often doesn't apply as it causes problems due to scope issues.
So change your extension to be like this.
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container){
$this->container = $container;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->container->get('security.context')->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}

Related

Is this approach of returning exceptions is correct?

Is my approach of return Exceptions and user object from the registration service to the controller and returning the result as json correct ? I am making RESTful API and I am not sure if I am on the right way. My controller:
<?php
namespace App\Controller;
use App\Service\RegistrationService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;
class RegistrationController
{
public function __construct(
private RegistrationService $registrationService,
)
{
}
#[Route(path: '/api/v1/register', methods: 'POST')]
public function register(Request $request): JsonResponse
{
$registeredUser = $this->registrationService->register($request->request->all());
if ($registeredUser instanceof \Exception) {
return new JsonResponse([
'type' => 'error',
'content' => $registeredUser->getMessage(),
'status' => $registeredUser->getCode()
]);
}
// Here will serialize the $registeredUser object
// and will return it as content in the JsonResponse
return new JsonResponse([
'type' => 'success',
'content' => $serializedUser,
'status' => 200
]);
}
}
And my registration service:
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class RegistrationService
{
public function __construct(
private UserRepository $userRepository,
private ValidatorInterface $validator
)
{
}
public function register(array $userData): User|\Exception
{
if (!$this->checkIfUserExists($userData['email'])) {
$user = new User(
$userData['email'],
$userData['password'],
$userData['referralCode']
);
$errors = $this->validator->validate($user);
if (count($errors) > 0) {
return new \Exception(message: (string)$errors, code: 400);
}
$this->userRepository->add($user, true);
return $user;
}
return new \Exception(message: 'User with this email already exists!', code: 409);
}
private function checkIfUserExists(string $email): bool
{
return $this->userRepository->findOneBy(['email' => $email]) !== null;
}
}
Will be thankful or any advises!
Yes that works. The best one I have seen is to have a middleware which has mapping between more specific error messages and less specific error messages. Before returning the exception to the user the middleware logs it and sends less specific errror messages to end user. Depending on your api, you may not want end user to see specific error messages but still might want to log them

Drupal: "Error: Class not found" when calling a function from a controller within submitForm()

I'm trying to do some stuff while submitting a form in a custom module. Some of that is done by calling a function from a controller. That's when i get:
Error: Class 'Drupal\ice_cream\Controller\OrderController' not found in Drupal\ice_cream\Form\OrderForm->submitForm() (line 77 of modules\custom\ice_cream\src\Form\OrderForm.php).
As far as I can tell the namespaces aren't wrong? Or is that not related to this error?
This is how my OrderForm.php and submitForm() looks like:
<?php
namespace Drupal\ice_cream\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ice_cream\Controller\OrderController;
/**
* Implements the order form.
*/
class OrderForm extends FormBase {
... (omitted code for getFormid and buildForm)
public function submitForm(array &$form, FormStateInterface $form_state) {
//Check if the order is ice or waffles.
if($form_state->getValue('foodType') == 'ice'){
//Save order to the DB.
OrderController::saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
... (more code)
}
}
}
This is how the controller looks like:
<?php
namespace Drupal\ice_cream\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Database;
/**
* Order controller for interacting (insert, select,...) with the ice cream table in the DB.
*/
class OrderController extends ControllerBase {
/**
* Saves an order for either Ice or Waffles with their options (tast or toppings).
*/
public function saveOrder($foodType, $options) {
$connection = Database::getConnection();
//Check if ice or waffles (to only insert the right field with $options).
if($foodType == "ice"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => $options,
'toppings' => "",
])
->execute();
return true;
}elseif($foodType == "waffles"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => "",
'toppings' => $options,
])
->execute();
return true;
}
}
}
Try using the below code:
$obj= new OrderController;
$obj->saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
Solved:
Just solved it with my mentor. The code was more or less correct, still needed to make my functions static in the OrderController and also I made a stupid error of forgetting the .php extension in my filename when I created it with the 'touch' terminal command...

Symfony Undefined index

I am creating an application that fetches and search for product name from different sources (DB, XML, JSON, ...)(for this code Im testing only with the DB), my idea was to create an interface for that.
I created the interface ProductRepositoryInterface and the class DoctrineProductRepository then I declared them both as services.
In my controller, I call the search function with the product name as param.
Here is my interface ProductRepositoryInterface :
namespace Tyre\TyreBundle\Repository;
interface ProductRepositoryInterface
{
function search(string $needle);
}
My interface DoctrineProductRepository:
namespace Tyre\TyreBundle\Repository;
class DoctrineProductRepository implements ProductRepositoryInterface
{
public function __constructor(EntityManager $em)
{
$this->em = $em;
}
public function search(string $needle)
{
$repository = $this->em->getRepository('TyreTyreBundle:Products');
$query = $repository->createQueryBuilder('u')
->where("u.name LIKE '%".$needle."%' or u.manufacturer LIKE '%".$needle."%'")
->getQuery();
return $query->getArrayResult();
}
}
My Service.yml
services:
Tyre\TyreBundle\Repository\DoctrineProductRepository:
class: Tyre\TyreBundle\Repository\DoctrineProductRepository
Tyre\TyreBundle\Repository\ProductRepositoryInterface:
class: Tyre\TyreBundle\Repository\ProductRepositoryInterface
and finally my controller :
namespace Tyre\TyreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Tyre\TyreBundle\Repository\DoctrineProductRepository;
use Tyre\TyreBundle\Repository\ProductRepositoryInterface;
class DefaultController extends Controller
{
public function indexAction()
{
return $this->render('TyreTyreBundle:Default:search.html.twig');
}
public function searchAction(Request $request) {
$repositoryMap = [
'db' => DoctrineProductRepository::class,
];
$serviceName = $repositoryMap[$request->get('db')]; /***This is Line 56 ***/
/** #var ProductRepositoryInterface */
$repository = $this->get($serviceName);
$results = $repository->search($request->get('search_for'));
return $this->render('TyreTyreBundle:Default:detail.html.twig', array('results' => $results));
}
public function detailAction()
{
//forward the user to the search page when he tries to access directly to the detail page
return $this->render('TyreTyreBundle:Default:search.html.twig');
}
}
But I get an error :
EDIT
When I try http://localhost:8000/search?db=db , I get other error (I var_dumped $repositoryMap) :
click to view
Am I missing anything?
The reason for your 'ContextErrorException' is :
$request->get('search_for')
is empty because you are passing nothing in the url for that key. Pass 'search_for' also in addition with 'db' like:
http://localhost:8000/search?db=db&search_for=myvalue

2 Deprecations in my Symfony 3 Project

I have 2 deprecations in my Symfony 3 project when try to define a custom service in service.yml that I would like to solve but I don't get the way...
This my code FollowingExtension.php
<?php
namespace AppBundle\Twig;
use Symfony\Bridge\Doctrine\RegistryInterface;
class FollowingExtension extends \Twig_Extension {
protected $doctrine;
public function __construct(RegistryInterface $doctrine) {
$this->doctrine = $doctrine;
}
public function getFilters() {
return array(
new \Twig_SimpleFilter('following', array($this, 'followingFilter'))
);
}
public function followingFilter($user, $followed){
$following_repo = $this->doctrine->getRepository('BackendBundle:Following');
$user_following = $following_repo->findOneBy(array(
"user" => $user,
"followed" => $followed
));
if(!empty($user_following) && is_object($user_following)){
$result = true;
}else{
$result = false;
}
return $result;
}
public function getName() {
return 'following_extension';
}
}
And this is my services.yml:
following.twig_extension:
class: AppBundle\Twig\FollowingExtension
public: false
arguments:
$doctrine: "#doctrine"
tags:
- { name: twig.extension }
I would appreciate the help they gave me in trying to solve my problem.
First problem solution
It looks like because of registering services with duplicate name, as declared in your class:
`return 'following_extension';`
Make sure that you only have one twig service named following_extension. If you are sure that only one twig service named following_extension, you probably register more than one service using that class.
Second problem solution
Replace
`use Symfony\Bridge\Doctrine\RegistryInterface;`
with
`use Doctrine\Common\Persistence\ManagerInterface;`
and also replace
`public function __construct(RegistryInterface $doctrine) {`
with
`public function __construct(ManagerInterface $doctrine) {`
Finally solved the problem and i want share the info...
Reading the documentation of Symfony found this How to Create Service Aliases and Mark Services as Private and them I declared my service this way:
in service.yml:
following.twig_extension: '#AppBundle\Twig\FollowingExtension'
and FollowingExtension.php
<?php
namespace AppBundle\Twig;
use Doctrine\Common\Persistence\ManagerRegistry;
class FollowingExtension extends \Twig_Extension {
private $managerRegistry;
public function __construct(ManagerRegistry $managerRegistry) {
$this->managerRegistry = $managerRegistry;
}
public function getFilters() {
return array(
new \Twig_SimpleFilter('following', array($this, 'followingFilter'))
);
}
public function followingFilter($user, $followed){
$following_repo = $this->managerRegistry->getRepository('BackendBundle:Following');
$user_following = $following_repo->findOneBy(array(
"user" => $user,
"followed" => $followed
));
if(!empty($user_following) && is_object($user_following)){
$result = true;
}else{
$result = false;
}
return $result;
}
}
Thanks for helping me and I'm sorry if my english is bad.

How to use authorizationChecker inside a trait

I have a trait that checks if a user is logged in or not and number of attempts to a specific location.
This trait I am trying to use inside a FormType in order to display a captcha after a number of attempts.
Inside getIpOrUserId() I am trying to check if user is logged in $this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY') but it returns an error
Attempted to call an undefined method named "get" of class.
I don't think that it is possible to create a Trait as a Service so I can inject the security object.
Is there a way to achieve this?
Trait
<?php
Trait CheckAttempts {
public function getTryAttempts()
{
if ($this->getIpOrUserId() == null) {
return false;
} else {
$attempts = $this->getDoctrine()
->getRepository('SiteBundle:LoginAttempts')
->findOneByIpOrUserId($this->getIpOrUserId());
}
return $attempts->getAttempts();
}
protected function getIpOrUserId()
{
//get logged in user
//if ($this->authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
$ipOrUserId = $this->get('security.token_storage')->getToken()->getUser()->getId();
} else {
$ipOrUserId = $this->container->get('request_stack')->getCurrentRequest()->getClientIp();
}
return $ipOrUserId;
}
FormType
class RegisterType extends AbstractType
{
use FormSendLimitTrait;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
var_dump($this->getTryAttempts());
$form->add('captcha', 'captcha',
'label' => 'site.captcha'
]);
/*if ($attempts->getAttempts() > 6) {
$form->add('captcha', 'captcha', [
'label' => 'site.captcha'
]);
}*/
})
get method works only if you extend Symfony\Bundle\FrameworkBundle\Controller\Controller class, usually it is used inside your controller classes. And it returns only $this->container->get($id), nothing else, this means it returns Symfony\Component\DependencyInjection\Container class. You should inject security.authorization_checker service into your class (or other services which you want), or even whole service_container service (but it's not recommended).
Example:
class MyClass
{
private $securityChecker;
public function __construct(Symfony\Component\Security\Core\Authorization\AuthorizationChecker $securityChecker)
{
$this->securityChecker = $securityChecker;
}
...
}
services.yml
services:
my_class_service:
class: Acme\DemoBundle\MyClass
arguments:
- #security.authorization_checker
But in your case you should not use traits like you're doing.

Resources