Missing metadata driver in Symfony 3.4 BETA 4 - symfony

After upgrading my project from Symfony 3.4.0 BETA 2 to 3.4.0 BETA 4 I get the following error in every request and every cli command:
[Doctrine\ORM\ORMException]
It's a requirement to specify a Metadata Driver and pass it to Doctrine\ORM\Configuration::setMetadataDriverImpl().
Exception trace:
Doctrine\ORM\ORMException::missingMappingDriverImpl() at D:\jinya-gallery-cms\vendor\doctrine\orm\lib\Doctrine\ORM\EntityManager.php:830
Doctrine\ORM\EntityManager::create() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:952
Container3xxrjsx\appDevDebugProjectContainer->getDoctrine_Orm_DefaultEntityManagerService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:2164
Container3xxrjsx\appDevDebugProjectContainer->getJinyaGallery_Monolog_MySqlHandlerService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:2299
Container3xxrjsx\appDevDebugProjectContainer->getMonolog_Logger_CacheService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:1508
Container3xxrjsx\appDevDebugProjectContainer->getCache_AnnotationsService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:1409
Container3xxrjsx\appDevDebugProjectContainer->getAnnotationReaderService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:940
Container3xxrjsx\appDevDebugProjectContainer->getDoctrine_Orm_DefaultEntityManagerService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:2164
Container3xxrjsx\appDevDebugProjectContainer->getJinyaGallery_Monolog_MySqlHandlerService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:2330
Container3xxrjsx\appDevDebugProjectContainer->getMonolog_Logger_EventService() at D:\jinya-gallery-cms\var\cache\dev\Container3xxrjsx\appDevDebugProjectContainer.php:1703
Container3xxrjsx\appDevDebugProjectContainer->getDebug_EventDispatcherService() at D:\jinya-gallery-cms\vendor\symfony\symfony\src\Symfony\Component\DependencyInjection\Container.php:299
Symfony\Component\DependencyInjection\Container->get() at D:\jinya-gallery-cms\vendor\symfony\symfony\src\Symfony\Bundle\FrameworkBundle\Console\Application.php:65
Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at D:\jinya-gallery-cms\vendor\symfony\symfony\src\Symfony\Component\Console\Application.php:129
Symfony\Component\Console\Application->run() at D:\jinya-gallery-cms\bin\console:27
After checking the generated cache code I found out, that the code that instantiates the EntityManager is completely different than in BETA 2.
This is the code from BETA 2:
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the public 'doctrine.orm.default_entity_manager' shared service.
$a = ${($_ = isset($this->services['annotation_reader']) ? $this->services['annotation_reader'] : $this->load(__DIR__.'/getAnnotationReaderService.php')) && false ?: '_'};
$b = new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($a, array(0 => 'D:\\jinya-gallery-cms\\src\\HelperBundle\\Entity', 1 => 'D:\\jinya-gallery-cms\\src\\DataBundle\\Entity'));
$c = new \Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain();
$c->addDriver($b, 'HelperBundle\\Entity');
$c->addDriver($b, 'DataBundle\\Entity');
$c->addDriver(new \Doctrine\ORM\Mapping\Driver\XmlDriver(new \Doctrine\Common\Persistence\Mapping\Driver\SymfonyFileLocator(array('D:\\jinya-gallery-cms\\vendor\\friendsofsymfony\\user-bundle\\Resources\\config\\doctrine-mapping' => 'FOS\\UserBundle\\Model'), '.orm.xml')), 'FOS\\UserBundle\\Model');
$d = new \Doctrine\ORM\Configuration();
$d->setEntityNamespaces(array('HelperBundle' => 'HelperBundle\\Entity', 'DataBundle' => 'DataBundle\\Entity'));
$d->setMetadataCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_metadata_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_metadata_cache'] : $this->load(__DIR__.'/getDoctrineCache_Providers_Doctrine_Orm_DefaultMetadataCacheService.php')) && false ?: '_'});
$d->setQueryCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_query_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_query_cache'] : $this->load(__DIR__.'/getDoctrineCache_Providers_Doctrine_Orm_DefaultQueryCacheService.php')) && false ?: '_'});
$d->setResultCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_result_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_result_cache'] : $this->load(__DIR__.'/getDoctrineCache_Providers_Doctrine_Orm_DefaultResultCacheService.php')) && false ?: '_'});
$d->setMetadataDriverImpl($c);
$d->setProxyDir(($this->targetDirs[0].'/doctrine/orm/Proxies'));
$d->setProxyNamespace('Proxies');
$d->setAutoGenerateProxyClasses(true);
$d->setClassMetadataFactoryName('Doctrine\\ORM\\Mapping\\ClassMetadataFactory');
$d->setDefaultRepositoryClassName('Doctrine\\ORM\\EntityRepository');
$d->setNamingStrategy(new \Doctrine\ORM\Mapping\UnderscoreNamingStrategy());
$d->setQuoteStrategy(new \Doctrine\ORM\Mapping\DefaultQuoteStrategy());
$d->setEntityListenerResolver(${($_ = isset($this->services['doctrine.orm.default_entity_listener_resolver']) ? $this->services['doctrine.orm.default_entity_listener_resolver'] : $this->load(__DIR__.'/getDoctrine_Orm_DefaultEntityListenerResolverService.php')) && false ?: '_'});
$this->services['doctrine.orm.default_entity_manager'] = $instance = \Doctrine\ORM\EntityManager::create(${($_ = isset($this->services['doctrine.dbal.default_connection']) ? $this->services['doctrine.dbal.default_connection'] : $this->load(__DIR__.'/getDoctrine_Dbal_DefaultConnectionService.php')) && false ?: '_'}, $d);
${($_ = isset($this->services['doctrine.orm.default_manager_configurator']) ? $this->services['doctrine.orm.default_manager_configurator'] : $this->services['doctrine.orm.default_manager_configurator'] = new \Doctrine\Bundle\DoctrineBundle\ManagerConfigurator(array(), array())) && false ?: '_'}->configure($instance);
return $instance;
This is the code from BETA 4
<?php
namespace ContainerXo2t2t9;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* #final since Symfony 3.3
*/
class appDevDebugProjectContainer extends Container
{
/**
* Gets the public 'doctrine.orm.default_entity_manager' shared service.
*
* #return \Doctrine\ORM\EntityManager
*/
protected function getDoctrine_Orm_DefaultEntityManagerService($lazyLoad = true)
{
$a = ${($_ = isset($this->services['doctrine.dbal.default_connection']) ? $this->services['doctrine.dbal.default_connection'] : $this->getDoctrine_Dbal_DefaultConnectionService()) && false ?: '_'};
$b = ${($_ = isset($this->services['annotation_reader']) ? $this->services['annotation_reader'] : $this->getAnnotationReaderService()) && false ?: '_'};
if (isset($this->services['doctrine.orm.default_entity_manager'])) {
return $this->services['doctrine.orm.default_entity_manager'];
}
$c = new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($b, array(0 => 'D:\\jinya-gallery-cms\\src\\HelperBundle\\Entity', 1 => 'D:\\jinya-gallery-cms\\src\\DataBundle\\Entity'));
$d = new \Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain();
$e = new \Doctrine\ORM\Configuration();
$this->services['doctrine.orm.default_entity_manager'] = $instance = \Doctrine\ORM\EntityManager::create($a, $e);
$d->addDriver($c, 'HelperBundle\\Entity');
$d->addDriver($c, 'DataBundle\\Entity');
$d->addDriver(new \Doctrine\ORM\Mapping\Driver\XmlDriver(new \Doctrine\Common\Persistence\Mapping\Driver\SymfonyFileLocator(array('D:\\jinya-gallery-cms\\vendor\\friendsofsymfony\\user-bundle\\Resources\\config\\doctrine-mapping' => 'FOS\\UserBundle\\Model'), '.orm.xml')), 'FOS\\UserBundle\\Model');
$e->setEntityNamespaces(array('HelperBundle' => 'HelperBundle\\Entity', 'DataBundle' => 'DataBundle\\Entity'));
$e->setMetadataCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_metadata_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_metadata_cache'] : $this->getDoctrineCache_Providers_Doctrine_Orm_DefaultMetadataCacheService()) && false ?: '_'});
$e->setQueryCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_query_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_query_cache'] : $this->getDoctrineCache_Providers_Doctrine_Orm_DefaultQueryCacheService()) && false ?: '_'});
$e->setResultCacheImpl(${($_ = isset($this->services['doctrine_cache.providers.doctrine.orm.default_result_cache']) ? $this->services['doctrine_cache.providers.doctrine.orm.default_result_cache'] : $this->getDoctrineCache_Providers_Doctrine_Orm_DefaultResultCacheService()) && false ?: '_'});
$e->setMetadataDriverImpl($d);
$e->setProxyDir(($this->targetDirs[0].'/doctrine/orm/Proxies'));
$e->setProxyNamespace('Proxies');
$e->setAutoGenerateProxyClasses(true);
$e->setClassMetadataFactoryName('Doctrine\\ORM\\Mapping\\ClassMetadataFactory');
$e->setDefaultRepositoryClassName('Doctrine\\ORM\\EntityRepository');
$e->setNamingStrategy(new \Doctrine\ORM\Mapping\UnderscoreNamingStrategy());
$e->setQuoteStrategy(new \Doctrine\ORM\Mapping\DefaultQuoteStrategy());
$e->setEntityListenerResolver(${($_ = isset($this->services['doctrine.orm.default_entity_listener_resolver']) ? $this->services['doctrine.orm.default_entity_listener_resolver'] : $this->services['doctrine.orm.default_entity_listener_resolver'] = new \Doctrine\Bundle\DoctrineBundle\Mapping\ContainerAwareEntityListenerResolver($this)) && false ?: '_'});
${($_ = isset($this->services['doctrine.orm.default_manager_configurator']) ? $this->services['doctrine.orm.default_manager_configurator'] : $this->services['doctrine.orm.default_manager_configurator'] = new \Doctrine\Bundle\DoctrineBundle\ManagerConfigurator(array(), array())) && false ?: '_'}->configure($instance);
return $instance;
}
}
The result is, that in BETA 4 the EntityManager is created before the config values are set. Does anyone know how I can affect the cache generation to create a working cache?

