SYMFONY custom CONSTRAINT -> Pass variable to a custom CONSTRAINT / How CONSTRAINT binded to a form field can OVERRIDE CONSTRAINT in ANNOTATION - forms

My goal: I built a custom constraint in SYMFONY, I needed to pass a variable to that constraint.
The context: The constraint do a check if a value is unique in the DB, if it is not, it raises a CONSTRAINT alert. That works alright when the FORM is used to create a new tuple in the DB but if it is an edit it raises an exception which should be bypass by checking that the value already existing, exists for the tuple Id being edited.
Hence I needed to pass the Id of the tuple being edited to my constraint check.
At first I implemented my custom constraint in my entity:
class MyEntity{
/**
* #MyBundleAssert\CheckValueAlreadyInDB(
* message = "already_exists_in_db",
* fieldToSearch = "my_value",
* tableToSearch = "my_table"
*)
*/
private myValue;
}
As one can see, I did not find a way to implement a way to pass a VARIABLE using the constraint with ANNOTATION. By searching, I understood I could do that by using the __construct() of my custom constraint class:
/**
* #Annotation
*/
class CheckValueAlreadyInDB extends Constraint{
public $message;
public $fieldToSearch;
public $tableToSearch;
public $idToCheck;
public $idToCheckFieldName;
public function __construct($options){
if(count($options)>0){
$this->idToCheck = $options['idToCheck'];
$this->idToCheckFieldName = $options['idToCheckFieldName'];
$this->fieldToSearch = $options['fieldToSearch'];
$this->tableToSearch = $options['tableToSearch'];
$this->message = $options['message'];
}
}
public function validatedBy()
{
return 'validator_check_value_already_in_db';
}
}
And, the ConstraintValidator extended class linked to it:
class CheckValueAlreadyInDBValidator extends ConstraintValidator
{
private $con;
public function __construct($con){
$this->con = $con;
}
public function validate($value, Constraint $constraint)
{
////My stuff to get a record from the DB////
$sel = new PdoSelect($this->con);
$search = $sel->returnRecordsInTableForSpecificKey([$constraint->fieldToSearch],[$value], $constraint->tableToSearch,false);
//////////////////////////////////////////////
$sameId = false;
if($constraint->idToCheck!==null){
$idToCheckInRetrieveRecord = $search->{$constraint->idToCheckFieldName};
$sameId = ($idToCheckInRetrieveRecord==$constraint->idToCheck)?true:false;
}
if($search!=null&&!$sameId){
$this->context->buildViolation($constraint->message)
->setParameter('%string%', $value)
->addViolation();
}
}
}
With service:
validator.unique.check_value_already_in_db:
class: MyBundle\Form\CustomConstraints\CheckValueAlreadyInDBValidator
arguments: ['#doctrine.dbal.default_connection']
tags:
- { name: validator.constraint_validator, alias: validator_check_value_already_in_db }
I my FORM (AbstractType extended class) for the field regarding myValue, I did edit the constraints attribute.
class MyEntityType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
....
$builder->add('myValue',****Type::class,array(
'constraints' => array(
new CheckValueAlreadyInDB(array(
'idToCheck'=>$options['data']->getId(),
'idToCheckFieldName'=>'id',
'fieldToSearch'=>'my_value',
'tableToSearch'=>'my_table',
'message' => "value_already_exists_in_db"))
)
));
...
}
}
I thought that the CONSTRAINT defined in the buildForm() would override the one defined in the * #MyBundleAssert\CheckValueAlreadyInDB(..) of MyEntity class (which should be the default behaviour). But It did not! I had to delete the ANNOTATION above MyEntity to make the constraint work as defined in the buildForm().
Does anyone know if there is a setting that could permit to have a constraint in a buildForm() overriding one existing as an ANNOTATION in MyEntity, but still let the ANNOTATION above a field in MyEntity be the default behavior? Or is there is a way to pass VARIABLE to ANNOTATIONS?

