How can I have two User that share the same table? - symfony

I have an entity User with lots of feature built for it.
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
}
This entity is good as is for most of my User.
However, a few of them will have a special ROLE, ROLE_TEACHER.
With this role, I need to store a lot of new variables specially for them.
If I create a new entity Teacher, doctrine creates a new table with every User's data + the Teacher's data.
/**
* #ORM\Entity(repositoryClass="App\Repository\TeacherRepository")
* #Table(name="teachers")
*/
class Teacher extends User
{
/**
* #ORM\Column(type="string", length=64, nullable=true)
*/
protected $test;
public function __construct()
{
parent::__construct();
}
}
What I want, is for Teacher & User to share the users table and have the teachers table only store the extra data. How could I achieve that ?

This is more of system design problem than implementation problem. as #Gary suggested you can make use of Inheritance Mapping which can have Performance issues, I'd rather suggest re think your schema and make use of database normalization techniques to break up your data into more manageable entities.
You can have User entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
/**
* One user has many attibute data. This is the inverse side.
* #OneToMany(targetEntity="UserData", mappedBy="data")
*/
private $data;
}
With other UserData Entity with OneToMany relationship :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserDataRepository")
* #Table(name="user_data")
*/
class UserData
{
/* variables + getter & setter */
#ORM\Id()
private $id;
/**
* Many features have one product. This is the owning side.
* #ManyToOne(targetEntity="User", inversedBy="data")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #ORM\Column(type="string")
*/
private $attribute;
/*
* #ORM\Column(name="value", type="object")
*/
private $value;
}
Now you can have list of user attributes without requiring specific structure to each role. It's scalable and arbitrary.
You can also define same Relation with TeacherData, StudentData or UserProfile Entities with foreign keys and branch your application logic according to the roles. Key is to break data into their separate domains and keep common data in one table. Load related data by querying related entity, this increases readability and makes it easy to break complex structure into manageable codebase.

Related

inconsistent mapping in symfony

I have two entities, User and Notification. In each notification, there is a sender and receiver that are both User entities. But doctrine doesn't like it. The schema validation says:
The mappings ACME\CoreBundle\Entity\Notifications#sender and ACME\CoreBundle\Entity\User#notifications are inconsistent with each other.
Here are the mappings for both entities:
/**
* Notifications
*
* #ORM\Table(name="notifications")
*
*/
class Notifications
{
/**
* #ORM\ManyToOne(targetEntity="WD\UserBundle\Entity\User", inversedBy="notifications")
*/
protected $sender;
/**
* #ORM\ManyToOne(targetEntity="WD\UserBundle\Entity\User", inversedBy="notifications")
*/
protected $receiver;
}
And the User one:
/**
* User
*
* #ORM\Table(name="My_user")
*
*/
class User extends BaseUser
{
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="WD\CoreBundle\Entity\Notifications", mappedBy="receiver")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $notifications;
}
For readability reasons, I did not put the whole entities code, but I believe these should be enough info.
I believe the error comes from the fact I cannot put two 'mappedBy" values in User entity, but I'm not sure. And if it is, then I have no idea how to fix this.
I've found kinda similar cases on this website, but none that was exactly like mine (or I haven't found them).
Any idea how I could fix this?
I think the issue might be that you're having two properties (sender, receiver) and using the same column to map them. If you need to distinguish between sent and received, you'll need to have sender and receiver properties on Notification and then in your user have sentNotifications and receivedNotifications. You can combine them in an un-mapped method in your User if you do need to get everything together in one call such as:
/**
* #var Notification[]|ArrayCollection
*/
public function getAllNotifications()
{
return new ArrayCollection(
array_merge(
$this->sentNotifications->toArray(),
$this->receivedNotifications->toArray()
)
);
}

Symfony: How to configure embedded form to create new child object?

