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();
}
}
}
Related
I override (custom operation and service) the DELETE operation of my app to avoid deleting data from DB. What I do is I update a field value: isDeleted === true.
Here is my controller :
class ConferenceDeleteAction extends BaseAction
{
public function __invoke(EntityService $entityService, Conference $data)
{
$entityService->markAsDeleted($data, Conference::class);
}
...
My service :
class EntityService extends BaseService
{
public function markAsDeleted(ApiBaseEntity $data, string $className)
{
/**
* #var ApiBaseEntity $entity
*/
$entity = $this->em->getRepository($className)
->findOneBy(["id" => $data->getId()]);
if ($entity === null || $entity->getDeleted()) {
throw new NotFoundHttpException('Unable to find this resource.');
}
$entity->setDeleted(true);
if ($this->dataPersister->supports($entity)) {
$this->dataPersister->persist($entity);
} else {
throw new BadRequestHttpException('An error occurs. Please do try later.');
}
}
}
How can I hide the "deleted" items from collection on GET verb (filter them from the result so that they aren't visible) ?
Here is my operation for GET verb, I don't know how to handle this :
class ConferenceListAction extends BaseAction
{
public function __invoke(Request $request, $data)
{
return $data;
}
}
I did something; I'm not sure it's a best pratice.
Since when we do :
return $data;
in our controller, API Platform has already fetch data and fill $data with.
So I decided to add my logic before the return; like :
public function __invoke(Request $request, $data)
{
$cleanDatas = [];
/**
* #var Conference $conf
*/
foreach ($data as $conf) {
if (!$conf->getDeleted()) {
$cleanDatas[] = $conf;
}
}
return $cleanDatas;
}
So now I only have undeleted items. Feel free to let me know if there is something better.
Thanks.
Custom controllers are discouraged in the docs. You are using Doctrine ORM so you can use a Custom Doctrine ORM Extension:
// api/src/Doctrine/ConferenceCollectionExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Conference;
use Doctrine\ORM\QueryBuilder;
final class CarCollectionExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if ($resourceClass != Conference::class) return;
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.isDeleted = false OR $rootAlias.isDeleted IS NULL);
}
}
This will automatically be combined with any filters, sorting and pagination of collection operations with method GET.
You can make this Extension specific to an operation by adding to the if statement something like:
|| $operationName == 'conference_list'
If you're not using the autoconfiguration, you have to register the custom extension:
# api/config/services.yaml
services:
# ...
'App\Doctrine\ConferenceCollectionExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
If you also want to add a criterium for item operations, see the docs on Extensions
I would like to know if you can validate a field depending on its value from a form in Symfony?
For example, I have an url and a name within a form associated to it. If the value of the name is 'instagram' let's say, I want that the url to be validated by the class Instagram Validator and so on. I wrote a switch but I get the error:
Call to a member function buildViolation() on null
What I've tried:
public function validateURL($url, $name)
{
$message = '';
switch ($name) {
case "Instagram":
$instagramValidator = new InstagramValidator();
$instagramValidator->validate($url, new Url());
break;
default:
return;
}
return $message;
}
InstagramValidator:
class InstagramValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($url, Constraint $constraint)
{
if (!preg_match('/(?:http:\/\/)?(?:www\.)?instagram\.com\/(?:(?:\w)*#!\/)?(?:pages\/)?(?:[\w\-]*\/)*([\w\-]*)/', $url)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($url))
->addViolation();
}
}
}
So there is a way to handle this?
You've got this error because you didn't pass $context to validator's constructor.
Create your own constraint and use it via Validation::createValidator()->validate():
use Symfony\Component\Validator\Constraints\Regex;
class Instagram extends Regex
{
public function __construct()
{
parent::__construct(['pattern' => '/your regex/']);
}
public function validatedBy(): string
{
return Regex::class . 'Validator';
}
}
Now you can use it:
$violations = Validation::createValidator()->validate($url, new Instagram());
Also, you can use your own validator, you just have to set it in validatedBy function of your constraint.
My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}
I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }
I'm using OhDateExtraValidatorBundle, and because my Pull Request hasn't been accepted yet I'll need to override it locally.
I read the documentation but couldn't make it work for a validator constraint.
Here is what I did :
Created a new Bundle (so I can override more than one extern bundle), called MyDateExtraValidatorBundle
Added the getParent() method :
public function getParent()
{
return 'OhDateExtraValidatorBundle';
}
Wrote my modification in the same path than the original bundle :
namespace MYVENDOR\MyDateExtraValidatorBundle\Validator\Constraints;
use Oh\DateExtraValidatorBundle\Validator\Constraints\DateExtraValidator as ConstraintValidator;
class DateExtraValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
parent::validate($value, Constraint $constraint);
if (null === $value || '' === $value){
return;
}
if(is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
}
if (!$dateTime->getTimestamp())
{
$this->context->addViolation($constraint->invalidMessage);
return;
}
}
}
But it's never loaded.
I also tried using directly the name of my bundle in the entity (with the custom validator) class, but doesn't work either.
use MYVENDOR\MyDateExtraValidatorBundle\Validator\Constraints as OhAssert;
=>
The annotation "#MYVENDOR\MyDateExtraValidatorBundle\Validator\Constraints\DateExtra" [...] does not exist, or could not be auto-loaded.
What's the right way to do ?
Bundle inheritance does not currently allow overriding validation metadata.
There is also a pending issue on this subject.
As a workaround, I would create my own validator.
Acme/FooBundle/Validator/Constraints/MyDateExtra.php
Here, you just extend the base metadata, to keep the messages and configuration.
#Annotation allows your class to be called via annotations.
use Oh\DateExtraValidatorBundle\Validator\Constraints\DateExtra;
/**
* #Annotation
*/
class MyDateExtra extends DateExtra
{
}
Acme/FooBundle/Validator/Constraints/MyDateExtraValidator.php
Here, you extend the behaviour of the base validator with your own logic.
use Oh\DateExtraValidatorBundle\Validator\Constraints\DateExtraValidator;
class MyDateExtraValidator extends DateExtraValidator
{
public function validate($value, Constraint $constraint)
{
parent::validate($value, Constraint $constraint);
if (null === $value || '' === $value) {
return;
}
if(is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
}
if (!$dateTime->getTimestamp()) {
$this->context->addViolation($constraint->invalidMessage);
}
}
}
You should now be able to use it into your model.
use Acme\FooBundle\Validator\Constraints as Extra;
class Foo
{
/**
* #Extra\MyDateExtra
*/
protected $time;
}