Symfony 5 > Display Roles as "human readable" - symfony

I want set up Symfony 5 to use "human readable" roles for display purposes. When displaying user information in TWIG I can use {{ user.roles[0] }}. However, this displays (for example) "ROLE_ACCOUNTADMIN" (as is saved against the user in the database), but I would like it to display as "Account Administrator". With Symfony 3 where a user_roles table was used, there was a "role" and a "name" field, but this has been removed. Is it possible to accomplish this in Symfony 5 without having to define / include an array whenever I want to use this?

You can create an extension twig and you have many solutions ...,
but for the solution that I see the simplest, just put a field on the entity and initialize it with the main role, so you will have the info on the api and twig, mailing ...
<?php
namespace App\Entity;
// ...
class User
{
public static $USER_ROLE_AS_HUMAN_READABLE_INDEX = [
'ROLE_ACCOUNTADMIN' => 'Account Administrator',
'OTHER_ROLE' => 'Description',
// ...
]
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// without mapping ..
private $roleHumanReadable
// ...
public function __construct() {
$this->initializeRoleHumanReadable();
}
public function getRoleHumanReadable():?string
{
return $this->roleHumanReadable;
}
public function initializeRoleHumanReadable():void
{
$rolePrincipal = $this->getRoles()[0] ?? null;
if (!isset(static::$USER_ROLE_AS_HUMAN_READABLE_INDEX[$rolePrincipal])) {
return;
}
$this->roleHumanReadable = static::$USER_ROLE_AS_HUMAN_READABLE_INDEX[$rolePrincipal];
}
}

Related

Easy Admin 3 (Symfony 4) AssociationField in OneToOne relationship shows already associated entities