I'm building a user management page where I create or edit users.
The user consists of two entities, user and profile, which have a one to one relationship (I would merge, but can't for historical reasons).
/* User.php - Entity Class
/**
* #var Profile
* #Assert\Type(type="App\Entity\Profile")
* #Assert\Valid()
* #ORM\OneToOne(targetEntity="Profile", mappedBy="user", cascade={"persist"})
*/
private $profile;
/* Profile.php - Profile Entity Class
/**
* #var \App\Entity\User
*
* #ORM\OneToOne(targetEntity="App\Entity\User", inversedBy="profile")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
I built two forms.
One form is the profile form which contains all the essential profile fields (first_name, last_name, email), although does not explicitly contain the relation field (user_id).
The other form is the user form, which contains the basic user fields (username, password), and also includes the profile form.
$builder->add('profile', ProfileForm::class);
When I use this form for editting, everything works fine, and changes to both objects persist. But when I try to use the form to create a new user, it fails, saying that I'm missing user_id.
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'user_id' cannot be null
It seems like this should work, but I'm missing something.
Thanks to #Cerad for this answer. I was not setting the link in my User entity setProfile class.
/**
* Set profile
*
* #param \App\Entity\Profile $profile
*
* #return SfGuardUser
*/
public function setProfile(\App\Entity\Profile $profile = null)
{
$this->profile = $profile;
$this->profile->setUser($this); // Adding this line fixed my issue
return $this;
}

How to implement role / resources ACL in Symfony2

I'm a bit disconcerted by the way access control lists are implemented in Symfony2.
In Zend Framework (versions 1 & 2), a list of resources and a list of roles are defined and each role is assigned a subset of resources it's allowed to access. Resources and roles are therefore the main vocabulary of ACL implementation, which is not the case in Symfony2, where only roles rule.
In a legacy app database, I have tables defining a list of roles, a list of resources and a list of allowed resources for each role (many-to-many relationship). Each user is assigned a role (admin, super admin, editor, and such).
I need to make use of this database in a Symfony2 application.
My resources look like this : ARTICLE_EDIT, ARTICLE_WRITE, COMMENT_EDIT, etc.
My User entity in Symfony implements the Symfony\Component\Security\Core\User\UserInterface interface and therefore has a getRoles) method.
I intend to use this method to define the allowed resources, which means I use roles as resources (I mean that what's called resources in Zend Framework is called roles here).
Do you confirm that I should use this method ?
This means I don't care anymore about the role (admin, editor, ...) of each user, but only about its resources.
I would then use $this->get('security.context')->isGranted('ROLE_ARTICLE_WRITE') in my controllers.
Is this the right way to do it and wouldn't it be a circumvented way to use roles in Symfony?
To answer this question years later, it was pretty easy to solve.
The solution is to mix the notions of roles and resources.
Let's assume a role table, a resource table and and role_resource many to many relation are defined.
Users are stored in a user table.
Here are the corresponding Doctrine entities:
User:
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/**
* #ManyToOne(targetEntity="Role")
* #JoinColumn(name="role_id", referencedColumnName="id")
**/
private $role;
// ...
}
Role:
class Role
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/** #Column(type="string") */
private $name;
/**
* #ManyToMany(targetEntity="Resource")
* #JoinTable(name="role_resource",
* joinColumns={#JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="resource_id", referencedColumnName="id")}
* )
**/
private $resources;
// ...
}
Resource:
class Resource
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/** #Column(type="string") */
private $name;
// ...
}
So now the solution is to implement the getRoles of UserInterface this way:
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Role\Role;
class User implements UserInterface
{
// ...
/**
* #var Role[]
**/
private $roles;
/**
* {#inheritDoc}
*/
public function getRoles()
{
if (isset($this->roles)) {
return $this->roles;
}
$this->roles = array();
$userRole = $this->getRole();
$resources = $userRole->getResources();
foreach ($resources as $resource) {
$this->roles[] = new Role('ROLE_' . $resource);
}
return $this->roles;
}
}
This way, resources attributed to the current user can be checked this way (considering there is a resource whose name is ARTICLE_WRITE):
$this->get('security.context')->isGranted('ROLE_ARTICLE_WRITE')
I think this will answer your question.
http://symfony.com/doc/current/cookbook/security/acl.html
http://symfony.com/doc/current/cookbook/security/acl_advanced.html
$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete');
$mask = $builder->get(); // int(29)
$identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);