I found the solution.
My mistake was to try to use constraints in class MyEntityType extends AbstractType:
$builder->add('myValue',****Type::class,array(
'constraints' => array(
new CheckValueAlreadyInDB(array(
'idToCheck'=>$options['data']->getId(),
'idToCheckFieldName'=>'id',
'fieldToSearch'=>'my_value',
'tableToSearch'=>'my_table',
'message' => "value_already_exists_in_db"))
)
));
Update:
DON'T USE IT HERE
Have a look at class-constraint-validator section in the doc.
Implement the ConstraintValidator extended class above the class of the Entity where the validator has to execute its check and not above one attribute of the Entity class. That way one can have access to other attributes of the entity and use it as conditionals in the ConstraintValidator extended class.

Related

Add own sys_category to record with extbase

I made the table of one of my models categorizable which works fine if I set categories in the Backend. If I try to add categories via a frontend form I always get the error:
Call to a member function attach() on null
and have no clue why that is so. Maybe anyone of you can help.
In controller I try to add as usual, find the category
$videoCat = $this->categoryRepository->findByUid(28);
and add it like that
$this->video->addTxVideoCat($videoCat);
That's where the error occurs.
Find below how I added the category to the model.
ext_tables.sql `tx_video_cat int(11) DEFAULT '0' NOT NULL,`
extended tca in TCA/Overrides/sys_template.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::makeCategorizable(
'addvideo',
'tx_addvideo_domain_model_video',
// Do not use the default field name ("categories") for pages, tt_content, sys_file_metadata, which is already used
'tx_video_cat',
array(
// Set a custom label
'label' => 'LLL:EXT:addvideo/Resources/Private/Language/locallang.xlf:video_categories',
// This field should not be an exclude-field
'exclude' => FALSE,
// Override generic configuration, e.g. sort by title rather than by sorting
// string (keyword), see TCA reference for details
'l10n_mode' => 'exclude',
// list of keywords, see TCA reference for details
'l10n_display' => 'hideDiff',
)
);
Created extended category repository
namespace Pixelink\Addvideo\Domain\Repository;
class CategoryRepository extends \TYPO3\CMS\Extbase\Domain\Repository\CategoryRepository
Created extended model
namespace Pixelink\Addvideo\Domain\Model;
class Category extends \TYPO3\CMS\Extbase\Domain\Model\Category
{}
Category mapping in typoscript
plugin.tx_addvideo{
persistence {
classes{
Pixelink\Addvideo\Domain\Model\Category {
mapping {
tableName = sys_category
columns {
}
}
}
}
}
}
and in Model\Video.php I added the following
/**
* txVideoCat
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelink\Addvideo\Domain\Model\Category>
*/
protected $txVideoCat = null;
/**
* Get categories
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelink\Addvideo\Domain\Model\Category>
*/
public function getTxVideoCat()
{
return $this->txVideoCat;
}
/**
* Set categories
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $txVideoCat
*/
public function setTxVideoCat($txVideoCat)
{
$this->txVideoCat = $txVideoCat;
}
/**
* Add category to a post
*
* #param \Pixelink\Addvideo\Domain\Model\Category $txVideoCat
*/
public function addTxVideoCat(\Pixelink\Addvideo\Domain\Model\Category $txVideoCat)
{
$this->txVideoCat->attach($txVideoCat);
}
You should initialize your property in your Video model constructor:
public function __construct()
{
$this->txVideoCat = new ObjectStorage();
}
Your $this->txVideoCat is null. Use initiailizeObject() method to assign it:
public function initializeObject()
{
$this->txVideoCat = new ObjectStorage();
}

symfony validator as service not working

