Symfony using NotEqualTo validator with an object - symfony

According to Symfony docs it should be possible to use a NotEqualTo constraint with objects
value
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
I have the following entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\UniqueConstraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotEqualTo;
/**
* Class Template
* #ORM\Entity(repositoryClass="NoteRepository")
* #ORM\Table(uniqueConstraints={#UniqueConstraint(name="note_unique",columns={"from_id", "to_id"})})
*/
class Note
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #var $id int
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Template")
* #var $from Template
* #NotBlank()
* #NotEqualTo(value="$to")
*/
protected $from;
/**
* #ORM\ManyToOne(targetEntity="Template")
* #NotBlank()
* #var $to Template
*/
protected $to;
/**
* #ORM\Column(type="text")
* #var $notes string
*/
protected $notes;
}
I want to avoid getting $from == $to, how can I configure the constraint to use an instance of the class Template at validation time or more generrally how can I configure the constraint to use an object
Right now if a dump the values that the validator is receiving
class NotEqualToValidator extends AbstractComparisonValidator
{
/**
* {#inheritdoc}
*/
protected function compareValues($value1, $value2)
{
var_dump($value1);
var_dump($value2);
return $value1 != $value2;
}
}
I get
object(AppBundle\Entity\Template)
string '$to' (length=3)

Okay, despite Symfony docs states to be possible pass object to NotEqualTo Constraint it isn't true when using YML or Annotations.
So in order to dynamically unsure Note::$to is equals (or not equals) to Note::$from you could to use either Getters Constraint Targets or Callback Constraint.
However, there is a third option much easier (IMO): Expression Constraint
Expression Constraint approach
This constraint allows you to use an expression for more complex,
dynamic validation. See Basic Usage for an example. See Callback for a
different constraint that gives you similar flexibility.
Here is an example based on your question:
use Symfony\Component\Validator\Constraints as Assert;
/*
* Entity class...
*/
class Note
{
//...
/**
* #ORM\ManyToOne(targetEntity="Template")
* #var $from Template
* #Assert\NotBlank()
* #Assert\Expression('not (value == this.getTo())')
*/
protected $from;
//...
}
That is, just add #Assert\Expression('not (value == this.getTo())') to $from property.
Constraints Targets: Getters approach
use Symfony\Component\Validator\Constraints as Assert;
/*
* Entity class...
*/
class Note
{
//...
/**
* #Assert\isFalse(message='"from" cannot be equals to "to"')
* #return bool True if self::$from is not equals to self::$to
*/
function isFromAndToNotEqual()
{
return !($this->getFrom() != $this->getTo());
}
//...
}
Setting the property path validation:
Be aware taking this approach (Getters As Constraints Targets) and using validation with forms, it'll not show error messages next/closer to form field.
However, you always can set/config the error_mapping option (in Form type) in order to show the custom errors display next to a specific field, thus the solution would be something like this:
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('from')
->add('to')
// more fields...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
//...
# it cause error message display next to
# "from" field (or whaterver field you which).
'error_mapping' => array(
'fromAndToNotEqual' => 'from',
),
));
}
}
Please note the current solutions is based on Symfony 2.7, but it
should be work nicely on other older versions (except some details for
Form class, etc).

Related

API PLATFORM custom identifier in parent class/table

I have entities that make use of Inheritance Mapping Doctrine inheritance. I have a custom identifier that I generate with #ORM\PrePersist(), which is in a trait and this is used in the parent class.
I want to be able to update properties that the child class has, for this reason, I need to run endpoints on the child entity
When I run an item operation, api platform can't find the resource.
PATCH /api/childas/{hash}
NotFoundHttpException
Not Found
api platform, it doesn't recognize hash as identifier. Take the id as your identified, even if it is false and hash is true.
Trait to generate hashes with which I identify the resource
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use Doctrine\ORM\Mapping as ORM;
trait HashableTrait
{
/**
* #ORM\Column(type="string", length=255)
* #ApiProperty(identifier=true)
*/
private $hash;
public function getHash(): ?string
{
return $this->hash;
}
/**
* #ORM\PrePersist()
*/
public function setHash()
{
$this->hash = \sha1(\random_bytes(10));
}
}
Parent class, is the table where the hash will be stored
<?php
namespace App\Entity;
use App\Entity\HashableTrait;
/**
* #ORM\HasLifecycleCallbacks()
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\DiscriminatorMap({
* 1 = "App\Entity\ChildA",
* 2 = "App\Entity\ChildB"
* })
*/
class Parent
{
use HashableTrait;
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #ApiProperty(identifier=false)
*/
private $id;
public function getId(): ?int
{
return $this->id;
}
// Properties, setters, getters
}
Child class, on which I want to perform operations, such as updating some property that belongs to this class
<?php
namespace App\Entity;
class ChildA extends Parent
{
// Custom properties for ChildA
}
Config api platform operations for entity Child
App\Entity\ChildA:
collectionOperations:
post: ~
itemOperations:
post: ~
get: ~
patch: ~
delete: ~
I have thought about using data providers, but I keep getting the error.
The error was because both the hash property in the trait and the id property in the parent entity must be accessible from the entity to use.
Doctrine ORM uses reflection class to get information about attributes and their annotations. ReflectionClass::hasProperty obviously does not allow viewing private properties in the parent class.

