set role for users in edit form of sonata admin - symfony

I'm using Symfony 2.1 for a project. I use the FOSUserBundle for managing users & SonataAdminBundle for administration usage.
I have some questions about that:
As an admin, I want to set roles from users in users edit form. How can I have access to roles in role_hierarchy? And how can I use them as choice fields so the admin can set roles to users?
When I show roles in a list, it is shown as string like this:
[0 => ROLE_SUPER_ADMIN] [1 => ROLE_USER]
How can I change it to this?
ROLE_SUPER_ADMIN, ROLE_USER
I mean, having just the value of the array.

Based on the answer of #parisssss although it was wrong, here is a working solution. The Sonata input field will show all the roles that are under the given role if you save one role.
On the other hand, in the database will be stored only the most important role. But that makes absolute sense in the Sf way.
protected function configureFormFields(FormMapper $formMapper) {
// ..
$container = $this->getConfigurationPool()->getContainer();
$roles = $container->getParameter('security.role_hierarchy.roles');
$rolesChoices = self::flattenRoles($roles);
$formMapper
//...
->add('roles', 'choice', array(
'choices' => $rolesChoices,
'multiple' => true
)
);
And in another method:
/**
* Turns the role's array keys into string <ROLES_NAME> keys.
* #todo Move to convenience or make it recursive ? ;-)
*/
protected static function flattenRoles($rolesHierarchy)
{
$flatRoles = array();
foreach($rolesHierarchy as $roles) {
if(empty($roles)) {
continue;
}
foreach($roles as $role) {
if(!isset($flatRoles[$role])) {
$flatRoles[$role] = $role;
}
}
}
return $flatRoles;
}
See it in action:

As for the second question I added a method in the User class that looks like this
/**
* #return string
*/
public function getRolesAsString()
{
$roles = array();
foreach ($this->getRoles() as $role) {
$role = explode('_', $role);
array_shift($role);
$roles[] = ucfirst(strtolower(implode(' ', $role)));
}
return implode(', ', $roles);
}
And then you can declare in your configureListFields function:
->add('rolesAsString', 'string')

i found an answer for my first question!(but the second one in not answered yet..)
i add the roles like below in configureFormFields function :
protected function configureFormFields(FormMapper $formMapper) {
//..
$formMapper
->add('roles','choice',array('choices'=>$this->getConfigurationPool()->getContainer()->getParameter('security.role_hierarchy.roles'),'multiple'=>true ));
}
I would be very happy if anyone answers the second question :)

Romain Bruckert's solution is almost perfect, except that it doesn't allow to set roles, which are roots of role hierrarchy. For instance ROLE_SUPER_ADMIN.
Here's the fixed method flattenRoles, which also returns root roles:
protected static function flattenRoles($rolesHierarchy)
{
$flatRoles = [];
foreach ($rolesHierarchy as $key => $roles) {
$flatRoles[$key] = $key;
if (empty($roles)) {
continue;
}
foreach($roles as $role) {
if(!isset($flatRoles[$role])) {
$flatRoles[$role] = $role;
}
}
}
return $flatRoles;
}
Edit: TrtG already posted this fix in comments