The solution which worked for me:
Install Lazy Services with composer require ocramius/proxy-manager
For each listener set additional parameter lazy: true in tags
app.service_doctrine.foo_listerner:
class: AppBundle\Service\Doctrine\FooListener
...
tags:
- { name: doctrine.orm.entity_listener, entity_manager: default, lazy: true }

Related

Gedmo Translatable persist default translation

I have a website built with Symfony 2.8 and Gedmo Translatable.
In order to use HINT_INNER_JOIN and filter items which don't have a translation I had to set persist_default_translation to true:
stof_doctrine_extensions:
default_locale: '%locale%' # TODO: what does it happen when removing this line?
translation_fallback: true
persist_default_translation: true
orm:
default:
timestampable: true
blameable: true
translatable: true
Unfortunately this caused that my existing translations for the default language are no more persisted (and they appear empty).
I would need to force re-save all of my entities to generate the default locale again.
How can I do that? I tried with clone and persist but it creates a duplicate of the entity.
Is it possible to force Doctrine to update all the fields again?
I ended up creating a custom command to migrate all the translations. I created a fake translation called "kr" and then updated all the record with "kr" to "fr" with an SQL query.
I used reflection and other "black magic" to get the properties with the Translatable annotation, maybe this could help someone with the same problem. Here is the code:
class NormalizeTranslationsCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
// the name of the command (the part after "app/console")
->setName('app:normalize-translations')
// the short description shown while running "php app/console list"
->setDescription('Normalizes the translations.')
// the full command description shown when running the command with
// the "--help" option
->setHelp('This command allows you to normalize the translations...')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// all the translatable classes
$classes = [
Entities\MyClass1::class,
Entities\MyClass2::class,
];
foreach ($classes as $class) {
$this->processClass($class, $output);
}
}
private function processClass($class, $output)
{
$output->writeln(sprintf('Processing class <info>%s</info>', $class));
// gets all the properties
$properties = $this->getProperties($class);
// gets the translatable properties
$translatableProperties = $this->getTranslatableProperties($properties, $class);
$output->writeln(sprintf('Found %d translatable properties: %s', count($translatableProperties), implode(', ', $translatableProperties)));
$defaultLanguage = 'kr'; // fake language
$em = $this->getContainer()->get('doctrine')->getManager();
$repository = $em->getRepository('Gedmo\\Translatable\\Entity\\Translation');
$items = $em->getRepository($class)->findAll();
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($items as $item) {
foreach ($translatableProperties as $translatableProperty) {
$value = $propertyAccessor->getValue($item, $translatableProperty);
$repository->translate($item, $translatableProperty, $defaultLanguage, $value);
}
$em->flush();
}
}
private function getProperties($class)
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// array of PropertyListExtractorInterface
$listExtractors = array($reflectionExtractor);
// array of PropertyTypeExtractorInterface
$typeExtractors = array($phpDocExtractor, $reflectionExtractor);
// array of PropertyDescriptionExtractorInterface
$descriptionExtractors = array($phpDocExtractor);
// array of PropertyAccessExtractorInterface
$accessExtractors = array($reflectionExtractor);
$propertyInfo = new PropertyInfoExtractor(
$listExtractors,
$typeExtractors,
$descriptionExtractors,
$accessExtractors
);
return $propertyInfo->getProperties($class);
}
private function getTranslatableProperties($properties, $class)
{
$translatableProperties = [];
// https://gist.github.com/Swop/5990316
$annotationReader = new AnnotationReader();
foreach ($properties as $property) {
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
$propertyAnnotations = $annotationReader->getPropertyAnnotations($reflectionProperty);
foreach ($propertyAnnotations as $propertyAnnotation) {
if ($propertyAnnotation instanceof Translatable) {
// this property is translatable
$translatableProperties[] = $property;
}
}
} catch (\ReflectionException $e) {
// missing property
continue;
}
}
return $translatableProperties;
}
}

