Symfony2 - Set minimum number of embedded forms - symfony

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..

Related

Symfony 4+ form validation done in a controller action?

I understand how constraints are applied in Form types, etc.
But in the case of implementing a password reset, I need to check for the existence of an email and error if it does not exist.
How might one achieve that in Symfony 4+?
This doesn't seem to solve my issue:
https://symfony.com/doc/current/validation/raw_values.html
You can specify custom constraints for your form input fields.
See : https://symfony.com/doc/current/validation/custom_constraint.html
For custom constraint you need to specify constraint and validator classes like this:
/**
* #Annotation
*/
class NonExistingEmail extends Constraint
{
public $message = 'Email does not exist';
}
class NonExistingEmailValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof NonExistingEmail) {
throw new UnexpectedTypeException($constraint, NonExistingEmail::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) take care of that
if (null === $value || '' === $value) {
return;
}
$user = $this->userRepository->findOneBy(["email" => $value])
if (!$user) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}

Handling Symfony Collection violations for user data

I'm writing an API that will take in a JSON string, parse it and return the requested data. I'm using Symfony's Validation component to do this, but I'm having some issues when validating arrays.
For example, if I have this data:
{
"format": {
"type": "foo"
}
}
Then I can quite easily validate this with PHP code like this:
$constraint = new Assert\Collection(array(
"fields" => array(
"format" => new Assert\Collection(array(
"fields" => array(
"type" => new Assert\Choice(["foo", "bar"])
)
))
)
));
$violations = $validator->validate($data, $constraint);
foreach ($violations as $v) {
echo $v->getMessage();
}
If type is neither foo, nor bar, then I get a violation. Even if type is something exotic like a DateTime object, I still get a violation. Easy!
But if I set my data to this:
{
"format": "uh oh"
}
Then instead of getting a violation (because Assert\Collection expects an array), I get a nasty PHP message:
Fatal error: Uncaught Symfony\Component\Validator\Exception\UnexpectedTypeException: Expected argument of type "array or Traversable and ArrayAccess", "string" given [..]
If there a neat way to handle things like this, without needing to try / catch and handle the error manually, and without having to double up on validation (e.g. one validation to check if format is an array, then another validation to check if type is valid)?
Gist with the full code is here: https://gist.github.com/Grayda/fec0ed7487641645304dee668f2163ac
I'm using Symfony 4
As far as I can see, all built-in validators throw an exception when they are expecting an array but receive something else, so you'll have to write your own validator. You can create a custom validator that first checks if the field is an array, and only then runs the rest of the validators.
The constraint:
namespace App\Validation;
use Symfony\Component\Validator\Constraints\Composite;
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class IfArray extends Composite
{
public $message = 'This field should be an array.';
public $constraints = array();
public function getDefaultOption()
{
return 'constraints';
}
public function getRequiredOptions()
{
return array('constraints');
}
protected function getCompositeOption()
{
return 'constraints';
}
}
And the validator:
namespace App\Validation;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class IfArrayValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof IfArray) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\IfArray');
}
if (null === $value) {
return;
}
if (!is_array($value) && !$value instanceof \Traversable) {
$this->context->buildViolation($constraint->message)
->addViolation();
return;
}
$context = $this->context;
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
}
}
Note that this is very similar to the All constraint, with the major difference being that if !is_array($value) && !$value instanceof \Traversable is true, the code will add a violation instead of throwing an exception.
The new constraint can now be used like this:
$constraint = new Assert\Collection(array(
"fields" => array(
"format" => new IfArray(array(
"constraints" => new Assert\Collection(array(
"fields" => array(
"type" => new Assert\Choice(["foo", "bar"])
)
))
)),
)
));

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
]);
}

Sorting Object Array in Symfony on the basis of date & time

//Suppose Entity Notes has property 'creationdate' & 'getCreationDate()' method to access.
DefaultController extends Controller {
public function indexAction(){
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('Bundle:Notes');
$notes = $repository->findBy(array('userid' => $userId);
//Now I want to sort the notes array as per creation date using usort
usort($notes, array($this,"cmp"));
}
function cmp($a, $b) {
return strtotime($a->getCreationDate()) > strtotime($b->getCreationDate())? -1:1;
}
}
You can set the order in your call to the repository rather than after like so...
$notes = $repository->findBy(
array('userid' => $userId), // search criteria
array('creationdate' => 'ASC') // order criteria
);
I know you said you wanted to use usort but it seems kind of unnecessary.

set role for users in edit form of sonata admin

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

Resources