Just to overplay it a bit, here is my enhanced version of Romain Bruckert and Sash which gives you an array like this:
array:4 [▼
"ROLE_USER" => "User"
"ROLE_ALLOWED_TO_SWITCH" => "Allowed To Switch"
"ROLE_ADMIN" => "Admin (User, Allowed To Switch)"
"ROLE_SUPER_ADMIN" => "Super Admin (Admin (User, Allowed To Switch))"
]
This helps you find all roles, that include a specific role:
I know its much code, it could be done much better, but maybe it helps somebody or you can at least use pieces of this code.
/**
* Turns the role's array keys into string <ROLES_NAME> keys.
* #param array $rolesHierarchy
* #param bool $niceName
* #param bool $withChildren
* #param bool $withGrandChildren
* #return array
*/
protected static function flattenRoles($rolesHierarchy, $niceName = false, $withChildren = false, $withGrandChildren = false)
{
$flatRoles = [];
foreach ($rolesHierarchy as $key => $roles) {
if(!empty($roles)) {
foreach($roles as $role) {
if(!isset($flatRoles[$role])) {
$flatRoles[$role] = $niceName ? self::niceRoleName($role) : $role;
}
}
}
$flatRoles[$key] = $niceName ? self::niceRoleName($key) : $key;
if ($withChildren && !empty($roles)) {
if (!$recursive) {
if ($niceName) {
array_walk($roles, function(&$item) { $item = self::niceRoleName($item);});
}
$flatRoles[$key] .= ' (' . join(', ', $roles) . ')';
} else {
$childRoles = [];
foreach($roles as $role) {
$childRoles[$role] = $niceName ? self::niceRoleName($role) : $role;
if (!empty($rolesHierarchy[$role])) {
if ($niceName) {
array_walk($rolesHierarchy[$role], function(&$item) { $item = self::niceRoleName($item);});
}
$childRoles[$role] .= ' (' . join(', ', $rolesHierarchy[$role]) . ')';
}
}
$flatRoles[$key] .= ' (' . join(', ', $childRoles) . ')';
}
}
}
return $flatRoles;
}
/**
* Remove underscors, ROLE_ prefix and uppercase words
* #param string $role
* #return string
*/
protected static function niceRoleName($role) {
return ucwords(strtolower(preg_replace(['/\AROLE_/', '/_/'], ['', ' '], $role)));
}

The second answer is below.
Add lines in sonata admin yml file .
sonata_doctrine_orm_admin:
templates:
types:
list:
user_roles: AcmeDemoBundle:Default:user_roles.html.twig
and in user_roles.html.twig files add below lines
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
{% block field %}
{% for row in value %}
{{row}}
{% if not loop.last %}
,
{% endif %}
{% endfor %}
{% endblock %}
then into your admin controller and inconfigureListFields function add this line
->add('roles', 'user_roles')
hope this will solve your problem

Related

Creating a block; "the options do not exist"

Working on a project using Symfony and the CMF bundle. I followed the documentation in creating your own block.
However, when rendering the block, Symfony throws an exception telling me that the specified options do not exist.
An exception has been thrown during the rendering of a template ("The options "title", "url" do not exist. Known options are: "attr", "extra_cache_keys", "template", "ttl", "use_cache"") in FooMainBundle::Page/Standard.html.twig at line 15.
This is my template (Standard.html.twig):
{% extends "FooMainBundle::layout.html.twig" %}
{% block content %}
{{ sonata_block_render({'name': 'rssBlock'}, {
'title': 'Symfony CMF news',
'url': 'http://cmf.symfony.com/news.rss'
}) }}
{% endblock %}
This is my Document:
<?php
namespace Foo\BarContentBundle\Document;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
use Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\AbstractBlock;
/**
* #PHPCR\Document(referenceable=true)
*/
class RssBlock extends AbstractBlock
{
/**
* #PHPCR\String(nullable=true)
*/
private $feedUrl;
/**
* #PHPCR\String()
*/
private $title;
public function getType()
{
return 'foo_barcontent.block.rss';
}
public function getOptions()
{
$options = array(
'title' => $this->title,
);
if ($this->feedUrl) {
$options['url'] = $this->feedUrl;
}
return $options;
}
public function getTitle() {
return $this->title;
}
public function setTitle($title) {
$this->title = $title;
}
public function getFeedUrl() {
return $this->feedUrl;
}
public function setFeedUrl($feedUrl) {
$this->feedUrl = $feedUrl;
}
}
This is my service:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Block\BaseBlockService;
class RssBlockService extends BaseBlockService
{
public function getName()
{
return 'Rss Reader';
}
/**
* Define valid options for a block of this type.
*/
public function setDefaultSettings(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'url' => false,
'title' => 'Feed items',
'template' => 'FooBarContentBundle:Block:block_rss.html.twig',
));
}
/**
* The block context knows the default settings, but they can be
* overwritten in the call to render the block.
*/
public function execute(BlockContextInterface $blockContext, Response $response = null)
{
$block = $blockContext->getBlock();
if (!$block->getEnabled()) {
return new Response();
}
// merge settings with those of the concrete block being rendered
$settings = $blockContext->getSettings();
$resolver = new OptionsResolver();
$resolver->setDefaults($settings);
$settings = $resolver->resolve($block->getOptions());
$feeds = false;
if ($settings['url']) {
$options = array(
'http' => array(
'user_agent' => 'Sonata/RSS Reader',
'timeout' => 2,
)
);
// retrieve contents with a specific stream context to avoid php errors
$content = #file_get_contents($settings['url'], false, stream_context_create($options));
if ($content) {
// generate a simple xml element
try {
$feeds = new \SimpleXMLElement($content);
$feeds = $feeds->channel->item;
} catch (\Exception $e) {
// silently fail error
}
}
}
return $this->renderResponse($blockContext->getTemplate(), array(
'feeds' => $feeds,
'block' => $blockContext->getBlock(),
'settings' => $settings
), $response);
}
// These methods are required by the sonata block service interface.
// They are not used in the CMF. To edit, create a symfony form or
// a sonata admin.
public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
{
throw new \Exception();
}
public function validateBlock(ErrorElement $errorElement, BlockInterface $block)
{
throw new \Exception();
}
}
And I placed this in my services.xml:
<service id="foo_barcontent.block.rss" class="Foo\BarBundle\Block\RssBlockService">
<tag name="sonata.block" />
<argument>foo_barcontent.block.rss</argument>
<argument type="service" id="templating" />
</service>

