So I'm beginning to struggle with Doctrine2 when it comes to a many-to-many relation for a project where the relation has 1 extra column.
I have the following tables:
Profiles
id
extra data
Skills
id
name
profile_has_skills
profile_id
skill_id
level
Now I added the level column later on, and noticed some problems happening, of course I am missing level now whenever I try to create the relation.
My question is, with the code below, how would I go over to add this in my doctrine?
My controller:
public function store(Request $request)
{
$time = new DateTime();
$this->validate($request, [
'name' => 'required',
'lastname' => 'required',
'gender' => 'required',
'profile_skills' => 'required'
]);
$this->em->getConnection()->beginTransaction();
try {
$profile = new Profile(
$request->input('company_id'),
$request->input('name'),
$request->input('lastname'),
$request->input('gender'),
new DateTime(),
$time,
$time
);
$company = $this->em->getRepository(Company::class)->find($request->input('company_id'));
$profile->addCompany($company);
foreach($request->input('profile_skills') as $skill => $level) {
$skill = $this->em->getRepository(Skill::class)->find($skill);
$skill->level = $level;
$profile->addSkill($skill);
}
$this->em->persist($profile);
$this->em->flush();
$this->em->getConnection()->commit();
} catch (OptimisticLockException $e) {
$this->em->getConnection()->rollBack();
throw $e;
}
return redirect(route('profiles.index'));
}
My ProfileHasSkill entity looks as follow:
/**
* #ORM\Entity
* #ORM\Table(name="profile_has_skill")
*
*/
class ProfileHasSkill
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #Column(type="integer", name="profile_id")
*/
protected $profile_id;
/**
* #Column(type="integer", name="skill_id")
*/
protected $skill_id;
/**
* #Column(type="integer", name="level")
*/
protected $level;
/**
* #param $profile_id
* #param $skill_id
* #param $level
*/
public function __construct($profile_id, $skill_id, $level = 0)
{
$this->profile_id = $profile_id;
$this->skill_id = $skill_id;
$this->level = $level;
}
And my addSkill method inside the profile entity is as follow:
public function addSkill(Skill $skill)
{
if ($this->skills->contains($skill)) {
return;
}
return $this->skills->add($skill);
}
But anytime I try to run this it gives me the following error
An exception occurred while executing
'INSERT INTO profile_has_skill (profile_id, skill_id) VALUES (?, ?)'
with params [3, 2]: SQLSTATE[HY000]: General error: 1364 Field 'level'
doesn't have a default value
Now I know one way to get rid of this error is setting a default value in the database, but I much rather just find out why it's not picking up my skill level that I'm also passing?
As per my solution which has worked, by reading another question passed by #Nicola Havric - Read as follow That doctrine does not support extra columns in a many-to-many relation. Thus you should use the relation as it's own entity. My own solution was to change the way I wanted it to run with flushing.
In my controller I changed my code as follow:
try {
$profile = new Profile(
$request->input('company_id'),
$request->input('name'),
$request->input('lastname'),
$request->input('gender'),
new DateTime(),
$time,
$time
);
$company = $this->em->getRepository(Company::class)->find($request->input('company_id'));
$profile->addCompany($company);
//Flush the user, so I can grab it's profile ID
$this->em->persist($profile);
$this->em->flush();
foreach($request->input('profile_skills') as $skill => $level) {
$skill = $this->em->getRepository(Skill::class)->find($skill);
$skill->level = $level;
$profile->addSkill($skill);
}
$this->em->getConnection()->commit();
Inside my Profile Entity function:
public function addSkill(Skill $skill)
{
//I left this check since it only checks if the relation is set already. If so, it will skip it.
if ($this->skills->contains($skill)) {
return;
}
//Since this function gets called inside a loop, I can call the entity to add a new "relation" to the table.
(new ProfileHasSkill($this->getId(), $skill, $skill->level))->addSkill($this->getId(), $skill, $skill->level);
return true;
}
Inside my ProfileHasSkill entity:
public function addSkill($profileId, $skill)
{
//Creating a new ProfileHasSkill inside the table.
$profileSkill = new ProfileHasSkill(
$profileId,
$skill->getId(),
$skill->level
);
/*Since I do a roll-back inside my controller in case something goes wrong.
I decided to add the flush here.
As far no additional checks where needed in my case
since I require a Profile instance and a Skill instance inside the Profile entity.*/
EntityManager::persist($profileSkill);
EntityManager::flush();
}
The thing with many-to-many relationships is that any additional columns other than two primary keys from both tables are considered pivot columns, when attaching entities to such relationships you want to use the method attach which accepts array of ids as first parameter and an array with pivot columns, take the following into consideration.
public function addSkill(Skill $skill)
{
if ($this->skills->contains($skill)) {
return;
}
//Dunno what this method does
return $this->skills->add($skill);
//But this is the correct way of adding a skill
$this->skills->attach($skill->id, ['level' => $skill->level]);
}
Hope this can clarify few things even though Eloquent was used as an example; here is the manual link for the above code.
Related
I have a notes model. Which has a polymorphic 'noteable' method that ideally anything can use. Probably up to 5 different models such as Customers, Staff, Users etc can use.
I'm looking for the best possible solution for creating the note against these, as dynamically as possible.
At the moment, i'm adding on a query string in the routes. I.e. when viewing a customer there's an "Add Note" button like so:
route('note.create', ['customer_id' => $customer->id])
In my form then i'm checking for any query string's and adding them to the post request (in VueJS) which works.
Then in my controller i'm checking for each possible query string i.e.:
if($request->has('individual_id'))
{
$individual = Individual::findOrFail($request->individual_id_id);
// store against individual
// return note
}elseif($request->has('customer_id'))
{
$customer = Customer::findOrFail($request->customer_id);
// store against the customer
// return note
}
I'm pretty sure this is not the best way to do this. But, i cannot think of another way at the moment.
I'm sure someone else has come across this in the past too!
Thank you
In order to optimize your code, dont add too many if else in your code, say for example if you have tons of polymorphic relationship then will you add tons of if else ? will you ?,it will rapidly increase your code base.
Try instead the follwing tip.
when making a call to backend do a maping e.g
$identifier_map = [1,2,3,4];
// 1 for Customer
// 2 for Staff
// 3 for Users
// 4 for Individual
and so on
then make call to note controller with noteable_id and noteable_identifier
route('note.create', ['noteable_id' => $id, 'noteable_identifier' => $identifier_map[0]])
then on backend in your controller you can do something like
if($request->has('noteable_id') && $request->has('noteable_identifier'))
{
$noteables = [ 'Customers', 'Staff', 'Users','Individual']; // mapper for models,add more models.
$noteable_model = app('App\\'.$noteables[$request->noteable_identifier]);
$noteable_model::findOrFail($request->noteable_id);
}
so with these lines of code your can handle tons of polymorphic relationship.
Not sure about the best way but I have a similar scenario to yours and this is the code that I use.
my form actions looks like this
action="{{ route('notes.store', ['model' => 'Customer', 'id' => $customer->id]) }}"
action="{{ route('notes.store', ['model' => 'User', 'id' => $user->id]) }}"
etc..
And my controller looks this
public function store(Request $request)
{
// Build up the model string
$model = '\App\Models\\'.$request->model;
// Get the requester id
$id = $request->id;
if ($id) {
// get the parent
$parent = $model::find($id);
// validate the data and create the note
$parent->notes()->create($this->validatedData());
// redirect back to the requester
return Redirect::back()->withErrors(['msg', 'message']);
} else {
// validate the data and create the note without parent association
Note::create($this->validatedData());
// Redirect to index view
return redirect()->route('notes.index');
}
}
protected function validatedData()
{
// validate form fields
return request()->validate([
'name' => 'required|string',
'body' => 'required|min:3',
]);
}
The scenario as I understand is:
-You submit noteable_id from the create-form
-You want to remove if statements on the store function.
You could do that by sending another key in the request FROM the create_form "noteable_type". So, your store route will be
route('note.store',['noteableClass'=>'App\User','id'=>$user->id])
And on the Notes Controller:
public function store(Request $request)
{
return Note::storeData($request->noteable_type,$request->id);
}
Your Note model will look like this:
class Note extends Model
{
public function noteable()
{
return $this->morphTo();
}
public static function storeData($noteableClass,$id){
$noteableObject = $noteableClass::find($id);
$noteableObject->notes()->create([
'note' => 'test note'
]);
return $noteableObject->notes;
}
}
This works for get method on store. For post, form submission will work.
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Requests\NoteStoreRequest $request
* #return \Illuminate\Http\Response
*/
public function store(NoteStoreRequest $request) {
// REF: NoteStoreRequest does the validation
// TODO: Customize this suffix on your own
$suffix = '_id';
/**
* Resolve model class name.
*
* #param string $name
* #return string
*/
function modelNameResolver(string $name) {
// TODO: Customize this function on your own
return 'App\\Models\\'.Str::ucfirst($name);
}
foreach ($request->all() as $key => $value) {
if (Str::endsWith($key, $suffix)) {
$class = modelNameResolver(Str::beforeLast($key, $suffix));
$noteable = $class::findOrFail($value);
return $noteable->notes()->create($request->validated());
}
}
// TODO: Customize this exception response
throw new InternalServerException;
}
I tried to look up on Google but didn't find anyone with such a problem. I think I did everything like the documentation guides but I guess I'm missing something
So I have a form with checkbox like this:
$builder->add(
'productTypes',
EntityType::class,
array(
'label' => 'Available for products',
'class' => 'ShopBundle:ProductType',
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
'by_reference' => false,
)
);
When I'm editing everything goes smooth, I can edit existing entry and check or uncheck this checkbox, it saves properly, but when I try to add new Object I get error:
PHP Fatal error: Call to a member function add() on null in
C:\xampp\htdocs\uniacar-sf\src\ShopBundle\Entity\ProductAttribute.php
on line 188
This is my controller action:
public function editAction(Request $request, $id = null)
{
$this->setMenuTab('cars', 'admin');
$productTypes = new ArrayCollection();
if (!empty($id)) {
$attribute = $this->getRepo(ProductAttribute::class)->find($id);
$this->setTitle('admin.cars.attributes.edit');
foreach ($attribute->getProductTypes() as $value) {
$productTypes->add($value);
}
} else {
$attribute = new ProductAttribute();
$this->setTitle('admin.cars.attributes.new');
}
$form = $this->createForm(ProductAttributeForm::class, $attribute);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$attribute = $form->getData();
foreach ($productTypes as $productType) {
if (false === $attribute->getProductTypes()->contains($productType)) {
$productType->getAttributes()->removeElement($attribute);
$this->db()->persist($productType);
}
}
$this->db()->persist($attribute);
$this->db()->flush();
return $this->redirectToRoute('carAdmin', array('tab' => 'attributes'));
}
$this->setVariables(
array(
'form' => $form->createView(),
'attribute' => $attribute,
)
);
return $this->response();
}
$this->db() is my shortcut for $this->getDoctrine()->getManager()
And this is definition part of ProductAttribute that relates to ProductType:
/**
* Constructor
*/
public function __construct() {
$this->productTypes = new ArrayCollection();
}
/**
* Many Attributes have Many ProductTypes
* #ORM\ManyToMany(targetEntity="ProductType", mappedBy="attributes", cascade={"persist"})
*/
private $productTypes;
/**
* #param ProductType $productType
*/
public function addProductType(ProductType $productType)
{
$this->productTypes->add($productType);
$productType->addProductAttribute($this);
}
/**
* #param ProductType $productType
*/
public function removeProductType(ProductType $productType)
{
$this->productTypes->removeElement($productType);
}
Also there is part of ProductType Entity that relates to ProductAttribute:
/**
* Constructor
*/
public function __construct() {
$this->attributes = new ArrayCollection();
}
/**
* Many ProductTypes have Many Attributes
* #ORM\ManyToMany(targetEntity="ProductAttribute", inversedBy="productTypes")
* #ORM\JoinTable(name="product_type_to_attribute")
*/
private $attributes;
/**
* #param ProductAttribute $attribute
*/
public function addProductAttribute(ProductAttribute $attribute)
{
if (!$this->attributes->contains($attribute)) {
$this->attributes->add($attribute);
}
}
public function removeProductAttribute(ProductAttribute $attribute)
{
$this->attributes->removeElement($attribute);
}
I tried to follow Symfony Embed Form Tutorial (How to Embed a Collection of Forms)
I know that in this case there is no embeded collection (I have another field in this Entity, that is embeded collection of forms and it works just fine) but from what I understand relations are the same in this case, it's many to many so I have to tell the Symfony how to treat relations, add and remove objects.
I dumped data that comes in POST but it's the same as for edition - productType is there. Any ideas why do I get this error?
It fires in ProductAttribute Entity in the line $this->productTypes->add($productType);
EDIT:
I updated the controller code, I messed up the logic about unlinking ProductType from ProductAttribute. But it doesn't have any impact on the problem, still the same 500 error when I try to save new object.
EDIT2:
I can't get stack trace from Symfony because I get ordinary browser 500 error (probably because it's Fatal Error, I found it in apache logs). The line in controller which creates error is $form->handleRequest($request);.
This is not a Collection of Forms, but you are using collection specific method, this is not a good practice, however, you don't need this below code when you create a new object.
foreach ($productTypes as $value) {
if (false === $attribute->getProductTypes()->contains($value)) {
$attribute->getProductTypes()->removeElement($value);
}
}
So, I haven't found solution to the problem but I solved it somehow by fixing file structure of my project (moved bundle's Resources from general Resources folder to Bundle's Resources folder). I have no idea why this fixed the issue and what is even the connection between working but not proper folder structure and submitting forms but now it works, so I will mark the question as answered.
I am working on an action which uses layered Relations.
So i have a Player Entity, which has a Relation OwnedCard. Which has a Relation CardLevel which has a Relation card.
So i am using
/**
* #param Player $player
* #Route("/{id}/cards", name="loki.tuo.ownedcard.cards.show", requirements={"id":"\d+"})
*
* #ParamConverter("player", class="LokiTuoResultBundle:Player", options={"repository_method" = "findWithOwnedCards"})
* #return Response
* #Security("is_granted('view.player', player)")
*/
public function showCardsForPlayerAction(Player $player)
{
$allCards = $player->getOwnedCards();
$allCards = Collection::make($allCards)->sortBy(function (OwnedCard $elem) {
//$elem->getCard() calls the getName() method on CardLevel which delegates it to Card
return $elem->getCard()->getName();
});
$deck = $allCards->filter(function (OwnedCard $item) {
return $item->getAmountInDeck() > 0;
});
$combined = $deck->map(function (OwnedCard $item) {
return $item->toDeckString();
});
$formOptions = ['attr' => ['class' => 'data-remote']];
$ownedCardForm = $this->createForm(OwnedCardType::class, null, $formOptions);
$massOwnedCardForm = $this->createForm(MassOwnedCardType::class, null, [
'action' => $this->generateUrl('loki.tuo.ownedcard.card.add.mass', ['id' => $player->getId()]),
'method' => 'POST',
]);
//Render Template
For this I created a method which Joins and Selects these Relations
public function findWithOwnedCards($id)
{
$qb = $this->createQueryBuilder('player')
->join('player.ownedCards', 'ownedCards')
->join('ownedCards.card', 'cardLevel')
->join('cardLevel.card', 'card')
->addSelect(['ownedCards'])
->addSelect(['cardLevel'])
->addSelect(['card'])
->where('player.id = :id')
->setParameter('id', $id);
return $qb->getQuery()->getSingleResult();
}
But Unfortunately the Symfony Profiler tells me, there are a lot calls like
SELECT * FROM card_level WHERE card_id = ?
(I shortened the Query for better readability)
So this means, at some point Symfony/Doctrine doesnt use the Joined Relationships but somehow thinks they are lazy loaded, and needs to fetch them.
So now my Question: How can I find out, where or when the queries are executed? Is there some point in the Code where I could set a breakpoint or throw an Exception to see a stacktrace to see where this comes from?
Try setting 'fetch' property to "EAGER" for your associations
Here's an example from doctrine docs
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#manytoone
I am trying to populate two columns with random strings whenever a new user is created in laravel
maybe something like this in User model
public function putStringInDatabase () {
$this->public_key = str_random(40);
}
and whenever a new row is added in users table public_key and private_key columns get updated automatically by that random string
Take a look at Model Events. Specifically creating.
For example:
User::creating(function($user) {
$this->public_key = str_random(40);
$this->private_key = str_random(40);
});
http://php.net/manual/en/function.uniqid.php
Try making the string unique!
You could do it in the controller when you are creating the record
/**
* Create a new Object instance.
*
* #param array $data
*
* #return Object
*/
protected function create(array $data)
{
$object = new Object([
'fields' => $data['values']
'email' => $data['email'],
]);
$object->public_key = str_random(40);
$object->save();
return $object;
}
This will let you add a public_key outside of the mass assignment.
I have problem saving entity trough form with ManyToMany relations.
I can not save fields that are on "mappedBy" side of relation.
Code below is not saving anything to database and not trowing any errors:
// Entity/Pet
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Customer", mappedBy="pet", cascade={"persist"})
*/
private $customer;
/**
* Set customer
*
* #param \AppBundle\Entity\Customer $customer
* #return Pet
*/
public function setCustomer($customer)
{
$this->customer = $customer;
return $this;
}
// Entity/Customer
/**
* #var Pet
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pet", inversedBy="customer", cascade={"persist"})
* #ORM\JoinTable(name="customer_pet",
* joinColumns={
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="pet_id", referencedColumnName="id")
* }
* )
*/
private $pet;
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
));
It is working the other way around. So if I am saving something from CustomerType everything works.
EDIT:
Solution below worked for me but after couple days I found a problem with that solution. If form will be submitted with value that has been already saved in the database then Symfony will trow an error. To prevent that I had to check if given customer has been already assigned to the pet.
Checking of currently assigned customers had to be done on the beginning of function and not after form submission because for some reason after submission Pet() object contains submitted values not only those present in the db.
So on the beginning I've putted all already assigned customers in to the array
$em = $this->getDoctrine()->getManager();
$pet = $em->getRepository('AppBundle:Pet')->find($id);
$petOriginalOwners = array();
foreach ($pet->getCustomer() as $petCustomer)
{
$petOriginalOwners[] = $petCustomer->getId();
}
And after form submission I've checked if submitted ID's are in the array
if ($form->isValid())
{
foreach ($form['customer']->getData()->getValues() as $v)
{
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer && !in_array($v->getId(), $petOriginalOwners) )
{
$customer->addPet($pet);
}
}
$em->persist($pet);
$em->flush();
return $this->redirect($this->generateUrl('path'));
}
In Symfony2 the entity with the property with the inversedBy doctrine comment is the one that is supposed to EDIT THE EXTRA TABLE CREATED BY THE MANYTOMANY RELATION. That is why when you create a customer it inserts the corresponding rows in that extra table, saving the corresponding pets.
If you want the same behavior to happen the other way around, I recommend:
//PetController.php
public function createAction(Request $request) {
$entity = new Pet();
$form = $this->createCreateForm($entity);
$form->submit($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($form['customer']->getData()->getValues() as $v) {
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer) {
$customer->addPet($entity);
}
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('pet_show', array('id' => $entity->getId())));
}
return $this->render('AppBundle:pet:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
private function createCreateForm(Pet $entity) {
$form = $this->createForm(new PetType(), $entity, array(
'action' => $this->generateUrl('pet_create'),
'method' => 'POST',
));
return $form;
}
These two are but standard Symfony2 CRUD-generated actions in the controller corresponding to Pet entity.
The only tweak is the foreach structure inserted in the first action, that way you forcibly add the same pet to each customer you select in the form, thus getting the desired behavior.
Look, it is highly probable THIS is not the RIGHT WAY, or the PROPER WAY, but is A WAY and it works. Hope it helps.
In my case with a services <-> projects scenario, where services has "inversedBy" and projects has "mappedBy" I had to do this in my project controller's edit action so that when editing a project the services you checked would be persisted.
public function editAction(Request $request, Project $project = null)
{
// Check entity exists blurb, and get it from the repository, if you're inputting an entity ID instead of object ...
// << Many-to-many mappedBy hack
$servicesOriginal = new ArrayCollection();
foreach ($project->getServices() as $service) {
$servicesOriginal->add($service);
}
// >> Many-to-many mappedBy hack
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
// << Many-to-many mappedBy hack
foreach ($servicesOriginal as $service) {
if (!$project->getServices()->contains($service)) {
$service->removeProject($project);
$em->persist($service);
}
}
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
$em->persist($project);
$em->flush();
return; // I have a custom `redirectWithMessage()` here, use what you like ...
}
return $this->render("Your-template", [
$form => $form->createView(),
$project => $project,
]);
}
This works for both adding and removing entities in the many-to-many from the "mappedBy" side, so EntityType inputs should work as intended.
What's going on here is we're first building an "original" collection containing all of the service entities already linked to for this project. Then when the form is saving we're ensuring:
First that any unchecked services (those in the original collection but not the project object) have the project removed from their internal collection, then persisted.
Second that any newly checked services each add the project to their internal collection, then persisted.
Important: This depends on your entity's addService() and addProject() methods respectively check that each others' collections do not contain duplications. If you don't do this you'll end up with an SQL level error about a duplicate record insertion.
In the service entity I have:
/**
* Add project
*
* #param Project $project
*
* #return Service
*/
public function addProject(Project $project)
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
}
if (!$project->getServices()->contains($this)) {
$project->getServices()->add($this);
}
return $this;
}
In the project entity I have:
/**
* Add service
*
* #param Service $service
*
* #return Project
*/
public function addService(Service $service)
{
if (!$this->services->contains($service)) {
$this->services->add($service);
}
if (!$service->getProjects()->contains($this)) {
$service->getProjects()->add($this);
}
return $this;
}
You could alternatively check this in your controller instead, but makes sense if the model validates this itself when possible, as the model would break anyway if there were duplicates from any source.
Finally in your controller's create action you'll likely need this bit too just before $em->persist($project). (You won't need to work with an "original" collection as none will exist yet.)
// << Many-to-many mappedBy hack
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
I just had the same problem and I solved it differently.
Changing the code in the controller is not the better way to do it.
In my case I have a GenericController that handle all my CRUDs so I can't put in it specific code.
The best way to do it is by adding in your PetType a listener like this :
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
))
->addEventListener( FormEvents::SUBMIT, function( FormEvent $event ) {
/** #var Pet $pet */
$pet = $event->getData();
foreach ( $pet->getCustomers() as $customer ) {
$customer->addPet( $pet );
}
} );
That way you'll keep the mapping logic in the same place.