Symfony : 6.1.4
EasyAdmin :4.3.5
I try to display the products created by the logged in user. By default, all products are displayed regardless of the author.
ProductCrudController.php
class ProductCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Product::class;
}
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
$response = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
$response->andWhere('entity.creator = :creator')->setParameter('creator', ???????);
return $response;
}
[...]
}
Problem : I need to get the id of the user who created the product.
But I can't get it back via $_SESSION :
{{dump(app.user)}}
One solution I can think of is to use app.user.id in ProductCrudController.php but it is a php file. Do you have any leads to achieve this or other ideas?
You should be able to use $this->getUser() in your controller and retrieve the current logged in user and use it to filter your products.
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
return parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters)
->andWhere('entity.creator = :creator')
->setParameter('creator', $this->getUser());
}
Related
I have a CrudController for my entity, Participant. I want to add a custom action, sendAcknowledgementEmail. The EasyAdmin docs doesn't mention anything about the custom function parameters or return values.
I have the following code
public function configureActions(Actions $actions): Actions
{
$send_acknowledgement_email = Action::new('sendAcknowledgementEmail', 'Send Acknowledgement Email', 'fa fa-send')
->linkToCrudAction('sendAcknowledgementEmail');
return $actions
->add(Crud::PAGE_INDEX, $send_acknowledgement_email)
->add(Crud::PAGE_EDIT, $send_acknowledgement_email)
;
}
public function sendAcknowledgementEmail() //Do I need parameters?
{
//How do I get the Entity?
//What should I return?
}
So far, EasyAdmin detects the custom function but I get an error "The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned null. Did you forget to add a return statement somewhere in your controller?"
How do I continue from here?
The v3.x of the bundle is quite new and the documentation is not perfect yet.
Based on Ceochronos answer, here is my implementation for a clone action.
public function configureActions(Actions $actions): Actions
{
$cloneAction = Action::new('Clone', '')
->setIcon('fas fa-clone')
->linkToCrudAction('cloneAction');
return $actions
->add(Crud::PAGE_INDEX, $cloneAction);
}
public function cloneAction(AdminContext $context)
{
$id = $context->getRequest()->query->get('entityId');
$entity = $this->getDoctrine()->getRepository(Product::class)->find($id);
$clone = clone $entity;
// custom logic
$clone->setEnabled(false);
// ...
$now = new DateTime();
$clone->setCreatedAt($now);
$clone->setUpdatedAt($now);
$this->persistEntity($this->get('doctrine')->getManagerForClass($context->getEntity()->getFqcn()), $clone);
$this->addFlash('success', 'Product duplicated');
return $this->redirect($this->get(CrudUrlGenerator::class)->build()->setAction(Action::INDEX)->generateUrl());
}
After browsing through the EasyAdmin AbstractCrudController I came up with the following working code.
In order to get the current object you need the parameter AdminContext
For my use case I want to return to the CrudController index action, so for that I can do a redirect.
Note: you need to inject the CrudUrlGenerator service in your constructor controller.
public function sendAcknowledgementEmail(AdminContext $context)
{
$participant = $context->getEntity()->getInstance();
// Your logic
$url = $this->crudUrlGenerator->build()
->setController(ParticipantCrudController::class)
->setAction(Action::INDEX)
->generateUrl();
return $this->redirect($url);
}
My current function looks like this:
public function sendAcknowledgementEmail(AdminContext $context)
{
$participant = $context->getEntity()->getInstance();
$participant->sendAcknowledgementEmail();
$this->addFlash('notice','<span style="color: green"><i class="fa fa-check"></i> Email sent</span>');
$url = $this->crudUrlGenerator->build()
->setController(ParticipantCrudController::class)
->setAction(Action::INDEX)
->generateUrl();
return $this->redirect($url);
}
My current working code
<?php
namespace App\Controller\Admin;
use App\Service\WebinarService;
use EasyCorp\Bundle\EasyAdminBundle\Router\CrudUrlGenerator;
use Symfony\Contracts\Translation\TranslatorInterface;
// ...
class ParticipantCrudController extends AbstractCrudController
{
private CrudUrlGenerator $crudUrlGenerator;
private WebinarService $webinar_service;
private TranslatorInterface $translator;
public function __construct(CrudUrlGenerator $crudUrlGenerator, WebinarService $webinar_service, TranslatorInterface $translator)
{
$this->crudUrlGenerator = $crudUrlGenerator;
$this->webinar_service = $webinar_service;
$this->translator = $translator;
}
// ...
public function sendAcknowledgementEmail(AdminContext $context): Response
{
$participant = $context->getEntity()->getInstance();
try {
$this->webinar_service->sendAcknowledgementEmail($participant);
$this->addFlash('notice', 'flash.email.sent');
} catch (Exception $e) {
$this->addFlash('error', $this->translator->trans('flash.error', ['message' => $e->getMessage()]));
}
$url = $this->crudUrlGenerator->build()
->setController(ParticipantCrudController::class)
->setAction(Action::INDEX)
->generateUrl()
;
return $this->redirect($url);
}
}
My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}
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';
}
}
How you can access to the context user (i.e.: $this->getUser()) from inside a repository?
Something like:
public function findInviteRequestByUser(\Fundacity\UserBundle\Entity\User $user)
{
$contextUser = $this->getUser();
...
// HERE GOES THE QUERY
...
return $query->getResult();
}
you should pass security context as an argument.
Your controller
$securityContext = $this->get('security.context');
$em->getRepository('YourBundle')->findInviteRequestByUser($securityContext);
Your entity repository
public function findInviteRequestByUser(\Symfony\Component\Security\Core\SecurityContext $context)
{
$user = $context->getToken()->getUser();
...
}
I'm trying to find a good way for handling my access controls in Symfony2.
My requirements:
90% of my application can only be accessed by authenticated users
in many controllers I need to check if the user is the owner
there are also some differences for different user roles
What I've done already:
installed JMSSecurityExtraBundle to check permissions via annotation
defined global ace's for my entity classes
I create an ace for the owner for every object during the create process
The check for owner and roles is no Problem. I only want to define in a global way that a user has to be authenticated and for exceptions (sites that can be accessed anonymous) I want to define it separated (best via annotations).
I don't want to do this via routing pattern.
I'm not sure it be what you're looking for, but did you try with Event Listener ?
You can make your verification in the onKernelController method. Then, you will can create different Interfaces and check the type of your controller in the listener.
class AceBuilderListener implements EventSubscriber{
private $container;
public function setContainer($container){
$his->container = $container;
}
public function getSubscribedEvents()
{
return array(
Events::prePersist,
Events::preUpdate,
Events::preRemove,
Events::postPersist,
Events::postUpdate,
Events::postRemove,
Events::loadClassMetadata,
);
}
public function prePersist(){ echo( get_class($entity) ); }
public function preUpdate(){ echo( get_class($entity) ); }
public function preRemove(){ echo( get_class($entity) ); }
public function postPersist(){ echo( get_class($entity) ); }
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
echo get_class($entity);
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product | x) {
// ... do something with the Product
}
}
public function postRemove(){ die( get_class($entity) ); }
public function loadClassMetadata( LoadClassMetadataEventArgs $args ){
$classMetadata = $args->getClassMetadata();
$entityManager = $args->getEntityManager();
$user = $this->container->get('security.context')->getToken()->getUser();
// you can check here if isGranted();
// and get the entity from the object $classMetadata
$this->container->get('security.context')->isGranted('EDIT', $entity);
}
}