Get serialization context groups in controller

I have the problem that depending on user rights, there are different context groups used, and I can't find the place where the context groups are set.
For debugging issues I'm searching an possibility to find out which serialization context group an api call is using. This is my code:
<?php
namespace AppBundle\Controller\Api\Upload;
use AppBundle\Entity\Upload\UploadRepository;
use AppBundle\Entity\Upload\UploadType;
use AppBundle\Entity\Upload\UploadTypeRepository;
use Doctrine\ORM\ORMException;
use GuzzleHttp\Psr7\UploadedFile;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use AppBundle\General\Registry;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
use AppBundle\Entity\Upload\Upload;
use AppBundle\Entity\Application\ApplicationData;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\Serializer\Serializer;
/**
* Class UploadController
*
* #package AppBundle\Controller\Api\Upload
*
* #ApiDoc()
* #ApiResource(attributes={"pagination_enabled"=true})
*/
class UploadController extends Controller
/**
* Get an upload.
*
* #ApiDoc(
* resource=true,
* description="gets an upload",
* )
* #Route(
* name="getUploadSpecial",
* path="/fileuploads/{id}",
* defaults={"_api_resource_class"=Upload::class, "_api_item_operation_name"="getUpload"}
* )
* #Method("GET")
*
* #param Upload $data
*
* #return null|string
*
*/
public function getUploadAction($data)
{
// here I'd like to return the serialization context group
return $data;
}
Is there the possibility to get the serialization context group in the controller?
Well, it seems, the controller isn't the right place to find it.
It is better to dump in
src/Serializer/JsonEncoder.php in function encode, right before the return like this:
/**
* {#inheritdoc}
*/
public function encode($data, $format, array $context = [])
{
dump($data, $context);die;
return $this->jsonEncoder->encode($data, $format, $context);
}

Update another field on slug creation using DoctrineExtensios

I'm using DoctrineExtensions in my Symfony 2 project, i have a simple Entity class where i'm using Sluggable on a property, then i would like to set the value to another property based on the slug, but, even when using Lifecycle Callbacks #ORM\PrePersist, #ORM\PreFlush, at this time the slug property still empty, meaning no slug is generated yet, here is my class, to keep this short, i'm not going to put here the get and set function of each property, just the part of the class that are important for this example(please, read the comments)
<?php
namespace My\LearnBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Banner
*
* #ORM\Table(name="banner")
* #ORM\HasLifecycleCallbacks()
*/
class Banner {
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=128, nullable=false)
* #Assert\NotBlank()
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=256, nullable=false)
* #Gedmo\Slug(fields={"name"})
*/
private $slug;
/**
* #var string
*
* #ORM\Column(name="tracking_url", type="string", length=256, nullable=false)
*/
private $trackingUrl;
/**
* Set slug
*
* #param string $slug
* #return Banner
*/
public function setSlug($slug) {
$this->slug = $slug;
$this->trackingUrl = $slug."/tracking"; //Doesn't work
return $this;
}
/**
* Set trackingUrl value
*
* #ORM\PreUpdate
*/
public function setTrackingUrlValue() {
//the slug is empty. Doesn't work
$this->trackingUrl = $this->slug."/tracking";
return $this;
}
/**
* Set trackingUrl value
*
* #ORM\PreFlush
*/
public function setTrackingUrlValueOnFlush() {
//the slug is empty. Doesn't work
return $this->setTrackingUrlValue();
}
}
What i've tried? well, using the setSlug function but it doesn't work(note comments on example above), seems it is not called. Using Lifecycle Callbacks #ORM\PrePersist, #ORM\PreFlush and #ORM\PreUpdate, doesn't work neither.
Now i solved this in the controller, after calling flush on the EntityManager, setting the property value based on the slug and calling flush again, so, making 2 database query in a single request, one for insert, one for update. I don't want to use an Event Listener because this behavior is just for this particular entity, or exist a way to attach an event listener to a single entity?.
But right now, i would like to know:
why what i was trying to do using Lifecycle Callbacks didn' work?
Why using the setSlug function didn't work?
A cleaner way to accomplish what i want?
thanks
What's probably happening is that the annotated listeners have a higher priority than the one creating the slug (or they have an equal priority in which case the annotated ones get probably added before).
I'm afraid you have to ditch annotations, create an actual listener and tag it for the event registration compiler pass to pick it up. What's nasty with this one is that the bundle seems to use onFlush for creating the slug (code).
Listener
namespace Acme\DemoBundle\Listener;
use Acme\DemoBundle\Model\TrackingUrlUpdateable;
use Doctrine\ORM\Event\OnFlushEventArgs;
class TrackingUrlUpdater
{
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uof = $em->getUnitOfWork();
// Let's process both types of entities in a single loop.
$entities = array_merge(
$uof->getScheduledEntityInsertions(),
$uof->getScheduledEntityUpdates()
);
foreach ($entities as $entity) {
// Using a fictional interface (e.g. for making testing easier).
if (!($entity instanceof TrackingUrlUpdateable)) {
continue;
}
// `Banner::updateTrackingUrl()` would internally change the
// tracking url to the correct one.
$entity->updateTrackingUrl();
// The change-set must be recomputed as its fields were modified
// in the previous step.
$uof->recomputeSingleEntityChangeSet(
$em->getClassMetadata(get_class($entity)),
$entity
);
}
}
}
Registration
What's left now is to register the listener with a lower priority than the Sluggable listener.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
<services>
<service id="acme.listener.tracking_url" class="Acme\DemoBundle\Listener\TrackingUrlUpdater">
<tag name="doctrine.event_listener" event="onFlush" priority="-1" />
</service>
</services>
</container>
Oh, and don't forget to test!
Assuming you're using StofDoctrineExtensionsBundle to integrate the library, the alternative would be to increase the priority of the sluggable listener so that your annotated callbacks would get invoked after the sluggable listener.
A compiler pass would probably do the trick.
namespace Acme\DemoBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class SluggableListenerPriorityChangingPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$id = 'stof_doctrine_extensions.listener.sluggable';
$tag = 'doctrine.event_subscriber';
$definition = $container->getDefintion($id);
$attributes = $definition->getTag($tag);
if (!$attributes) {
throw new \LogicException("The listener (`$id`) must have a `$tag` tag.");
}
$attributes['priority'] = 10;
$definition
->clearTag($tag);
->addTag($tag, $attributes)
;
}
}
The sluggable listener service itself is registered here, if its enabled from the configuration.