I need a validator that needs to query database. This means I have to create a Constraint a ConstraintValidator and setup a service to inject EntityManager.
I did it, and researched official documentation and lots of posts and couldn't make it work. It seems that validatedBy() is not able to start the service (injecting then the EntityManager).
Here is the code I'm struggling with:
services.yml
validator.frontend.class:
class: Project\UsersBundle\Validation\Constraints\ConstrainsActiveValidator
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constrain_validator, alias: the_alias }
ConstrainsActive.php
namespace Project\UsersBundle\Validation\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
* */
class ConstrainsActive extends Constraint
{
public function validatedBy()
{
return 'the_alias'; // get_class($this).'Validator';
}
}
ConstrainsActiveValidator.php
namespace Project\UsersBundle\Validation\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
class ConstrainsActiveValidator extends ConstraintValidator
{
protected $em;
public function __construct(EntityManager $v)
{
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
$this->context->buildViolation('This name sounds totally fake2!')
->atPath('useEmail')
->addViolation();
}
}
Update:
Thanks for the typo issue. It was a mistake when adapting code to posting here. I fixed! :)
Here you are the error I'm always getting:
Attempted to load class "the_alias" from the global namespace in /var/www/Project/current/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Validator/ConstraintValidatorFactory.php line 71. Did you forget a use statement for this class?
I temporally edited the file ConstraintValidatorFactory.php, and did a var_dump for the var $this->validators and get the response:
array (size=4)
'validator.expression' => string
'validator.expression' (length=20)
'Symfony\Component\Validator\Constraints\EmailValidator' => string
'validator.email' (length=15) 'security.validator.user_password' =>
string 'security.validator.user_password' (length=32)
'doctrine.orm.validator.unique' => string
'doctrine.orm.validator.unique' (length=29)
It seems like symfony framework didn't consider the alias "the_alias" set up in my custom service validator.frontend.class, because it didn't come in the var_dump.
I hope this can give you some clue. Thanks!
You code is ok, check the typo in the construct method where you name the EntityManager as $v and ferer it as $m.
Change the construct of ConstrainsActiveValidatorclass as:
public function __construct(EntityManager $v)
{
$this->em = $v;
}
What error do you have?
Just for people that has the same issue. I finally decided to set checking form parameters inside a type, I mean:
Bundle/Form/addType.php
adding an event listener to the $builder object:
public function buildForm(FormBuilderInterface $builder, array $options) {
...
$builder->addEventListener(FormEvents::POST_SUBMIT, function ($event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) return;
// Checking comes here!!!
// ......
}
}
In my opinion, validators are great but when a project gets bigger, checking information inside the event is the best way. You can also use some constraints that provide symfony as EmailConstraints:
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
Answering myself. Let's validate that an entered slug for a post in a blog is unique:
Let's assume we have a bundle called AppBundle
1) Create the subfolder structure "Validator/Contraints" inside src/AppBundle.
2) Inside .../Validator/Constraints, create a file called SlugUnique.php (The constraint):
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class SlugUnique extends Constraint
{
public $message = 'Slug "{{ slug }}" already taken';
public function getTargets()
{
return array(self::PROPERTY_CONSTRAINT);
}
public function validatedBy()
{
return 'app.validator.blog.slug_unique';
}
}
3) Create a file called SlugUniqueValidator.php at the same folder level (The constraint validator):
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\Blog as BlogEntity;
/**
* #Annotation
*/
class SlugUniqueValidator extends ConstraintValidator
{
/** #var EntityManager */
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
/** #var BlogEntity $blogEntity */
$blogEntity = $this->context->getRoot()->getViewData();
if (null !== $this->em->getRepository('AppBundle:Blog')->findBy(['slug' => $blogEntity->slug])) {
$this->context->buildViolation($blogEntity->slug)
->atPath('slug')
->addViolation();
}
}
}
4) Create an entry into services.yml with properly tag items:
app.validator_constraints_blog.slug_unique_validator:
class:AppBundle\Validator\Constraints\SlugUniqueValidator
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: 'validator.constraint_validator', alias: 'app.validator.blog.slug_unique' }
Note that the value for the alias parameter must match the value returned by validatedBy in the SlugUnique class constraint.
5) Finally, assign such validation to the corresponding entity field:
namespace AppBundle\Entity;
use AppBundle\Validator\Constraints as ValidatorConstraints;
class Blog
{
...
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255, nullable=false)
* #ValidatorConstraints\SlugUnique
*/
private $slug;
public getSlug()
...
Notice that depending on the Entity fieldName and the form fieldName, that you may have to set the parameter 'property_path' when building the form type. Example:
$builder->add(
'slug', TextType::class,
array('required' => false, 'property_path' => 'slug')
);
Please note that you can inject a custom service into SlugUniqueValidator instead on the EntityManager for better separation of concerns and bests practices.

