Symfony - Entity with a unique value in two fields - symfony

In my user entity, I have two fields (username and email)
and I want them to be mutually unique. I put the Annotation « UniqueEntity » on the top of my entity class and a unique property on each field like this :
/**
* Class User
* #package App\Entity
* #ORM\Table(name="user", uniqueConstraints={
* #ORM\UniqueConstraint(name="app_user_email", columns="email"),
* #ORM\UniqueConstraint(name="app_user_username", columns="username")
* })
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"email"})
* #UniqueEntity(fields={"username"})
*/
class User implements UserInterface, \Serializable
{
....
/**
* #var string
*
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\NotBlank
*/
private $username;
/**
* #var string
*
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\Email
* #Assert\NotBlank
* #Assert\Length(max=255, maxMessage="...")
*/
private $email;
....
}
In my database I can't have 2 identical email or username. This is a valid point. I want moreover that a value in one of the two fields is not found in one of the other two fields.
For example, if an entry in the User table has a specific email value, I do not want to be able to enter the same value in the username field for an other user.
What is the best way to do this ?
The purpose of all this is for a user to be able to authenticate with his username or email and if a mail for a user is equal to a username of another user it will cause a problem.

Hy, in your entity you can add a callback constraint
First add
use Symfony\Component\Validator\Context\ExecutionContextInterface;
and in your class add
/**
* #Assert\Callback
*/
public function validate(ExecutionContextInterface $context, $payload)
{
if ($this->username == $this->email) {
$context->buildViolation('The username can\'t be the email')
->atPath('username')
->addViolation();
}
}
check https://symfony.com/doc/current/reference/constraints/Callback.html for your symfony version

Related

Symfony api-platform: user should retrieve his own entities

I have this entity
/**
* #ApiResource()
* #ORM\Entity(repositoryClass="App\Repository\FeedRepository")
*/
class Feed implements AuthoredEntityInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="myFeeds")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\Column(type="string", length=2083, unique=true)
*/
private $url;
// various getters and setters
}
using the AuthoredEntityInterface I made I can automatically set the user to the logged user.
I'd need to know how to set the collectionOperations so when I am logged in as the user with id = 1, when I call /api/feeds I will only retrieve items with user = 1. If this is possible I would like to do this with an annotation, otherwise any other method is ok.
thanks.
If it is just for connected user, what you need is a current user extension (doctrine extension). Else, you need to create a "subresource' link.
Link to Extension, and to Subresource.
Enjoy :) (and thank you timisorean for the review)

Cascading simple derived identity in Doctrine