Symfony2 Entity Annotation Assert/Callback method not invoked

I've got a problem with Assert/Callback validation. I used this as a sample for my code, but Symfony just ignores the validation function. This is the relevant part of my entity code
namespace Vendor\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo; // gedmo annotations
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContext;
/**
* #Assert\Callback(methods={"isValidFirma"})
* #ORM\Entity(repositoryClass="Vendor\Bundle\Entity\UserProfileRepository")
* #ORM\Table(name="user_profile")
*/
class UserProfile
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
//...
public function isValidFirma(ExecutionContext $context){
$context->addViolationAtSubPath('Firma', 'Company name must be present', array(), null);
// as of sf 2.3 use addViolationAt() instead [reference: https://github.com/propelorm/PropelBundle/issues/234 ]
}
//...
}
isValidFirma is never invoked. I tried validation.yml file instead of annotation as well, no success. I cleared the cache about fifty times, after every change, didn't help either. What could be the problem?
The solution. The problem was in used validator groups. The assert validator has to be a part of that group, or else it wont trigger.
This piece of code in form class file was the culprit:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$vg = array('my-profile');
$resolver->setDefaults(array(
'validation_groups' => $vg
));
}
changing the line with assert to
* #Assert\Callback(methods={"isValidFirma"}, groups={"my-profile"})
did the trick.

FOSUserBundle Form Registration Overriding

I've got a problem when I want to override the FOSUserBundle registration Form.
The deal is, in the User entity, some of the users can have a "Sponsor" (a sponsor is a ManyToOne to the same entity), to be more explicit, this is the User Entity :
<?php
namespace Diz\UserBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* create FK "sponsor_id" referenced to the id field on the same table
* #ORM\ManyToOne(targetEntity="User")
* #ORM\JoinColumn(name="sponsor_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $sponsor;
public function __construct()
{
// import FOSUserBundle properities ->
parent::__construct();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set sponsor
*
* #param Dizsurf\UserBundle\Entity\User $sponsor
*/
public function setSponsor(\Dizsurf\UserBundle\Entity\User $sponsor)
{
$this->sponsor = $sponsor;
}
/**
* Get sponsor
*
* #return Dizsurf\UserBundle\Entity\User
*/
public function getSponsor()
{
return $this->sponsor;
}
}
You see ?
Then, to override the RegistrationFormType, I've created one with the official help :
<?php
namespace Diz\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilder;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilder $builder, array $options)
{
parent::buildForm($builder, $options);
// add your custom field
$builder->add('sponsor', 'fos_user_username');
}
public function getName()
{
return 'diz_user_registration';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Diz\UserBundle\Entity\User', // Ni de modifier la classe ici.
);
}
}
And that's all ! Look like to be pretty simple ! But...
To simply convert the username into a User Entity, FOS advice to use "fos_user_username" in the builder.
Ok for me, but when I test this form :
With a sponsor who does exist, I've got this error "Please enter a password". (of course I've entered the password twice..).
But when I submit a form with an user whose does not exist, the registration form was submitted with success !
Have I done something wrong ?
Thank you for your help ! ;-)
Dizda.
Fixed.
I've just upgraded symfony from 2.0.10 to 2.1 and the problem is not present anymore !

Resources