Symfony2 validation getters error for a specific field

I'm using Symfony2 validation getters to validate if endDate is later than startDate. It works fine except that I want the error to appear for endDate field not above the whole form. All other validation errors appears above the field it validates. Is there any way to do that?
validation part in my Entity:
/**
* #Assert\True(message = "Invalid date")
*/
public function isDatesValid()
{
return ($this->startDate < $this->endDate);
}
I don't think there is a way to achieve that with a getter validation.
Instead you could use a custom constraint: http://symfony.com/doc/current/cookbook/validation/custom_constraint.html
You'll need to overide the getTargets() method in order to access all properties:
class DateRangeConstraint extends Constraint {
public $message = 'Your message';
public function getTargets() {
return Constraint::CLASS_CONSTRAINT;
}
}
class DateRangeConstraintValidator extends ConstraintValidator {
public function validate($obj, Constraint $constraint) {
if ($obj->startDate() < $obj->endDate()) {
$this->context->addViolationAt(
'startDate',
$constraint->message,
array(),
null
);
}
}
}
Assign this constraint to one or both properties.

Simple recovery password form

again, im trying a simple form for recovery forgot user password.
The form only contains a email input. The form handler, receive the email, search the user, restore your password and send a email with the new password. Is important says that user not is authenticated.
So, i need create a custom constraint that check if the email exists in the db. I have an method of a own service that do it. So, i need bind the method with the constraint.
The custom validation class is:
class ExistEmailValidator extends ConstraintValidator
{
protected $userService;
public function setUserService($userService)
{
echo "Setter...";
var_dump($userService);
die();
$this->userService = $userService;
}
public function validate($value, Constraint $constraint)
{
if($this->userService->existUserEmail($value) == false){
$this->context->addViolation($constraint->message, array('%string%' => $value));
}
}
}
I inject the service from services.yml:
validator.unique.ExistEmailValidator:
class: AppsManantiales\CommonBundle\Validator\Constraints\ExistEmailValidator
tags:
- {name: validator.constraint_validator, alias: user_email_validator}
calls:
- [setUserService, ["userservice"]]
The problem is taht, never inject the service, because Symfony says:
Error: Call to a member function existUserEmail() on a non-object
Note: the user service id is: "userservice".
php app/console container:debug | grep UserService
userservice container AppsManantiales\CommonBundle\Services\UserService
Any ideas ?
UPDATE 1
My constraint class is:
class ExistEmail extends Constraint{
public $message = 'El e-mail "%string%" no pertenece a ningun usuario del sistema.';
public function validatedBy()
{
return get_class($this).'Validator';
}
}
And the form definition is:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('anUserEmail', 'email',
array(
'constraints' => array(
new ExistEmail()
)
));
}
Replace,
public function validatedBy()
{
return get_class($this).'Validator';
}
in your ExistEmail constraint class by,
public function validatedBy()
{
return 'user_email_validator';
}
in order to map your constraint with the right instance of ExistEmailValidator (The one instantiated in the service container).

Zend DB Table and Model one to one