How can I cascade a joined model with a OneToOne relationship where by only the User table has an auto increment strategy and the joined Profile model must have an id that matches the User id.
My models look like this:
Company\Model\User:
class User
{
/**
* #Id
* #GeneratedValue
* #Column(type="integer")
* #var int
*/
private $id;
/**
* #OneToOne(targetEntity="Profile", inversedBy="user", cascade={"persist"})
* #var Profile
*/
private $profile;
Company\Model\Profile:
class Profile
{
/**
* #Id
* #OneToOne(targetEntity="User", mappedBy="profile")
* #JoinColumn(name="id")
* #var User
*/
private $user;
When persisting an instance of the User model, it causes the following error to be output:
Entity of type Company\Model\Profile is missing an assigned ID for field 'profile'. The identifier generation strategy for this entity requires the ID field to be populated before EntityManager#persist() is called. If you want automatically generated identifiers instead you need to adjust the metadata mapping accordingly.
The doctrine documentation calls this a simple derived identity, but does not explain how to cascade it.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/tutorials/composite-primary-keys.html#use-case-2-simple-derived-identity
It turns out that the answer is actually quite simple.
First the mappedBy and inversedBy need to be swapped around.
Secondly, when setting the Profile on the User you must set the user on the profile in turn.
Company\Model\User:
class User
{
/**
* #Id
* #GeneratedValue
* #Column(type="integer")
* #var int
*/
private $id;
/**
* #OneToOne(targetEntity="Profile", mappedBy="user", cascade={"persist"})
* #var Profile
*/
private $profile;
public function setProfile(Profile $profile): void
{
$this->profile = $profile;
$this->profile->setUser($this);
}
Company\Model\Profile:
class Profile
{
/**
* #Id
* #OneToOne(targetEntity="User", inversedBy="profile")
* #JoinColumn(name="id")
* #var User
*/
private $user;

UniqueEntity Validation on Column+ManyToOne Relationship

I tried creating a multi column unique validation constraint but it won't work. Here is my model:
/**
* User
*
* #ORM\Entity
* #UniqueEntity({"webinar", "email"})
*/
class User implements UserInterface, \Serializable {
...
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255)
* #Assert\NotBlank()
* #Assert\Email()
* #Assert\Length(max="255")
*/
private $email;
...
/**
* #ORM\ManyToOne(targetEntity="Wefra\ADHSWebinarBundle\Entity\Webinar", inversedBy="registeredUsers")
* #ORM\JoinColumn(name="webinar_id", referencedColumnName="id")
*/
private $webinar;
...
}
What is happening is that, even though the two columns match the validation throws no error.
E.g. user1 hast the email example#example.com and webinar_id 6 and user2 tries to register with the same data without the validation generating an error.
I'm using Symfony2.5
Whoops, i found the problem. The webinar was not set when validating the model. I had to set the webinar_id before validating the Model.

Symfony2: Something like a Role Provider?

In my web application, I want my user's to be able to create roles and add users to them dynamically. The only thing I imagine, is to edit the security.yml every time, but this can't be the best solution, can it? It would be very nice, if there is something like a User Provider for roles, so I can define one which loads the roles from a database (doctrine).
Thanks for your help, hice3000.
Then, you should want to add a role Entity to your model Hice.
You have to know that Symfony2 provides support for dynamic roles too. You have a getRoles() method in the Symfony2 User spec in the API Doc, that your User entity should implement, that forces him to return Roles. These roles must either implement the role interface that specifies a getRole() method that returns, most usually, the role name itself.
You can then add the newly created role directly to your user role list that the getRoles() user method will then return.
Here is an example using annotations :
First role class
/**
* Role class
*
* #ORM\Entity()
*/
class Role implements RoleInterface, \Serializable
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="userRoles")
*/
private $users;
public function __construct()
{
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getRole()
{
return $this->name;
}
}
And the User class
/**
* User
*
* #ORM\Entity()
*/
class User implements UserInterface, \Serializable
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="username", type="string", length=255)
*/
private $username;
/**
* #ORM\ManyToMany(targetEntity="Role", inversedBy="users")
* #ORM\JoinTable(name="user_roles")
*/
private $userRoles;
public function __construct()
{
$this->userRoles = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getRoles()
{
return $this->userRoles->toArray();
}
I've skipped imports and methods to simplify the approach.
EDIT : There is something to know about serialization too. As Sharom commented on github, you musn't serialize users in roles neither roles in users. Just read his post and I think you'll understand :)

FOSUserBundle override mapping to remove need for username

I want to remove the need for a username in the FOSUserBundle. My users will login using an email address only and I've added real name fields as part of the user entity.
I realised that I needed to redo the entire mapping as described here.
I think I've done it correctly but when I try to submit the registration form I get the error:
"Only field names mapped by Doctrine can be validated for uniqueness."
The strange thing is that I haven't tried to assert a unique constraint to anything in the user entity.
Here is my full user entity file:
<?php
// src/MyApp/UserBundle/Entity/User.php
namespace MyApp\UserBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="depbook_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
*
* #Assert\NotBlank(message="Please enter your first name.", groups={"Registration", "Profile"})
* #Assert\MaxLength(limit="255", message="The name is too long.", groups={"Registration", "Profile"})
*/
protected $firstName;
/**
* #ORM\Column(type="string", length=255)
*
* #Assert\NotBlank(message="Please enter your last name.", groups={"Registration", "Profile"})
* #Assert\MaxLength(limit="255", message="The name is too long.", groups={"Registration", "Profile"})
*/
protected $lastName;
/**
* #ORM\Column(type="string", length=255)
*
* #Assert\NotBlank(message="Please enter your email address.", groups={"Registration", "Profile"})
* #Assert\MaxLength(limit="255", message="The name is too long.", groups={"Registration", "Profile"})
* #Assert\Email(groups={"Registration"})
*/
protected $email;
/**
* #ORM\Column(type="string", length=255, name="email_canonical", unique=true)
*/
protected $emailCanonical;
/**
* #ORM\Column(type="boolean")
*/
protected $enabled;
/**
* #ORM\Column(type="string")
*/
protected $salt;
/**
* #ORM\Column(type="string")
*/
protected $password;
/**
* #ORM\Column(type="datetime", nullable=true, name="last_login")
*/
protected $lastLogin;
/**
* #ORM\Column(type="boolean")
*/
protected $locked;
/**
* #ORM\Column(type="boolean")
*/
protected $expired;
/**
* #ORM\Column(type="datetime", nullable=true, name="expires_at")
*/
protected $expiresAt;
/**
* #ORM\Column(type="string", nullable=true, name="confirmation_token")
*/
protected $confirmationToken;
/**
* #ORM\Column(type="datetime", nullable=true, name="password_requested_at")
*/
protected $passwordRequestedAt;
/**
* #ORM\Column(type="array")
*/
protected $roles;
/**
* #ORM\Column(type="boolean", name="credentials_expired")
*/
protected $credentialsExpired;
/**
* #ORM\Column(type="datetime", nullable=true, name="credentials_expired_at")
*/
protected $credentialsExpiredAt;
public function __construct()
{
parent::__construct();
// your own logic
}
/**
* #return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* #return string
*/
public function getLastName()
{
return $this->lastName;
}
/**
* Sets the first name.
*
* #param string $firstname
*
* #return User
*/
public function setFirstName($firstname)
{
$this->firstName = $firstname;
return $this;
}
/**
* Sets the last name.
*
* #param string $lastname
*
* #return User
*/
public function setLastName($lastname)
{
$this->lastName = $lastname;
return $this;
}
}
I've seen various suggestions about this but none of the suggestions seem to work for me. The FOSUserBundle docs are very sparse about what must be a very common request.
I think the easiest way to go about this is to leave the bundle as is and rather setup your user class to have a username equal to the email address.
Do this by overriding the setEmail() method to also set the $username property to the $email parameter and the setEmailCanonical() to also set the $usernameCanonical to the $emailCanonical.
public function setEmail($email){
$this->email = $email;
$this->username = $email;
}
public function setEmailCanonical($emailCanonical){
$this->emailCanonical = $emailCanonical;
$this->usernameCanonical = $emailCanonical;
}
All you will have to do other than this is semantics related. Like having your form label read E-mail instead of the default Username label. You can do this by overriding the translations files. I'll leave this up to you (or someone else) since it might not even be necessary for you.
With this strategy you will have redundant data in your database but it will save you a lot of remapping headache.
If you are using doctrine 2, you can use Life Cycle Events to put your logic inside a callback.
http://docs.doctrine-project.org/en/2.0.x/reference/events.html
/**
* #ORM\PreUpdate()
* #ORM\PrePersist()
*/
public function setUsernameToEmail()
{
$this->username = $this->email;
$this->usernameCanonical = $this->emailCanonical;
}
When I didn't want to require users to enter emails (thus making emails optional in FOSUserBundle), I use Symfony 2.7 + FOSUser+SonataUser+SonataAdmin.
At the same time I needed entered emails to be unique in the system. So Users have 2 options when registering:
Leave email empty
Provide a unique email, that is not yet in the system
Below is my solution that works as expected (I don't claim it to be the cleanest one, but hopefully it will show you a way how to accomplish a similar task)
1) Changes to Entity/User.php
namespace AppBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*
*
* #ORM\AttributeOverrides({
* #ORM\AttributeOverride(name="email",
* column=#ORM\Column(
* type = "string",
* name = "email",
* nullable = true,
* unique = true
* )
* ),
* #ORM\AttributeOverride(name="emailCanonical",
* column=#ORM\Column(
* type = "string",
* name = "email_canonical",
* nullable = true,
* unique = true
* )
* )
* })
*
\*/
class User extends BaseUser
{
2) Executed app/console doctrine:migrations:diff & migrate, database tables were changed as expected adding "DEFAULT NULL" to email and email_canonical fields
3) No matter what I tried, email was being set to NULL, but email_canonical wasn't, it was returning "". I tried manually setting it to NULL in my RegistrationFormHandler, var_dump there confirmed that it was indeed set to NULL when email wasn't entered. But to the database FOSUser would submit "" empty string, which violated UNIQUE constraint I had set for emails, so the solution was to override method in Entity/User.php (as is discussed in previous answers to this question)
// src/AppBundle/Entity/User.php
// ...
public function setEmailCanonical($emailCanonical)
{
// when email is empty, force canonical to NULL
// for some reason by default "" empty string is inserted
$this->emailCanonical = $this->getEmail();
}
4) Change Validation for FOSUserBundle (or SonataUserBundle) in my case , so that it doesn't require email to be set. (I simply removed .. from validation.xml as non of those applied to email anymore)
Copy these 2 files into your config/validation/ directory (for SonataUser+FOSUser it is: Application/Sonata/UserBundle/Resources)
vendor/friendsofsymfony/user-bundle/FOS/UserBundle/Resources/config/storage-validation/orm.xml
above path, config/validation/orm.xml
Rename "Registration" group in those files to your own name, like "myRegistration".
Bind your new validation_group to fos_user in config.yml. If using Sonata User, it is:
sonata_user:
profile:
register:
form:
...
validation_groups:
- myRegistration
- Default
Have fun.

Resources