How to implement a nice solution for multilang entity slug based routes in Symfony2

I'd like to create a simple bundle to handle some multilingual pages in a website with translated slugs.
Based on translatable, sluggable and i18nrouting
implemented an entity (Page) with title, content, slug fields + locale property as the doc says
created a new Page set its title and content then translated it by $page->setTranslatableLocale('de'); and set those fields again with the german values, so that the data in the tables looks fine, they are all there
implemented the controller with type hinting signature: public function showAction(Page $page)
generated some urls in the template by: {{ path("page_show", {"slug": "test", "_locale": "en"}) }} and {{ path("page_show", {"slug": "test-de", "_locale": "de"}) }}, routes are generated fine, they look correct (/en/test and /de/test-de)
clicking on them:
Only the "en" translation works, the "de" one fails:
MyBundle\Entity\Page object not found.
How to tell Symfony or the Doctrine or whatever bundle to use the current locale when retrieving the Page? Do I have to create a ParamConverter then put a custom DQL into it the do the job manually?
Thanks!
Just found another solution which I think is much nicer and i'm going to use that one!
Implemented a repository method and use that in the controller's annotation:
#ParamConverter("page", class="MyBundle:Page", options={"repository_method" = "findTranslatedOneBy"})
public function findTranslatedOneBy(array $criteria, array $orderBy = null)
{
$page = $this->findOneBy($criteria, $orderBy);
if (!is_null($page)) {
return $page;
}
$qb = $this->getEntityManager()
->getRepository('Gedmo\Translatable\Entity\Translation')
->createQueryBuilder('t');
$i = 0;
foreach ($criteria as $name => $value) {
$qb->orWhere('t.field = :n'. $i .' AND t.content = :v'. $i);
$qb->setParameter('n'. $i, $name);
$qb->setParameter('v'. $i, $value);
$i++;
}
/** #var \Gedmo\Translatable\Entity\Translation[] $trs */
$trs = $qb->groupBy('t.locale', 't.foreignKey')->getQuery()->getResult();
return count($trs) == count($criteria) ? $this->find($trs[0]->getForeignKey()) : null;
}
It has one disadvantage there is no protection against same translated values ...
I found out a solution which i'm not sure the best, but works.
Implemented a PageParamConverter:
class PageParamConverter extends DoctrineParamConverter
{
const PAGE_CLASS = 'MyBundle:Page';
public function apply(Request $request, ParamConverter $configuration)
{
try {
return parent::apply($request, $configuration);
} catch (NotFoundHttpException $e) {
$slug = $request->get('slug');
$name = $configuration->getName();
$class = $configuration->getClass();
$em = $this->registry->getManagerForClass($class);
/** #var \Gedmo\Translatable\Entity\Translation $tr */
$tr = $em->getRepository('Gedmo\Translatable\Entity\Translation')
->findOneBy(['content' => $slug, 'field' => 'slug']);
if (is_null($tr)) {
throw new NotFoundHttpException(sprintf('%s object not found.', $class));
}
$page = $em->find($class, $tr->getForeignKey());
$request->attributes->set($name, $page);
}
return true;
}
public function supports(ParamConverter $configuration)
{
$name = $configuration->getName();
$class = $configuration->getClass();
return parent::supports($configuration) && $class == self::PAGE_CLASS;
}
}
TranslationWalker nicely gets the entity in active locale:
class PagesRepository extends \Doctrine\ORM\EntityRepository
{
public function findTranslatedBySlug(string $slug)
{
$queryBuilder = $this->createQueryBuilder("p");
$queryBuilder
->where("p.slug = :slug")
->setParameter('slug', $slug)
;
$query = $queryBuilder->getQuery();
$query->setHint(
Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
return $query->getSingleResult();
}
}
And in controller
/**
* #Entity("page", expr="repository.findTranslatedBySlug(slug)")
* #param $page
*
* #return Response
*/
public function slug(Pages $page)
{
// thanks to #Entity annotation (Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity)
// Pages entity is automatically retrieved by slug
return $this->render('content/index.html.twig', [
'page' => $page
]);
}

Sonata Media Bundle : acces media url

I am using sonata media bundle.
and I was wondering how can I access the media url in twig.
I just want the url, I do not need to show the media.
Any suggestions?
You have to use the path media helper:
{% path media, 'small' %}
In the above code, media is an instance of the media entity, and small is the chosen format.
http://sonata-project.org/bundles/media/master/doc/reference/helpers.html#twig-usage
But if you do not want to render the media right there and just store the url in a variable, you need to ask the media provider for the public url.
This was my case, that I needed to pass the url to another template.
I did it creating a custom function in my Twig Extension (see here: http://symfony.com/doc/current/cookbook/templating/twig_extension.html).
Provided that you have the container available in your extension service with $this->container, you can do like this:
public function getMediaPublicUrl($media, $format)
{
$provider = $this->container->get($media->getProviderName());
return $provider->generatePublicUrl($media, $format);
}
Register the function in the extension:
public function getFunctions() {
....
'media_public_url' => new \Twig_Function_Method($this, 'getMediaPublicUrl'),
....
);
}
And call your new helper form your template:
{% set img_url = media_public_url(media, 'small') %}
for instance
regards
#javigzz's is perfect in case of default context. I used custom context, so had to handle $format first taking into account context name:
$provider = $this->container->get($media->getProviderName());
$format = $provider->getFormatName($media, $format);
$url = $provider->generatePublicUrl($media, $format);
Additional Note
Since injecting container is not the best practice, it is better to get provider from the provider pool:
class Foo {
public function __construct(Sonata\MediaBundle\Provider\Pool $pool) {
$this->pool = $pool;
}
public function getUrl($media, $format) {
$provider = $this->pool->getProvider($media->getProviderName());
$format = $provider->getFormatName($media, $format);
$url = $provider->generatePublicUrl($media, $format);
return $url;
}
}
Since #javigzz's answer did not work for me, here is a twig extension that works with the latest version of sonata_media:
namespace Socialbit\SonataMediaTwigExtensionBundle\Twig;
use Sonata\CoreBundle\Model\ManagerInterface;
use Symfony\Component\DependencyInjection\Container;
Class:
/**
* Description of MediaPathExtension
*
* #author thomas.kekeisen
*/
class MediaPathExtension extends \Twig_Extension
{
/**
*
* #var type Container
*/
protected $container;
/**
*
* #var type ManagerInterface
*/
protected $mediaManager;
public function __construct(Container $container, $mediaManager)
{
$this->container = $container;
$this->mediaManager = $mediaManager;
}
public function getFunctions()
{
return array
(
'media_public_url' => new \Twig_Function_Method($this, 'getMediaPublicUrl')
);
}
/**
* #param mixed $media
*
* #return null|\Sonata\MediaBundle\Model\MediaInterface
*/
private function getMedia($media)
{
$media = $this->mediaManager->findOneBy(array(
'id' => $media
));
return $media;
}
public function getMediaPublicUrl($media, $format)
{
$media = $this->getMedia($media);
$provider = $this->container->get($media->getProviderName());
return $provider->generatePublicUrl($media, $format);
}
public function getName()
{
return 'SocialbitSonataMediaTwigExtensionBundleMediaPathExtension';
}
}
services.yml:
services:
socialbit.sonatamediatwigextensionbundle.mediapathextension:
class: Socialbit\SonataMediaTwigExtensionBundle\Twig\MediaPathExtension
public: false
arguments:
- #service_container
- #sonata.media.manager.media
tags:
- { name: twig.extension }
The usage will be the same:
{% set img_url = media_public_url(media, 'reference') %}
{{ dump(img_url) }}
You can use: {% path media, 'reference' %}
#Blauesocke - tried your solution and had exactly the same result for file provider with using both
{% set img_url = media_public_url(media, 'reference') %}
{{ dump(img_url) }}
and
{% path sonata_admin.value, 'reference' %}

How to call a twig filter dynamically

I need to render data with unknown type with filters that specific on each data type:
the rendered structures looks like:
array(
"value" => "value-to-render",
"filter" => "filter-to-apply",
)
{% for item in items %}
{{ item.value|item.filter|raw}}
{% endfor %}
So My Question is: How can I get twig to use item.filter as a filter on the value?
You have to write your filter, which will call filters by passing name to it.
How to initially write you Extension you can read here.
Assuming that you have created you extension, you have define your custom function, (e.g., customFilter).
//YourTwigFilterExtension.php
public function getFunctions()
{
return array(
...
'custom_filter' => new \Twig_Function_Method($this, 'customFilter'),
);
}
Then, you have to define this function
public function customFilter($context, $filterName)
{
// handle parameters here, by calling the
// appropriate filter and pass $context there
}
After this manipulations you'll be able to call in Twig:
{% for item in items %}
{{ custom_filter(item.value, item.filter)|raw }}
{% endfor %}
Or, if you've defined your filter as filter (not as function):
{% for item in items %}
{{ item.value|custom_filter(item.filter)|raw }}
{% endfor %}
This Twig extension did the trick for me:
<?php
namespace YourNamespace\YourBundle\Twig;
use \Twig_Extension;
use \Twig_SimpleFilter;
use \Twig_Environment;
class ApplyFilterExtension extends Twig_Extension
{
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'apply_filter_twig_extension';
}
public function getFilters()
{
return array(
new Twig_SimpleFilter('apply_filter', array($this, 'applyFilter'), [
'needs_environment' => true,
]
));
}
public function applyFilter(Twig_Environment $env, $value, $filterName)
{
$twigFilter = $env->getFilter($filterName);
if (!$twigFilter) {
return $value;
}
return call_user_func($twigFilter->getCallable(), $value);
}
}
And then in your template:
{% for item in items %}
{{ item.value|apply_filter(item.filter)|raw}}
{% endfor %}
This question is directly linked to one of my questions :
Show variable inside variable
The answer is that you need a kind of "eval" method that doen't exist yet(but soon). BUT you also can create your own function as #thecatontheflat mention it.
I just created a Symfony Bundle for that:
Take a look here: https://github.com/marcj/twig-apply_filter-bundle
Here's an extension to dossorio's answer that allows chaining multiple filters as well as passing extra parameters to filters when needed:
class applyFiltersExtension extends Twig_Extension
{
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'apply_filters_twig_extension';
}
public function getFilters()
{
return [
new Twig_SimpleFilter('apply_filters', [$this, 'applyFilters'], [
'needs_environment' => true,
]
)];
}
public function applyFilters(Twig_Environment $env, $value, array $filters = null)
{
if (empty($filters)) {
return $value;
}
foreach ($filters as $filter) {
if (is_array($filter)) {
$filter_name = array_shift($filter);
$params = array_merge([$env, $value], $filter);
} else {
$filter_name = $filter;
$params = [$env, $value];
}
$twigFilter = $env->getFilter($filter_name);
if (empty($twigFilter)) {
continue;
}
$value = call_user_func_array($twigFilter->getCallable(), $params);
}
return $value;
}
}
.