I have class that representing model of user with foreign key with is id of picture .
class Model_User extends Model_AbstractEntity
{
protected $u_id;
protected $u_email;
protected $u_firstname;
protected $u_lastname;
protected $u_password;
protected $u_salt;
protected $u_created_at;
protected $u_updated_at;
protected $u_fb;
protected $u_status;
protected $u_r_id;
protected $u_p_id;
}
Class with is responsible for picture model look like this:
class Model_Picture extends Model_AbstractEntity
{
protected $p_id;
protected $p_created_at;
protected $p_updated_at;
protected $p_caption;
protected $p_name;
protected $p_basePath;
protected $p_available;
protected $p_u_id;
}
This is only model part with is getting data from database.
Foreing key is u_p_id and key in picture is p_id
My problem is that when doing select() by Zend db table it returning me data with foreign key but how can I know which part of return data is picture part to set the proper picture model.... how to do it in proper way no to do 2 queries one for user and second for picture to create 2 associative objects.
I'm talking now about relation ont to one but maybe will be one to many..
Typically your entity models will not exist in void they will exist in concert with some type of Data Mapper Model. The Mapper will typically be charged with gathering the data from whatever source is handy and then constructing the entity model.
For example I have a music collection that has an album entity:
<?php
class Music_Model_Album extends Model_Entity_Abstract implements Interface_Album
{
//id is supplied by Entity_Abstract
protected $name;
protected $art;
protected $year;
protected $artist; //alias of artist_id in Database Table, foreign key
protected $artistMapper = null;
/**
* Constructor, copied from Entity_Abstract
*/
//constructor is called in mapper to instantiate this model
public function __construct(array $options = null)
{
if (is_array($options)) {
$this->setOptions($options);
}
}
/**
* Truncated for brevity.
* Doc blocks and normal getters and setters removed
*/
public function getArtist() {
//if $this->artist is set return
if (!is_null($this->artist) && $this->artist instanceof Music_Model_Artist) {
return $this->artist;
} else {
//set artist mapper if needed
if (!$this->artistMapper) {
$this->artistMapper = new Music_Model_Mapper_Artist();
}
//query the mapper for the artist table and get the artist entity model
return $this->artistMapper->findById($this->getReferenceId('artist'));
}
}
//set the artist id in the identity map
public function setArtist($artist) {
//artist id is sent to identity map. Can be called later if needed - lazy load
$this->setReferenceId('artist', $artist);
return $this;
}
//each album will have multiple tracks, this method allows retrieval as required.
public function getTracks() {
//query mapper for music track table to get tracks from this album
$mapper = new Music_Model_Mapper_Track();
$tracks = $mapper->findByColumn('album_id', $this->id, 'track ASC');
return $tracks;
}
}
In the mapper I would build the entity model like:
//excerpt from Model_Mapper_Album
//createEntity() is declared abstract in Model_Mapper_Abstract
public function createEntity($row)
{
$data = array(
'id' => $row->id,
'name' => $row->name,
'art' => $row->art,
'year' => $row->year,
'artist' => $row->artist_id,//
);
return new Music_Model_Album($data);
}
to use this method in a mapper method, might look like:
//this is actually from Model_Mapper_Abstract, b ut give the correct idea and will work in any of my mappers.
//this returns one and only one entity
public function findById($id)
{
//if entity id exists in the identity map
if ($this->getMap($id)) {
return $this->getMap($id);
}
//create select object
$select = $this->getGateway()->select();
$select->where('id = ?', $id);
//fetch the data
$row = $this->getGateway()->fetchRow($select);
//create the entity object
$entity = $this->createEntity($row);
//put it in the map, just in case we need it again
$this->setMap($row->id, $entity);
// return the entity
return $entity;
}
I have seen Entities and Mappers built in many different ways, find the method that you like and have fun.
A lot of code has been left out of this demonstration as it doesn't really apply to the question. If you need to see the complete code see it at GitHub.