accessing repository in symfony

I am using symfony and am inside the controller trying to return a set of records by using a range (rangeUpper, rangeLower).
I am passing the parameters in the request object fine. But when going to route in the controller and trying to access the repository class I am at a loss. My repository looks like;
public function findAllByParams (Request $request)
{
$criteria = $request->query->get('uniflytebundle_material-stock_select');
$criteria = ($request->get('materialId') == 0 ? [] : ['materialId' => $request->get('materialId')]);
$criteria = array_merge(($request->get('gaugeId') == 0 ? [] : ['gaugeId' => $request->get('gaugeId')]), $criteria);
$criteria = array_merge(($request->get('rangeUpper') == 0 ? [] : ['rangeUpper' => $request->get('rangeUpper')]), $criteria);
$criteria = array_merge(($request->get('rangeLower') == 0 ? [] : ['rangeLower' => $request->get('rangeLower')]), $criteria);
$criteria = array_filter($criteria);
$query = $this->createQueryBuilder('ms');
if (!empty($criteria)) {
if (!empty($criteria['materialId']) && !empty($criteria['gaugeId']) && !empty($criteria['rangeUpper']) && !empty($criteria['rangeLower'])) {
$query
->where('ms.material = :materialId')
->andWhere('ms.gauge = :gaugeId')
->andWhere('ms.widthDecimal <= :upperIdentifier')
->andWhere('ms.widthDecimal >= :lowerIdentifier')
->setParameter('materialId', $criteria['materialId'])
->setParameter('gaugeId', $criteria['gaugeId'])
->setParameter('upperIdentifier', $criteria['rangeUpper'])
->setParameter('lowerIdentifier', $criteria['rangeLower'])
;
}
}
$query->orderBy('ms.widthDecimal', 'DESC');
return $query->getQuery()->getResult();
}
My current controller looks like
public function selectStripWidthAction (Request $request)
{
$em = $this->getDoctrine()->getManager();
$materialId = $request->get('materialId');
$gaugeId = $request->get('gaugeId');
$materialStocks = $em->getRepository('UniflyteBundle:MaterialStock')->findAllByParams($request);
return $this->render('materialstock/dialog/select.html.twig', [
'MaterialStocks' => $materialStocks,
]);
}
In the Repository I have the query setup to accept and query by the Range. How do I pass the request object and retrieve the result set from the Repository? FindAllByParams call in the controller is not working.
Undefined method 'findAllByParams'. The method name must start with either findBy or findOneBy!
Thanks in advance for your time and effort.
Check that your MaterialStock have annotation for repository.
<?php
namespace UniflyteBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* MaterialStock
*
* #ORM\Table(name="material_stock")
* #ORM\Entity(repositoryClass="UniflyteBundle\Repository\MaterialStockRepository")
*/
class MaterialStock{
// [...]
}