Symfony2 - Set minimum number of embedded forms

Using the scenario in How to Embed a Collection of Forms, I would like to ensure that a Task always have at least 1 Tag. For my case however, the relationship of Task and Tag is 1:n rather than n:m.
I am particularly concerned on the scenario where all Tags are removed (I'd like to prevent this). How can I ensure that a Task form always have at least 1 Tag?
As pointed out by m0c the solution was indeed to utilize a custom validation constraint. However, I found out that such a constraint validator already exists in Symfony2.1 so I took the liberty of porting it for 2.0 (since some interfaces apparently changed in 2.1).
Here are ported versions (for 2.0) of Bernhard Schussek's Count.php and CountValidator.php for counting collections (see https://github.com/symfony/Validator/tree/master/Constraints).
Count.php
namespace MyVendor\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MissingOptionsException;
/**
* #Annotation
*
* #api
*/
class Count extends Constraint
{
public $minMessage = 'This collection should contain {{ limit }} elements or more.';
public $maxMessage = 'This collection should contain {{ limit }} elements or less.';
public $exactMessage = 'This collection should contain exactly {{ limit }} elements.';
public $min;
public $max;
public function __construct($options = null)
{
if (null !== $options && !is_array($options)) {
$options = array(
'min' => $options,
'max' => $options,
);
}
parent::__construct($options);
if (null === $this->min && null === $this->max) {
throw new MissingOptionsException('Either option "min" or "max" must be given for constraint ' . __CLASS__, array('min', 'max'));
}
}
}
CountValidator.php
namespace MyVendor\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class CountValidator extends ConstraintValidator
{
/**
* {#inheritDoc}
*/
public function isValid($value, Constraint $constraint)
{
if (null === $value) {
return false;
}
if (!is_array($value) && !$value instanceof \Countable) {
throw new UnexpectedTypeException($value, 'array or \Countable');
}
$count = count($value);
if ($constraint->min == $constraint->max
&& $count != $constraint->min) {
$this->setMessage($constraint->exactMessage, array(
'{{ count }}' => $count,
'{{ limit }}' => $constraint->min,
));
return false;
}
if (null !== $constraint->max && $count > $constraint->max) {
$this->setMessage($constraint->maxMessage, array(
'{{ count }}' => $count,
'{{ limit }}' => $constraint->min,
));
return false;
}
if (null !== $constraint->min && $count < $constraint->min) {
$this->setMessage($constraint->minMessage, array(
'{{ count }}' => $count,
'{{ limit }}' => $constraint->min,
));
return false;
}
return true;
}
}
I would create a custom Validator which checks if the property where the relationshop is mapped to on entity level has some elements in the collection.
And the Validator fails:
public function isValid($value, Constraint $constraint)
{
if (count($value) <1 ) {
//also define a message for your custom validator
$this->setMessage($constraint->message, array('%string%' => $value));
return false;
}
return true;
}
For instructions how to implement this custom validator: http://symfony.com/doc/current/cookbook/validation/custom_constraint.html
do u want to just validate upon posting, wheter or not there is atleast 1 tag,
or do u want the form to actually already have 1 empty tag in it upon loading ?
(wich i assume bacause u say "how can I ensure that a Task form always have at least 1 Tag?")
if u need the second, just
$tag1 = new Tag();
$tag1->name = 'tag1';
$task->getTags()->add($tag1);
Before
$form = $this->createForm(new TaskType(), $task);
like the docs say..

Resources