Using Symfony 4.4 with Easy Admin 3:
I've a OneToOne relationship
class Usuario
{
...
/**
* #ORM\OneToOne(targetEntity=Hora::class, inversedBy="usuario", cascade={"persist", "remove"})
*/
private $hora;
...
}
class Hora
{
...
/**
* #ORM\OneToOne(targetEntity=Usuario::class, mappedBy="hora", cascade={"persist", "remove"})
*/
private $usuario;
...
}
I've got a CRUD Controller for Usuario:
class UsuarioCrudController extends AbstractCrudController
{
public function configureFields(string $pageName): iterable
{
...
return [
...
AssociationField::new('hora', 'Hora'),
];
Everything seems ok, but in the admin form for "Usuario", the field "hora" shows all values in database, even the ones already assigned to other "Usuario" entities:
I would like the dropdown control to show only not assigned values, PLUS the value of the actual "Usuario" entity, so the control be easy to use.
Which is the proper way to do this with easyadmin?
I've managed to code the field to show only the not associated "Hora" values, using $this->getDoctrine() and ->setFormTypeOptions([ "choices" => in UsuarioCrudController class,
but I am not able to access the actual entity being managed, nor in UsuarioCrudController class (maybe there it is not accesible) neither in Usuario class (I've tried here __construct(EntityManagerInterface $entityManager) to no avail as the value doesn't seem to be injected, dunno why).
It is possible to customize a few things in easy admin by either overriding EasyAdmin methods or listening to EasyAdmin events.
Example of methods:
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
public function createEntity(string $entityFqcn)
public function createEditForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
//etc..
Example of events:
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
You could override easy admin createEditFormBuilder or createNewFormBuilder method, this way you could access the current form data and modify your hora field.
Something like :
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface {
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$unassignedValues = $this->yourRepo->findUnassignedValues();
$data = $context->getEntity()->getInstance();
if(isset($data) && $data->getHora()){
//if your repo return an ArrayCollection
$unassignedValues = $unassignedValues->add($data->getHora());
}
// if 'class' => 'App\Entity\Hora' is not passed as option, an error is raised (see //github.com/EasyCorp/EasyAdminBundle/issues/3095):
// An error has occurred resolving the options of the form "Symfony\Bridge\Doctrine\Form\Type\EntityType": The required option "class" is missing.
$formBuilder->add('hora', EntityType::class, ['class' => 'App\Entity\Hora', 'choices' => $unassignedValues]);
return $formBuilder;
}
Currently, easyadmin3 still lack documentation so sometimes the best way to do something is to look at how easy admin is doing things.
fwiw, the actual entity being edited can be accessed in a Symfony easyadmin CrudController's configureFields() method using:
if ( $pageName === 'edit' ) {
...
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()
...
This way in configureFields() I could add code to filter my entities:
$horas_libres = $this->getDoctrine()->getRepository(Hora::class)->findFree();
and then add the actual entity value also, which is what I was trying to do:
array_unshift( $horas_libres,
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()->getHora() );
Now the field can be constructed in the returned array with "choices":
return [ ...
AssociationField::new('hora', 'Hora')->setFormTypeOptions([
"choices" => $horas_libres
]),
]

Symfony validation callback

I'm trying to validate my entity via static callback.
I was able to make it work following the Symfony guide but something isn't clear to me.
public static function validate($object, ExecutionContextInterface $context, $payload)
{
// somehow you have an array of "fake names"
$fakeNames = array(/* ... */);
// check if the name is actually a fake name
if (in_array($object->getFirstName(), $fakeNames)) {
$context->buildViolation('This name sounds totally fake!')
->atPath('firstName')
->addViolation()
;
}
}
It works fine when I populate my $fakeNames array but what if I want to make it "dynamic"? Let's say I want to pick that array from the parameters or from the database or wherever.
How am I supposed to pass stuff (eg. the container or entityManager) to this class from the moment that the constructor doesn't work and it has to be necessarily static?
Of course my approach may be completely wrong but I'm just using the symfony example and few other similar issues found on the internet that I'm trying to adapt to my case.
You can create a Constraint and Validator and register it as service so you can inject entityManager or anything you need, you can read more here:
https://symfony.com/doc/2.8/validation/custom_constraint.html
or if you are on symfony 3.3 it is already a service and you can just typehint it in your constructor:
https://symfony.com/doc/current/validation/custom_constraint.html
This is the solution I was able to find in the end.
It works smoothly and I hope it may be useful for someone else.
I've set the constraint on my validation.yml
User\UserBundle\Entity\Group:
constraints:
- User\UserBundle\Validator\Constraints\Roles\RolesConstraint: ~
Here is my RolesConstraint class
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\Validator\Constraint;
class RolesConstraint extends Constraint
{
/** #var string $message */
public $message = 'The role "{{ role }}" is not recognised.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and here is my RolesConstraintValidator class
<?php
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RolesConstraintValidator extends ConstraintValidator
{
/** #var ContainerInterface */
private $containerInterface;
/**
* #param ContainerInterface $containerInterface
*/
public function __construct(ContainerInterface $containerInterface)
{
$this->containerInterface = $containerInterface;
}
/**
* #param \User\UserBundle\Entity\Group $object
* #param Constraint $constraint
*/
public function validate($object, Constraint $constraint)
{
if (!in_array($object->getRole(), $this->containerInterface->getParameter('roles'))) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ role }}', $object->getRole())
->addViolation();
}
}
}
Essentially, I set up a constraint which, every time a new user user is registered along with the role, that role must be among those set in the parameters. If not, it builds a violation.

Symfony - FOSRestBundle - show selected fields

I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio

JMSSerializerBundle - preserve relation name

I'm using Symfony2 with JMSSerializerBundle. And I'm new with last one =) What should I do in such case:
I have Image model. It contains some fields, but the main one is "name". Also, I have some models, which has reference to Image model. For example User and Application. User model has OneToOne field "avatar", and Application has OneToOne field "icon". Now, I want to serialize User instance and get something like
{
...,
"avatar": "http://example.com/my/image/path/image_name.png",
....
}
Also, I want to serialize Application and get
{
...,
"icon": "http://example.com/my/image/path/another_image_name.png",
...
}
I'm using #Inline annotation on User::avatar and Application::icon fields to reduce Image object (related to this field) to single scalar value (only image "name" needed). Also, my Image model has ExclusionPolicy("all"), and exposes only "name" field. For now, JMSSerializer output is
(For User instance)
{
...,
"name": "http://example.com/my/image/path/image_name.png",
...
}
(For Application instance)
{
...,
"name": "http://example.com/my/image/path/another_image_name.png",
...
}
The question is: How can I make JMSSerializer to preserve "avatar" and "icon" keys in serialized array instead of "name"?
Finally, I found solution. In my opinion, it is not very elegant and beautiful, but it works.
I told to JMSSerializer, that User::avatar and Application::icon are Images. To do that, I used annotation #Type("Image")
//src\AppBundle\Entity\User.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="avatar", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $avatar;
//...
//src\AppBundle\Entity\Application.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="icon", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $icon;
//...
I implemented handler, which serializes object with type Image to json.
<?php
//src\AppBundle\Serializer\ImageTypeHandler.php
namespace AppBundle\Serializer;
use AppBundle\Entity\Image;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpFoundation\Request;
class ImageTypeHandler implements SubscribingHandlerInterface
{
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
static public function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Image',
'method' => 'serializeImageToWebPath'
]
];
}
public function serializeImageToWebPath(JsonSerializationVisitor $visitor, Image $image = null, array $type, Context $context)
{
$path = $image ? "http://" . $this->request->getHost() . "/uploads/images/" . $image->getPath() : '';
return $path;
}
}
And the last step is to register this handler. I also injected request service to generate full web path to image in my handler.
app.image_type_handler:
class: AppBundle\Serializer\ImageTypeHandler
arguments: ["#request"]
scope: request
tags:
- { name: jms_serializer.subscribing_handler }
Also, you can use this workaround, to modify serialized data in post_serialize event.

