I build my business logic with propel. All the models extends an abstract model, in which I want to inject the security context:
Services.yml
parameters:
Abstract.Model : 'FooBundle\Model\AbstractModel'
services:
Abstract.Model:
class: 'FooBundle\Model\AbstractModel'
calls:
- [setSecurity, ["#security.context"]]
The AbstractModel
abstract class AbstractModel extends BaseObject
{
/**
* #param $security \Symfony\Component\Security\Core\SecurityContext
*/
protected $security;
/**
* Sets the security context
*
* #param $security \Symfony\Component\Security\Core\SecurityContext
*/
public function setSecurity($security)
{
$this->security = $security;
}
/**
* Code to be run before persisting the object
*
* #param PropelPDO $con
* #return boolean
*/
public function preSave(\PropelPDO $con = null)
{
$token = $this->security->getToken();
}
}
If propel runs the preSave method, it responses with a 500:
FatalErrorException: Error: Call to a member function getToken() on a non-object
Anyone know whats going wrong here?
Greetings
Update_01:
public function save(PropelPDO $con = null, $skipReload = false)
{
...
$ret = $this->preSave($con);
}
An abstract class cannot be instantiated.
Extend your Abstract class:
class MyModel extends AbstractModel
{
}
and use this class as service:
services:
My.Model:
class: 'FooBundle\Model\MyModel'
calls:
- [setSecurity, ["#security.context"]]
Related
I want to recover the date of the last time a user logged in my Symfony 5 website, I created a LoginListener and did the right settings (So-think-I ?) to make it work but in the class Login Listener :
namespace App\Event;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
// Get the User entity.
$user = $event->getAuthenticationToken()->getUser();
// Update your field here.
$user->setLastLogin(new \DateTime());
// Persist the data to database.
$this->em->persist($user);
$this->em->flush();
}
}
The setLastLogin(new \DateTime()) is described as an undefined method. Yet this method is in the entity User.php and it is properly called in the loginlistener. And the "use App\Entity\User :
/**
* {#inheritdoc}
*/
public function setLastLogin(\DateTime $time = null)
{
$this->lastLogin = $time;
return $this;
}
/**
* Gets the last login time.
*
* #return \DateTime|null
*/
public function getLastLogin()
{
return $this->lastLogin;
}
/**
* #var \DateTime $lastLogin
*
* #ORM\Column(type="datetime")
*/
private $lastLogin;
And when I try to make a exit(var_dump($user), nothing appears. Here is my services.yaml :
App\EventListener\LoginListener:
- tags:
- { name: 'kernel.event_listener', event: 'security.interactive_login', entity: 'App\Entity\User' }
Can someone help me please ? Thank you.
Today I started upgrading my application from symfony 3 to 4 (and so the related libraries) and I couldn't understand why I couldn't make certain routes work (I had a 401 error but they were supposed to be public routes so no security checks were made there), then I ended up finding this question: #Security annotation on controller class being overridden by action method
A recent comment on the question says that while in a previous version of symfony framework extra bundle, if you put the security annotation on both a class and a method inside that class, the method annotation would override the class annotation, now they stack instead.
This can also be seen (altough it's not very clear since you could already put a #Security annotation on both class and method) on the SensioFramework changelog https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/master/CHANGELOG.md for version 4.0
allowed using multiple #Security annotations (class and method)
This is a very big change for me since a lot of routes in my application relied on that behavior (which was similar to Symfony 1 where you could set a default security behavior and then a more specific one for each action)
/**
* #Route("my-route")
* #Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
class MyController extends Controller {
/**
* In Symfony 3.x this would've removed security checks for the route,
* now it checks both the class and the method Security expressions
* #Security(true)
*/
public function myAction(Request $request) {
}
}
Is there some way other than "don't upgrade to symfony 4" or "reorganize your code" (which is my "plan B") to have this behavior back? Something like a configuration option or similar...
I can't seem to find anything about this
I had forgot about this question but I did solve this issue by making my own annotation and EventListener.
Disclaimers:
1) My code uses the Dependency Injection bundle to inject and declare services using annotations
2) I'm sharing the code AS IS, with no warranty it'd work for you too, but i hope you can get the gist of it
I created 2 annotations (#IsGrantedDefault and #SecurityDefault) that work exactly like #IsGranted and #Security (they actually extend the original annotations) except they can be applied only to classes, then i created 2 event listeners, one for each annotation. The event listeners also extend the original event listeners, but they just check if a method already has a Security or IsGranted annotation, in which case they do nothing.
IsGrantedDefault.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Annotation;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
/**
* #Annotation
* #Target("CLASS")
*/
class IsGrantedDefault extends IsGranted {
public function getAliasName() {
return 'is_granted_default';
}
public function allowArray() {
return false;
}
}
SecurityDefault.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Annotation;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Annotation
* #Target("CLASS")
*/
class SecurityDefault extends Security {
public function getAliasName() {
return 'security_default';
}
public function allowArray() {
return false;
}
}
DefaultListenerTrait.php (Values::DEFAULT_LISTENER_PREFIX is just a string with an underscore "_")
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event\Traits;
use App\Project\AppBundle\Utils\Values;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
Trait DefaultListenerTrait {
/**
* #var string
*/
private $defaultAttribute;
/**
* #var string
*/
private $otherAttributes = [];
/**
* #var string
*/
private $attribute;
/**
* Sets the class attributes
* #param [type] $defaultAnnotation
* #param string|null $modifyAttr
* #return void
*/
protected function setAttributes($defaultAnnotation, ?string $modifyAttr) {
//Get the attirbutes names
$this->attribute = $modifyAttr;
$this->defaultAttribute = Values::DEFAULT_LISTENER_PREFIX . $defaultAnnotation->getAliasName();
$annotations = [new IsGranted([]), new Security([])];
foreach($annotations as $annotation) {
$this->otherAttributes[] = Values::DEFAULT_LISTENER_PREFIX . $annotation->getAliasName();
}
}
/**
* Checks wheter or not the request needs to be handled by the annotation. If it does adds the correct attribute to the request
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return boolean
*/
protected function updateDefaultListener(FilterControllerArgumentsEvent $event) {
$request = $event->getRequest();
$default = $request->attributes->get($this->defaultAttribute);
//If there's already an "IsGranted" annotation or there's no "IsGrantedDefault" annotation
if (!$default) {
return false;
}
foreach($this->otherAttributes as $attr) {
if ($request->attributes->get($attr) || !$default) {
return false;
}
}
//We set IsGranted from the default and then call the parent eventListener so that it can handle the security
$request->attributes->set($this->attribute, [$default]);
return true;
}
/**
* Calls the event listener for the class if the request is handled by the class
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return void
*/
protected function callEventListener(FilterControllerArgumentsEvent $event) {
if($this->updateDefaultListener($event)) {
parent::onKernelControllerArguments($event);
}
}
}
IsGrantedDefaultListener.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event;
use App\Project\AppBundle\Annotation\IsGrantedDefault;
use App\Project\AppBundle\Event\Traits\DefaultListenerTrait;
use App\Project\AppBundle\Utils\Values;
use RS\DiExtraBundle\Annotation as DI;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\IsGrantedListener;
use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* #DI\Service(autowire = true)
* #DI\Tag("kernel.event_subscriber")
*/
class IsGrantedDefaultListener extends IsGrantedListener {
use DefaultListenerTrait;
/**
* #param \Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter $argumentNameConverter
* #param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authChecker
* #DI\InjectParams({
* "argumentNameConverter" = #DI\Inject("framework_extra_bundle.argument_name_convertor"),
* "authChecker" = #DI\Inject("security.authorization_checker")
* })
*/
public function __construct(ArgumentNameConverter $argumentNameConverter, AuthorizationCheckerInterface $authChecker = null) {
parent::__construct($argumentNameConverter, $authChecker);
$modifyAttr = new IsGranted([]);
$this->setAttributes(new IsGrantedDefault([]), Values::DEFAULT_LISTENER_PREFIX . $modifyAttr->getAliasName());
}
/**
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return void
*/
public function onKernelControllerArguments(FilterControllerArgumentsEvent $event) {
$this->callEventListener($event);
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments'];
}
}
SecurityDefaultListener.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event;
use App\Project\AppBundle\Annotation\SecurityDefault;
use App\Project\AppBundle\Event\Traits\DefaultListenerTrait;
use App\Project\AppBundle\Utils\Values;
use Psr\Log\LoggerInterface;
use RS\DiExtraBundle\Annotation as DI;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener;
use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter;
use Sensio\Bundle\FrameworkExtraBundle\Security\ExpressionLanguage;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
/**
* #DI\Service(autowire = true)
* #DI\Tag("kernel.event_subscriber")
*/
class SecurityDefaultListener extends SecurityListener {
use DefaultListenerTrait;
/**
* #param \Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter $argumentNameConverter
* #param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authChecker
* #DI\InjectParams({
* "argumentNameConverter" = #DI\Inject("framework_extra_bundle.argument_name_convertor"),
* "language" = #DI\Inject("sensio_framework_extra.security.expression_language.default"),
* "trustResolver" = #DI\Inject("security.authentication.trust_resolver"),
* "roleHierarchy" = #DI\Inject("security.role_hierarchy"),
* "tokenStorage" = #DI\Inject("security.token_storage"),
* "authChecker" = #DI\Inject("security.authorization_checker"),
* "logger" = #DI\Inject("logger")
* })
*
*/
public function __construct(ArgumentNameConverter $argumentNameConverter, ExpressionLanguage $language = null, AuthenticationTrustResolverInterface $trustResolver = null, RoleHierarchyInterface $roleHierarchy = null, TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authChecker = null, LoggerInterface $logger = null) {
parent::__construct($argumentNameConverter, $language, $trustResolver, $roleHierarchy, $tokenStorage, $authChecker, $logger);
$modifyAttr = new Security([]);
$this->setAttributes(new SecurityDefault([]), Values::DEFAULT_LISTENER_PREFIX . $modifyAttr->getAliasName());
}
public function onKernelControllerArguments(FilterControllerArgumentsEvent $event) {
$this->callEventListener($event);
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments'];
}
}
You can delete the class annotation and declare them on all methods
I'm currently using Symfony 4 with Doctrine MongoDB Bundle, following the instruction from this link:
DoctrineMongoDBBundle. So, I have a UserDocument:
src/Document/UserDocument.php
/** #MongoDB\Document(collection="user", repositoryClass="App\Repository\UserRepository") */
class UserDocument
{
/**
* #MongoDB\Id
* #var ObjectId
*/
private $id;
/**
* #MongoDB\Field(type="string", name="first_name")
* #var string
*/
private $firstName;
/**
* #MongoDB\Field(type="string", name="middle_name")
* #var string
*/
private $middleName;
/**
* #MongoDB\Field(type="string", name="last_name")
* #var string
*/
private $lastName;
}
src/Repository/UserRepository.php
use Doctrine\ODM\MongoDB\DocumentRepository;
class UserRepository extends DocumentRepository
{
}
src/Controller/Content.php
class Content extends Controller
{
/**
* #Route("/content", name="content")
* #param UserRepository $user
* #return Response
*/
public function index(UserRepository $user)
{
$user->findAll();
return new Response();
}
}
So, after running the content page, I got the following error:
Cannot autowire service "App\Repository\UserRepository": argument "$uow" of method "__construct()" references class "Doctrine\ODM\MongoDB\UnitOfWork" but no such service exists.
The DocumentRepository constructor looks like this:
public function __construct(DocumentManager $dm, UnitOfWork $uow, ClassMetadata $classMetadata)
{
parent::__construct($dm, $uow, $classMetadata);
}
Repository shouldn't be Services, but if you want to keep it that way, just Autowire the DocumentManager and get the uow and classmetdata from the Document Manager.
UnitOfWork and ClassMetadata can't be autowired
Do something like that in your UserRepository, it should work.
public function __construct(DocumentManager $dm)
{
$uow = $dm->getUnitOfWork();
$classMetaData = $dm->getClassMetadata(User::class);
parent::__construct($dm, $uow, $classMetaData);
}
Make sure to exclude your repository class from autowiring. Example here: https://symfony.com/doc/current/service_container/3.3-di-changes.html
In case you want your repository class as a service you should do it using a factory service.
In the DI there is Autowiring, annotation definition and PHP definition.
In Symfony 3.3 the autowiring is enabled by default. So if I disable the autowiring, can I use the annotation to define a service?
class Foo
{
/**
* #Inject({"my.specific.service"})
*/
public function __construct(Bar $param1)
{
}
}
Update : Use JMSDiExtraBundle
namespace MediaBundle\Net;
use JMS\DiExtraBundle\Annotation\Inject;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Service;
/**
* #Service("some.service.id", public=false, environments = {"prod", "test", "dev"})
*/
class Foo
{
private $em;
private $session;
/**
* #InjectParams({
* "em" = #Inject("doctrine.orm.entity_manager"),
* "session" = #Inject("session")
* })
*/
public function __construct($em, $session)
{
$this->em = $em;
$this->session = $session;
}
}
Calling the service in the controller:
namespace MediaBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DefaultController extends Controller
{
/**
* #Route("/media")
*/
public function indexAction()
{
$someService = $this->get('some.service.id');
return $this->render('MediaBundle:Default:index.html.twig');
}
}
Result : You have requested a non-existent service "some.service.id".
Is your service injected somewhere? If not, it will be dropped from the container due to public=false, see http://symfony.com/blog/new-in-symfony-3-2-improved-private-services
In Symfony2 you can work with multiple entity managers and use something like the code below:
$em = $this->get('doctrine')->getManager();
$em = $this->get('doctrine')->getManager('default');
$customerEm = $this->get('doctrine')->getManager('customer');
We can inject the default manager to any service by using:
"#doctrine.orm.entity_manager"
How can you inject non-default entity managers into services?
If your entity managers config name is non_default then you can reference it as #doctrine.orm.non_default_entity_manager
For those who are using Symfony 3+, use the console :
php bin/console debug:container
Then you should see many lines starting with : 'doctrine.orm.MY_CUSTOM_ENTITY_MANAGER_xxxxxxxxxx'
So if you want the entity manager corresponding to your custom entity manager, find the line :
'doctrine.orm.MY_CUSTOM_ENTITY_MANAGER_entity_manager'
You can insert it in your service arguments.
Hope it helps.
You should define your custom entity manager as a service:
services:
name_of_your_custom_manager:
class: %doctrine.orm.entity_manager.class%
factory_service: doctrine
factory_method: getEntityManager
arguments: ["name_of_your_custom_manager"]
Then, you can inject it in the same way as you do with every service:
#name_of_your_custom_manager
Edit:
Pay attention that factory method may differ between symfony's version (it could be getEntityManager or getManager)
Hello first of all create your manager, in my example I create the manager for my Item class that is in a CoreBundle:
<?php
// src/Sybio/Bundle/CoreBundle/Manager/ItemManager.php:
namespace Sybio\Bundle\CoreBundle\Manager;
use Sybio\Bundle\CoreBundle\Entity\Item;
class ItemManager
{
/**
* #var \Doctrine\ORM\EntityManager $em entity manager
*/
protected $em;
/**
* #var \Doctrine\ORM\EntityRepository $em repository
*/
protected $repository;
/**
* #var string $entityName
*/
protected $entityName;
/**
* Constructor
*
* #param EntityManager $em
* #param string $entityName
*
* #return void
*/
public function __construct(EntityManager $em, $entityName)
{
$this->em = $em;
$this->repository = $em->getRepository($entityName);
$this->entityName = $entityName;
}
/**
* Save a entity object
*
* #param Object $entity
*
* #return Object Entity
*/
public function save($entity)
{
$this->persistAndFlush($entity);
return $entity;
}
/**
* Remove a entity object
*
* #param Object $entity
*
* #return Object Entity
*/
public function remove($entity)
{
return $this->removeAndFlush($entity);
}
/**
* Persist object
*
* #param mixed $entity
*
* #return void
*/
protected function persistAndFlush($entity)
{
$this->em->persist($entity);
$this->em->flush();
}
/**
* Remove object
*
* #param mixed $entity entity to remove
*
* #return void
*/
protected function removeAndFlush($entity)
{
$this->em->remove($entity);
$this->em->flush();
}
/**
* Returns entity repository object
*
* #return EntityRepository
*/
public function getRepository()
{
return $this->repository;
}
/**
* Create a new object
*
* #return mixed
*/
public function createNewObject()
{
return new Item();
}
// Create your own methods to manage the object
}
If the manager structure is shared between multiple manager, you can create a BaseManager extended by all other managers !
Then register it in the services.yml (or xml) file of your bundle:
# src/Sybio/Bundle/CoreBundle/Resources/config/services.yml or xml !:
parameters:
# Managers _________________
sybio.item_manager.entity: SybioCoreBundle:Item
sybio.item_manager.class: Sybio\Bundle\CoreBundle\Manager\ItemManager
services:
# Managers _________________
sybio.item_manager:
class: %sybio.item_manager.class%
arguments: [#doctrine.orm.entity_manager, %sybio.item_manager.entity%]
That's it, you can now use it:
// Controller:
$im = $this->get('sybio.item_manager');
$item = $im->createNewObject();
$im->save($item);
You can then improve your manager, here I give an array of config parameters to my manager:
# src/Sybio/Bundle/CoreBundle/Resources/config/services.yml or xml !:
sybio.item_manager:
class: %sybio.item_manager.class%
arguments: [#doctrine.orm.entity_manager, %sybio.item_manager.entity%, {'item_removed_state': %item_removed_state%, 'item_unpublished_state': %item_unpublished_state%, 'item_published_state': %item_published_state%}]
// src/Sybio/Bundle/CoreBundle/Manager/ItemManager.php:
public function __construct(EntityManager $em, $entityName, $params = array()) {
// ...
$this->params = $params;
}
If you create a BaseManager, you can also create a usefull generic method to initialize an object:
// src/Sybio/Bundle/CoreBundle/Manager/BaseManager.php:
/**
* Create a new object
*
* #return mixed
*/
public function createNewObject()
{
$entityName = explode(":", $this->entityName);
$entityName = "Sybio\Bundle\CoreBundle\Entity\\".$entityName[1];
return new $entityName;
}