An optional OneToOne relationship between entities?

I have a User Entity. This is considered the primary entity in this case and the mere fact it is being used means it is present.
The User entity, has a Store entity. But not all Users will necessarily have a Store entity.
It is worth noting that this is an existing database we are working with, and the id for the User table is the same as the id for the Store table. Name (id) and Value. It's just that in some cases, Store does not have a record for a given User id.
User:
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $store;
...
}
Store:
class Store extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
...
}
This causes problems in the controllers. If a User entity does not have a Store record, it fails with a "Entity not found" exception. This can be dealt with using a try catch easy enough (I haven't been able to find a way to check if an Entity object exists or is just a proxy). If the User does have a store record, all is fine here.
But the big issue I have is especially the Fixtures:
protected function createUser($id)
{
$user = new User();
$user->setId($id);
$user->setEmail($id.'#example.com');
$user->setUserName($id.'_name');
$user->setArea($this->manager->find('Area', 156)); // Global
$this->manager->persist($user);
return $user;
}
When I run Fixtures, this fails. Giving me the error "Integrity constraint violation: 1048 Column 'id' cannot be null". This message disappears if I remove the store entity from User. So in a nutshell, I cannot add a user if it doesn't have a store.
Anyone know what's happening? I've done some looking around and I can't find anything, including doctrine docs, on having optional relationships between Entities. Which I thought would have been a common situation.
Found the solution to this on this doc page:
http://docs.doctrine-project.org/en/latest/tutorials/composite-primary-keys.html#use-case-3-join-table-with-metadata
In my case, rather than the User entity being associated with the Store entity using the id field, the store property in the User entity would be associated to the Store entity by user (an entity object). In return, the Store object will hold a User entity, which is annotated as the entity's id.
I'm sure that's as confusing as hell, so just look at the sample above. Below are my adjusted Entity classes:
User
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store", mappedBy="user")
*/
protected $store;
...
}
Store
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}
Now, if there is no Store record present for a given User, the store property in the User entity will be null. Fixtures runs as expected too.
In addition to the answer above, I also needed to add an inversedBy attribute. Otherwise, an invalid Entity mapping error will be thrown.
Using the entities above, the Store object would need to look like this:
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User", inversedBy="store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}

Automatically updating created_by in the model

I wanted to have a created_by field for my model, say Product, that is automatically updated and I am using FOSUserBundle and Doctrine2. What is the recommended way of inputting the User id into Product?
Can I do it in the Product model? I am not sure how to do so and any help would be wonderful. Thanks!
I want to do something like this in the model, but I don't know how to get the user id.
/**
* Set updatedBy
*
* #ORM\PrePersist
* #ORM\PreUpdate
* #param integer $updatedBy
*/
public function setUpdatedBy($updatedBy=null)
{
if (is_null($updatedBy)) {
$updatedBy = $user->id;
}
$this->updatedBy = $updatedBy;
}
To relate the user to the product you want to associate the two entities:
http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="products")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* You may need to use the full namespace above instead of just User if the
* User entity is not in the same bundle e.g FOS\UserBundle\Entity\User
* the example is just a guess of the top of my head for the fos namespace though
*/
protected $user;
and for the automatic update field you may be after lifecyclecallbacks:
http://symfony.com/doc/current/book/doctrine.html#lifecycle-callbacks
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Product
{
/**
* #ORM\PreUpdate
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
}
EDIT
This discussion talks about getting the container in the entity in which case you could then get the security.context and find the user id from that if you mean to associate the current user to the product they edited:
https://groups.google.com/forum/?fromgroups#!topic/symfony2/6scSB0Kgds0
//once you have the container you can get the session
$user= $this->container->get('security.context')->getToken()->getUser();
$updated_at = $user->getId();
Maybe that is what you are after, not sure it is a good idea to have the container in the entity though, could you not just set the user on the product in the update action in your product controller:
public function updateAction(){
//....
$user= $this->get('security.context')->getToken()->getUser();
$product->setUser($user)
}

Resources