Symfony2 best practices for stay DRY

I'm new to Symfony2. I have to learn it for my new job (it starts this monday). Before that, I used a lot CodeIgniter... so this change a bit.
After reading tons of documentations, tuts, best practices ... create my Own intranet for testing (customers has websites, websites has accesses, accesses has website, website has category, accesses has accesscategory) I still have some questions.
First Question :
When you have a website with frontend and backend you have all the time some repetitives actions like :
- create new entity
- read entity
- update entity
- delete entity
...
In CI, I create a BaseController and a BaseModel and with some extends, I was OK.
This practice is still OK for Symfony 2 or do Symfony have another way to handle that ?
Like AppBundle\Controller\BaseController extended by a AppBundle\Controller\AdminController (and FrontController) extended by AppBundle\Controller\MyEntityController ?
Because Actually, each time, in each controller I have the same code. When I edit an entity (for example), it's the same process : load the entity by id, throw exception if no entity, create and hydrate the form, handleRequest the post and valid the form, reidrect or display the view... but... I always cut/paste the same code... aweful T__T
So I'm searching for the best way to handle that
** Second Question : **
What is the best and elegent way to work with the DoctrineManager ?
Do I have to call it, each time in my actions ? $em = $this->get... or, can I create something like MyEntityManager which call the EntityManager and the repository of my entity ?
Actually, this is what I do :
I create an abstract AppBundle\Manager\BaseManager with loadAndFlush
<?php
namespace AppBundle\Manager;
abstract class BaseManager
{
protected function persistAndFlush($entity)
{
$this->em->persist($entity);
$this->em->flush();
}
}
Then, for each Entity, I create his own manager :
<?php
namespace AppBundle\Manager;
use Doctrine\ORM\EntityManager;
use AppBundle\Manager\BaseManager;
use AppBundle\Entity\Customer;
class CustomerManager extends BaseManager
{
/**
* #var EntityManager
*/
protected $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param $customerId
* #return null|object
*/
public function loadCustomer($customerId)
{
return $this->getRepository()
->findOneBy(array('id' => $customerId));
}
/**
* #param Customer $customer
*/
public function saveCustomer(Customer $customer)
{
$this->persistAndFlush($customer);
}
/**
* #return \Doctrine\ORM\EntityRepository
*/
public function getRepository()
{
return $this->em->getRepository('AppBundle:Customer');
}
}
Then, I define this manager as a service :
parameters:
app.customer_manager.class: AppBundle\Manager\CustomerManager
services:
app.customer_manager:
class: %app.customer_manager.class%
arguments: [#doctrine.orm.entity_manager]
And Then I use the service in my Controller :
/**
* #Route("/edit/{customerId}", name="customer_edit")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editAction($customerId, Request $request)
{
if (!$customer = $this->get('app.customer_manager')->loadCustomer($customerId)) {
throw new NotFoundHttpException($this->get('translator')->trans('This customer does not exist.'));
}
$form = $this->get('form.factory')->create(new CustomerType(), $customer);
if($form->handleRequest($request)->isValid()) {
$this->get('app.customer_manager')->saveCustomer($customer);
$request->getSession()->getFlashBag()->add('notice', 'Client bien enregistré.');
return $this->redirect(
$this->generateUrl(
'customer_show', array(
'customerId' => $customer->getId()
)
)
);
}
return $this->render('default/customer/add.html.twig', array(
'form' => $form->createView(),
'customer' => $customer
));
}
Is it a good practice, is it too complicated ? Is there any better other way to process in symfony ?
For first question Symfony2 provides CRUD Generator, take a look at this.
For second one you should use Repository Pattern provided by framework, for more information about this checkout following links:
http://msdn.microsoft.com/en-us/library/ff649690.aspx
http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes

Resources