Callback Constraint doesn't show paylod (Symfony Validator Component)

My controller code:
public function postFilesAction(Request $request)
{
$validator = $this->get('validator');
$requestCredentials = RequestCredentials::fromRequest($request);
$errors = $validator->validate($requestCredentials);
...
validate method in RequestCredentials (Callback constraint).
/**
* #Assert\Callback(payload = {"errorCode" = "FILE_FILE_URL"})
*/
public function validate(ExecutionContextInterface $context)
{
if (! ($this->fileExistsAndValid() || $this->fileUrlExistsAndValid())) {
$context->buildViolation('Neither file nor file_url is present.')->addViolation();
}
}
Callback works as expected, but the value of $constraintViolation->$constraint->$payload is null.
When I'm trying to use payload in other Constraints (NotBlank, for example), it works (I can see it in ConstraintViolation object).
Is it Symfony bug or am I doing somethings wrong? Should I use some other solution to my problem? (I need to check if there's at least one of two fields (file or file_url) present in request).
In Symfony 3.0 you cannot easily access the payload in the callback when using the Callback constraint. Starting with Symfony 3.1, the payload will be passed as an additional argument to the callback (see https://github.com/symfony/symfony/issues/15092 and https://github.com/symfony/symfony/pull/16909).
I managed to solve this problem with following code in the assertion:
/**
* #Assert\Callback(payload = {"error_code" = "1"}, callback = "validate", groups = {"Default", "RequestCredentials"})
*/
public function validate(ExecutionContextInterface $context)
{
// some validation code
}
I think the problem was because of the Symfony Callback constraint constructor:
public function __construct($options = null)
{
// Invocation through annotations with an array parameter only
if (is_array($options) && 1 === count($options) && isset($options['value'])) {
$options = $options['value'];
}
if (is_array($options) && !isset($options['callback']) && !isset($options['groups'])) {
$options = array('callback' => $options);
}
parent::__construct($options);
}
When it is given $options = ['payload' => [...]] (what happened in my case) it turns it into $options = ['callback' => ['payload' => [...]]]
and then '$payload' data becomes inacessable in ConstraintViolation object.
But I'm still not sure whether it's Symfony imperfection or me not getting something and using it wrong.

Doctrine does not update FOSUserBundle User object

I'm encountered a weird problem.
I'm working on a Symfony 2.1 project with Doctrine 2.2 and the FOSUserBundle for user management.
I added a RequestListener, since the user can change the language of the site and I want to track the last used language of the user.
So I simply added a new property to the User Entity and then want to save the new language if has changed.
So I'm doing this in the request listener:
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
return;
}
if ($event->getRequest()->getRequestFormat() !== 'html') {
return;
}
if ($this->context->getToken()->getUser() instanceof \Foo\BarBundle\Entity\User) {
$this->request = $event->getRequest();
$this->user = $this->context->getToken()->getUser();
if ($this->user->getCustomer() instanceof \Foo\BarBundle\Entity\Customer) {
$this->customer = $this->user->getCustomer();
$permission = $this->permissionService->getPermissionSafely($this->customer);
$params = $this->request->get('_route_params');
$language = $this->getLanguage($permission['language']['languages']);
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
if ($language !== "all" && $this->request->get('_locale') !== $language) {
$params['_locale'] = $language;
$redirect = new RedirectResponse($this->router->generate($this->request->get('_route'), $params));
$event->setResponse($redirect);
}
if ($this->user->getLastLanguage() !== $locale) {
$this->user->setLastLanguage($locale);
$this->em->flush();
}
}
}
}
private function getLanguage($language)
{
if (!isset($language['en'])) {
return 'de';
}
if (!isset($language['de'])) {
return 'en';
}
if ($language['en'] && !($language['de'])) {
return 'en';
} else if (!$language['en'] && $language['de']) {
return 'de';
}
return 'all';
}
Important is the last if-conditional. If the current $locale is different than the last used, I want to update the user object. So there are three possible values: de_DE, en_US and null.
Now the weird behaviour comes in (and I don't know if it's a bug or what, but I'm confused):
It doesn't matter which value is stored in the database, it always gets updated to en_US.
If a user has visit the page for the first time (value null) and visits the site in german (value de_DE) it gets updated to en_US, but the profiler query says:
UPDATE `user` SET last_language = 'de_DE' WHERE id = 1
If a user has last_language = 'de_DE' and visits the site in german (de_DE) it gets updated to en_US, but the query profiler says, that there wasn't a update query. Which makes sense, because the $locale is the same like $this->user->getLastLanguage().
What the??
I have no idea what is going on here. Has anyone experienced a similar problem? Has this something to do with the fact, that I'm modifying the user object from the security context?
Update: The funny thing is, if I change line
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
to
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'es_US';
it gets updated to es_US event if $locale holds de_DE
Your ternary if statement will always fail because the return value of
$this->request->get('_locale')
will be :
de_DE
but never === 'de'. Therefore if you save $locale in your entity after calling
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
... the next time this statement fails and puts it back to 'en_US' calling ...
$user->setLastLanguage('en_US');
... in the end. Just do a better comparison like ...
$locale = (strstr($locale,'de') !== false) ? 'de_DE' : 'en_US';
Have you tried persist before flushing:
$this->user->setLastLanguage($locale);
$this->em->flush();
should be ...
$this->user->setLastLanguage($locale);
$this->em->persist($this->user);
$this->em->flush();
... if your user is newly created and not already managed by doctrine.

How to use class-scope aces in Symfony2?

I've got a problem with class-scope aces. I've created an ace for a
class like this :
$userIdentity = UserSecurityIdentity::fromAccount($user);
$classIdentity = new ObjectIdentity('some_identifier', 'Class\FQCN');
$acl = $aclProvider->createAcl($classIdentity);
$acl->insertClassAce($userIdentity, MaskBuilder::MASK_CREATE);
$aclProvider->updateAcl($acl);
Now, I'm trying to check the user's permissions. I've found this way
of doing things, which is not documented, but gives the expected
results on a class basis :
$securityContext->isGranted('CREATE', $classIdentity); // returns true
$securityContext->isGranted('VIEW', $classIdentity); // returns true
$securityContext->isGranted('DELETE', $classIdentity); // returns false
This method is well adapated to the "CREATE" permission check, where
there's no available object instance to pass to the method. However,
it should be possible to check if another permission is granted on a
particular instance basis :
$entity = new Class\FQCN();
$em->persist($entity);
$em->flush();
$securityContext->isGranted('VIEW', $entity); // returns false
This is where the test fails. I expected that an user who has a given
permission mask on a class would have the same permissions on every
instance of that class, as stated in the documentation ("The
PermissionGrantingStrategy first checks all your object-scope ACEs if
none is applicable, the class-scope ACEs will be checked"), but it
seems not to be the case here.
you are doing it right. and according to the bottom of this page, it should work, but it does not.
the easiest way to make it work is creating an AclVoter class:
namespace Core\Security\Acl\Voter;
use JMS\SecurityExtraBundle\Security\Acl\Voter\AclVoter as BaseAclVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Doctrine\Common\Util\ClassUtils;
class AclVoter extends BaseAclVoter
{
public function vote( TokenInterface $token , $object , array $attributes )
{
//vote for object first
$objectVote = parent::vote( $token , $object , $attributes );
if( self::ACCESS_GRANTED === $objectVote )
{
return self::ACCESS_GRANTED;
}
else
{
//then for object's class
$oid = new ObjectIdentity( 'class' , ClassUtils::getRealClass( get_class( $object ) ) );
$classVote = parent::vote( $token , $oid , $attributes );
if( self::ACCESS_ABSTAIN === $objectVote )
{
if( self::ACCESS_ABSTAIN === $classVote )
{
return self::ACCESS_ABSTAIN;
}
else
{
return $classVote;
}
}
else if( self::ACCESS_DENIED === $objectVote )
{
if( self::ACCESS_ABSTAIN === $classVote )
{
return self::ACCESS_DENIED;
}
else
{
return $classVote;
}
}
}
return self::ACCESS_ABSTAIN;
}
}
then in security.yml set this:
jms_security_extra:
voters:
disable_acl: true
and finally set up the voter as a service:
core.security.acl.voter.basic_permissions:
class: Core\Security\Acl\Voter\AclVoter
public: false
arguments:
- '#security.acl.provider'
- '#security.acl.object_identity_retrieval_strategy'
- '#security.acl.security_identity_retrieval_strategy'
- '#security.acl.permission.map'
- '#?logger'
tags:
- { name: security.voter , priority: 255 }
- { name: monolog.logger , channel: security }
You need to ensure each object has its own ACL (use $aclProvider->createAcl($entity)) for class-scope permissions to work correctly.
See this discussion: https://groups.google.com/forum/?fromgroups=#!topic/symfony2/pGIs0UuYKX4
If you don't have an existing entity, you can check against the objectIdentity you created.
Be careful to use "double-backslashes", because of the escaping of the backslash.
$post = $postRepository->findOneBy(array('id' => 1));
$securityContext = $this->get('security.context');
$objectIdentity = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');
// check for edit access
if (true === $securityContext->isGranted('EDIT', $objectIdentity)) {
echo "Edit Access granted to: <br/><br/> ";
print_r("<pre>");
print_r($post);
print_r("</pre>");
} else {
throw new AccessDeniedException();
}
That should work!
If you would check for "object-scope" you could just use $post instead of $objectIdentity in the isGranted function call.
I have tried to find the best solution for this problem and I think the best answer is the one of rryter / edited by Bart. I just want to extend the solution.
Let's say you want to give access to a specific object type for a specific user, but not for a concrete object instance (for example id=1).
Then you can do the following:
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = new ObjectIdentity('class', 'someNamspace\\SeperatedByDoubleSlashes');
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityContext = $this->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
$securityContext = $this->get('security.context');
// check for edit access
if (false === $securityContext->isGranted('EDIT', $objectIdentity)) {
throw new AccessDeniedException();
}
The difference to the example given by the symfony cookbook is that you are using the class scope and not the object scope.
There's only 1 line that makes the difference:
$objectIdentity = new ObjectIdentity('class', 'someNamspace\\SeperatedByDoubleSlashes');
instead of:
$objectIdentity = ObjectIdentity::fromDomainObject($object);
You can still add specific permissions for specific object instances if you have at least one class scope permission in your classes acl.

Resources