I am working on a Symfony 6.1 project and have the following situation. I have an entity called "MaterialGroup". A material group can be in a parent-child relation with itself so one material group can have some "subGroups" and one subGroup can only belong to one "upperGroup". So basically it is ay One-to-Many Relationship.
Now, when I want to update the relations in the "new" or "edit" function of the controller in symfony it basically does not work. I do not get any errors and the if I use the function "dd()" in order to preview the parent materialGroup, I can see the subGroups that should be mapped. However after the flush nothing happens.
Here is my current code:
Entity
class MaterialGroup
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=MaterialGroup::class, inversedBy="subGroups")
*/
private $parentMaterialGroup;
/**
* #ORM\OneToMany(targetEntity=MaterialGroup::class, mappedBy="parentMaterialGroup")
*/
private $subGroups;
public function __construct()
{
$this->subGroups = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getParentMaterialGroup(): ?self
{
return $this->parentMaterialGroup;
}
public function setParentMaterialGroup(?self $parentMaterialGroup): self
{
$this->parentMaterialGroup = $parentMaterialGroup;
return $this;
}
/**
* #return Collection<int, self>
*/
public function getSubGroups(): Collection
{
return $this->subGroups;
}
public function addSubGroup(self $subGroup): self
{
if (!$this->subGroups->contains($subGroup)) {
$this->subGroups[] = $subGroup;
$subGroup->setParentMaterialGroup($this);
}
return $this;
}
public function removeSubGroup(self $subGroup): self
{
if ($this->subGroups->removeElement($subGroup)) {
// set the owning side to null (unless already changed)
if ($subGroup->getParentMaterialGroup() === $this) {
$subGroup->setParentMaterialGroup(null);
}
}
return $this;
}
}
Edit function in Controller
#[Route('/{id}/edit', name: 'edit', methods: ['GET', 'POST'])]
public function edit(Request $request, MaterialGroup $materialGroup, EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(MaterialGroupType::class, $materialGroup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $request->request->all();
$subGroups = [];
if(isset($data["material_group"]["subGroups"]))
$subGroups = $data["material_group"]["subGroups"];
//Get the material group objects for all the subGroups
$requestSubGroups = [];
foreach($subGroups as $value) {
$requestSubGroups[] = $this->materialGroupRepository->find($value);
}
//"Insert" the new subGroup relations
foreach($requestSubGroups as $subGroup) {
$materialGroup->addSubGroup($subGroup);
}
//This dd() leads to the object you can see after the code for the edit function in this post
dd($materialGroup);
$this->entityManager->flush();
return $this->redirectToRoute($this->routeName.'_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm($this->routeName.'/edit.html.twig', [
$this->routeName => $materialGroup,
'form' => $form,
]);
}
This object shows the subGroups that should be mapped to this materialGroup before the flush. And after the flush they are not mapped:
App\Entity\Tables\MaterialGroup {#1116 ▼
-id: 982
-parentMaterialGroup: null
-subGroups: Doctrine\ORM\PersistentCollection {#1324 ▼
#collection: Doctrine\Common\Collections\ArrayCollection {#1335 ▼
-elements: array:5 [▼
0 => App\Entity\Tables\MaterialGroup {#1707 ▶}
1 => App\Entity\Tables\MaterialGroup {#1729 ▶}
2 => App\Entity\Tables\MaterialGroup {#1750 ▶}
3 => App\Entity\Tables\MaterialGroup {#1771 ▶}
4 => App\Entity\Tables\MaterialGroup {#1885 ▶}
]
}
#initialized: true
-snapshot: array:4 [ …4]
-owner: App\Entity\Tables\MaterialGroup {#1116}
-association: array:15 [ …15]
-em: Doctrine\ORM\EntityManager {#452 …11}
-backRefFieldName: "parentMaterialGroup"
-typeClass: Doctrine\ORM\Mapping\ClassMetadata {#1009 …}
-isDirty: true
}
}
Can anyone tell me what I am misunderstanding or doing wrong here?
tried setParentMaterialGroup function instead of addSubGroup after getting the subGroup:
$subGroup->setParentMaterialGroup($materialGroup);
Can anyone tell me what I am misunderstanding or doing wrong here?
this is:
A material group can be in a parent-child relation with itself
If u want relations u should create different entities for set relations between them.
Related
Having a ManyToMany relation between Article and Tag i have a form with a multiselect of type EntityType using Symfony FormBuilder.
Everything seems fine, even preselection, except when deselecting an already related Tag from the tags-select, it does not remove the underlying relation. Therefor I can only add more relations.
I tried removing all relations before saving but because of LazyLoading I get other trouble there. I cannot find anything regarding this in the documentation but this might be very common basics, right?
Can anyone help me out here?
This is the code of my EntityType-Field
$builder->add('tags', FormEntityType::class, [
'label' => 'Tags',
'required' => false,
'multiple' => true,
'expanded' => false,
'class' => Tag::class,
'choice_label' => function (Tag $tag) {
return $tag->getName();
},
'query_builder' => function (TagRepository $er) {
return $er->createQueryBuilder('t')
->where('t.isActive = true')
->orderBy('t.sort', 'ASC')
->addOrderBy('t.name', 'ASC');
},
'choice_value' => function(?Tag $entity) {
return $tag ? $tag->getId() : '';
},
'choice_attr' => function ($choice) use ($options) {
// Pre-Selection
if ($options['data']->getTags() instanceof Collection
&& $choice instanceof Tag) {
if ($options['data']->getTags()->contains($choice)) {
return ['selected' => 'selected'];
}
}
return [];
}
]);
And this is how I save after form-submit
$articleForm = $this->createForm(ArticleEditFormType::class, $article)->handleRequest($this->getRequest());
if ($articleForm->isSubmitted() && $articleForm->isValid()) {
// saving the article
$article = $articleForm->getData();
$this->em->persist($article);
$this->em->flush();
}
The Relations look like this:
Article.php
/**
* #ORM\ManyToMany(targetEntity=Tag::class, mappedBy="articles", cascade={"remove"})
* #ORM\OrderBy({"sort" = "ASC"})
*/
private $tags;
public function removeTag(Tag $tag): self
{
if ($this->tags->removeElement($tag)) {
$tag->removeArticle($this);
}
return $this;
}
Tag.php
/**
* #ORM\ManyToMany(targetEntity=Article::class, inversedBy="tags", cascade={"remove"})
* #ORM\OrderBy({"isActive" = "DESC","name" = "ASC"})
*/
private $articles;
public function removeArticle(Article $article): self
{
$this->articles->removeElement($article);
return $this;
}
Setters, adders, etc. won't be called on the entity by default.
In the case of a multiselect where the underlying object is a collection, to ensure that the element is removed by calling removeArticle you have to set the option by_reference to false.
This is applicable to CollectionType as well.
How to properly add task with epic_id?
on this request
curl -XPOST -H "Content-Type: application/json" -d '{"title": "test1","description":"jhgdsh","epic":1}' http://localhost:8080/api/task/add
had an error
Notice: Object of class App\Entity\Epic could not be converted to int (500 Internal Server Error)
TaskController.php
<?php
class TaskController extends AbstractApiController
{
/**
* #Route("/create", name="create")
*/
public function createAction( Request $request ): Response
{
$form = $this->buildForm( TaskType::class );
$form->handleRequest($request);
if( !$form->isSubmitted() || !$form->isValid() )
{
return $this->respond( $form, Response::HTTP_BAD_REQUEST );
}
/** #var Task $task */
$task = $form->getData();
$this->getDoctrine()->getManager()->persist( $task );
$this->getDoctrine()->getManager()->flush();
return $this->respond( $task );
}
}
Form/TaskType.php
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
...
->add('epic', EntityType::class,
[
'class' => Epic::class,
'choice_label' => 'epic',
'constraints' => [
new NotNull( [
'message' => 'Epic cannot be blank.'
] ),
new GreaterThan( [
'value' => 0
] )
]
]
);
}
Entity/Task.php
/**
* #ORM\Table(name="task")
* #ORM\Entity(repositoryClass="App\Repository\TaskRepository")
*/
class Task
{
....
/**
* #var Epic
*
* #ORM\ManyToOne(targetEntity="Epic", cascade={"all"})
*/
private Epic $epic;
....
}
Problem was with filter in form type
`new GreaterThan( [
'value' => 0
] )`
Hi you need to add a toString() to your Epic class
public function __toString() {
return (string) $this->//title;
}
I have this configuration which allows me to create a pdf document in the CRUD, is there a way to add this code in the CRUD easyAdmin or link the CRUD of my EasyAdmin documentos to the CRUD of symfony.
I have problems creating the document in the EasyAdmin table
DocumentController.php
<?php
namespace App\Controller;
use App\Entity\Document;
use App\Form\DocumentType;
use App\Repository\DocumentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Service\FileUploader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
/**
* #IsGranted("ROLE_USER")
* #Route("/documents")
*/
class DocumentController extends AbstractController
{
/**
* #Route("/", name="document_index", methods={"GET"})
*/
public function index(DocumentRepository $documentRepository): Response
{
return $this->render('document/index.html.twig', [
'documents' => $documentRepository->findAll([], ['created_at' => 'desc']),
]);
}
/**
* #Route("/new", name="document_new", methods={"GET","POST"})
*/
public function new(Request $request, FileUploader $fileUploader): Response
{
$document = new Document();
$document->setCreatedAt(new \DateTime('now'));
$form = $this->createForm(DocumentType::class, $document);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$file = $form['fileDocument']->getData();
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$fileName = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$file->move(
$this->getParameter('brochures_directory'),
$fileName
);
$document->setFileDocument($fileName);
$entityManager->persist($document);
$entityManager->flush();
return $this->redirectToRoute('document_index', array('id' => $document->getId()));
}
return $this->render('document/new.html.twig', [
// 'document' => $document,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="document_show", methods={"GET"})
*/
public function show(Document $document): Response
{
return $this->render('document/show.html.twig', [
'document' => $document,
]);
}
/**
* #Route("/{id}/edit", name="document_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Document $document): Response
{
$form = $this->createForm(DocumentType::class, $document);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$file = $form['fileDocument']->getData();
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$fileName = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$file->move(
$this->getParameter('brochures_directory'),
$fileName
);
$document->setFileDocument($fileName);
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('document_index');
}
return $this->render('document/edit.html.twig', [
'document' => $document,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="document_delete", methods={"DELETE"})
*/
public function delete(Request $request, Document $document): Response
{
if ($this->isCsrfTokenValid('delete' . $document->getId(), $request->request->get('_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($document);
$entityManager->flush();
}
return $this->redirectToRoute('document_index');
}
}
DocumentCrudController Easy Admin
<?php
namespace App\Controller\Admin;
use App\Entity\Document;
use App\Entity\Publication;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
class DocumentCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Document::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setPageTitle(Crud::PAGE_INDEX, 'Liste de Documents')
;
}
public function configureFields(string $pageName): iterable
{
ImageField::new('fileDocument', 'Document PDF')->setFormType(FileType::class)
->setBasePath('docs');
return [
IdField::new('id')->onlyOnIndex(),
TextField::new('nomDocument', 'Titre'),
DateTimeField::new('created_at', 'Date de création'),
TextField::new('fileDocument', 'Document PDF')
->hideOnIndex()
->setFormType(FileType::class, [
'constraints' => [
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'application/pdf',
'application/x-pdf',
],
'mimeTypesMessage' => 'Veuillez télécharger un document PDF valide',
])
],
]),
];
}
public function configureActions(Actions $actions): Actions
{
return $actions
->add(Crud::PAGE_INDEX, Action::DETAIL);
}
}
I don't know how I can implement the same configuration in easy admin.
Look Here this is what happens when i create a document from table EasyAdmin.
Thank you.
That's a little weird but you need to use the ImageField (https://symfony.com/bundles/EasyAdminBundle/current/fields/ImageField.html)
And in you CrudController:
public function configureFields(string $pageName): iterable{
return [
ImageField::new('pdf', 'Your PDF')
->setFormType(FileUploadType::class)
->setBasePath('documents/') //see documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
->setUploadDir('public/documents/')
->setColumns(6)
->hideOnIndex()
->setFormTypeOptions(['attr' => [
'accept' => 'application/pdf'
]
]),
];
}
See documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
----- EDIT ----
In your index page of CRUD, you can create a link for your file like this:
public function configureFields(string $pageName): iterable{
return [
ImageField::new('pdf', 'Your PDF')
->setFormType(FileUploadType::class)
->setBasePath('documents/') //see documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
->setUploadDir('public/documents/')
->setColumns(6)
->hideOnIndex()
->setFormTypeOptions(['attr' => [
'accept' => 'application/pdf'
]
]),
TextField::new('pdf')->setTemplatePath('admin/fields/document_link.html.twig')->onlyOnIndex(),
];
}
Your templates/admin/fields/document_link.html.twig :
{% if field.value %}
Download file
{% else %}
--
{% endif %}
I apologize in advance if my question seems silly to you, I'm a beginner, I've searched but I can't find the answers.
We are in a factory. In this factory, each worker can have several posts, and each posts can contain several workers. So we are in a ManyToMany relationship. The problem is that when I add a worker to a post, he doesn't add to the worker already present in this post, he replaces him! As if a post could only contain one worker.
Can someone tell me what I'm doing wrong or send me precisely the documentation related to this type of problem?
Thanks.
Here is the related code.
(Poste = Post, Operateur = Worker)
in the Post Entity :
/**
* #ORM\ManyToMany(targetEntity=Operateur::class, inversedBy="postes")
* #ORM\JoinTable(name="poste_operateur")
*/
private $operateurs;
/**
* #return Collection|Operateur[]
*/
public function getOperateurs(): Collection
{
return $this->operateurs;
}
public function addOperateur(Operateur $operateur): self
{
if (!$this->operateurs->contains($operateur)) {
$this->operateurs[] = $operateur;
$operateur->addPoste($this);
}
return $this;
}
public function removeOperateur(Operateur $operateur): self
{
$this->operateurs->removeElement($operateur);
$operateur->removePoste($this);
return $this;
}
In the Operateur (worker) entity :
/**
* #ORM\ManyToMany(targetEntity=Poste::class, mappedBy="operateurs")
*/
private $postes;
/**
* #return Collection|Poste[]
*/
public function getPostes(): Collection
{
return $this->postes;
}
public function addPoste(Poste $poste): self
{
if (!$this->postes->contains($poste)) {
$this->postes[] = $poste;
$poste->addOperateur($this);
}
return $this;
}
public function removePoste(Poste $poste): self
{
if ($this->postes->removeElement($poste)) {
$poste->removeOperateur($this);
}
return $this;
}
In the PosteController, method to add an operateur to a post :
/**
* #Route("/{id}/new", name="poste_ope", methods={"GET", "POST"})
*/
public function addOpe(Request $request, Poste $poste): Response
{
$form = $this->createForm(PosteType2::class, $poste);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash(
'success',
"L'opérateur a bien été ajouté au poste {$poste->getNom()} !"
);
return $this->redirectToRoute('operateur_index');
}
return $this->render('poste/addope.html.twig', [
'poste' => $poste,
'form' => $form->createView(),
]);
}
The form in PostType2 :
class PosteType2 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('operateurs', EntityType::class, [
'class' => Operateur::class,
'label' => 'ajouter un opérateur à ce poste',
'choice_label' => 'nom',
'multiple' => true,
'expanded' => true,
])
->add('save', SubmitType::class, [
'label' => 'Enregistrer',
'attr' => [
'class' => 'btn btn-primary'
]
]);
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Poste::class,
]);
}
}
The problem was in the PosteController, here is the correction :
add an addPost
Here is the documentation who helped me : https://symfony.com/doc/current/doctrine/associations.html#saving-related-entities
I am using the Sonata Admin back-end and I would like to add a new image field to my user entity which is an avatar. Since I am already using the SonataMediaBundle I followed this tutorial: https://sonata-project.org/blog/2013/10/11/mediabundle-mediatype-improved
Here is my entity configuration:
/**
* #var \Application\Sonata\MediaBundle\Entity\Media
*
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media", cascade={"all"}, fetch="LAZY")
* #ORM\JoinColumn(name="avatar_id", referencedColumnName="id")
*/
protected $avatar;
Unfortunately I have many problems:
In my back-end the preview is not shown:
If I delete the media in the gallery I receive this error when editing the user: Entity of type 'Application\Sonata\MediaBundle\Entity\Media' for IDs id(6) was not found
The resulting API (generated with FOSRestBundle) is unusable by the client:
"avatar": {
"provider_metadata": {
"filename": "Test.png"
},
"name": "Test.png",
"description": null,
"enabled": false,
"provider_name": "sonata.media.provider.image",
"provider_status": 1,
"provider_reference": "325564b03489a6473e7c9def01dc58bab611eccb.png",
"width": 1430,
"height": 321,
"length": null,
"copyright": null,
"author_name": null,
"context": "default",
"cdn_is_flushable": null,
"cdn_flush_at": null,
"cdn_status": null,
"updated_at": "2017-08-08T12:31:19+02:00",
"created_at": "2017-08-08T12:31:19+02:00",
"content_type": "image/png",
"size": 24978,
"id": 7
}
I resolved all the 3 problems! I put here my solutions for all those who have the same difficulties.
In my back-end the preview is not shown:
As explained here I have to add a custom form widget to my config.yml file:
twig:
# Sonata form themes
form_themes:
- 'SonataMediaBundle:Form:media_widgets.html.twig'
And in my UserAdmin:
->with('Profile')
->add('avatar', 'sonata_media_type', array(
'provider' => 'sonata.media.provider.image',
'context' => 'default',
))
->end()
Now the preview will be shown :)
If I delete the media in the gallery I receive this error when editing the user: Entity of type
'Application\Sonata\MediaBundle\Entity\Media' for IDs id(6) was not
found
As explained here I need to add onDelete="SET NULL" on my entity:
/**
* #var \Application\Sonata\MediaBundle\Entity\Media
*
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media", cascade={"persist"}, fetch="LAZY")
* #ORM\JoinColumn(name="avatar_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $avatar;
The resulting API (generated with FOSRestBundle) is unusable by the client:
This one was very tricky but I was able to implement a custom JMS handler getting started from this post.
I peeked into the SonataMediaBundle source code and I found this snippet:
/**
* Returns media urls for each format.
*
* #ApiDoc(
* requirements={
* {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="media id"}
* },
* statusCodes={
* 200="Returned when successful",
* 404="Returned when media is not found"
* }
* )
*
* #param $id
*
* #return array
*/
public function getMediumFormatsAction($id)
{
$media = $this->getMedium($id);
$formats = array(MediaProviderInterface::FORMAT_REFERENCE);
$formats = array_merge($formats, array_keys($this->mediaPool->getFormatNamesByContext($media->getContext())));
$provider = $this->mediaPool->getProvider($media->getProviderName());
$properties = array();
foreach ($formats as $format) {
$properties[$format]['url'] = $provider->generatePublicUrl($media, $format);
$properties[$format]['properties'] = $provider->getHelperProperties($media, $format);
}
return $properties;
}
So I included it into my source and the complete handler is the following:
<?php
namespace AppBundle\Serializer;
use Application\Sonata\MediaBundle\Entity\Media;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Sonata\MediaBundle\Provider\MediaProviderInterface;
class MediaHandler implements SubscribingHandlerInterface
{
private $mediaPool;
public function __construct($mediaPool)
{
$this->mediaPool = $mediaPool;
}
public static function getSubscribingMethods()
{
return array(
array(
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Application\Sonata\MediaBundle\Entity\Media',
'method' => 'serializeToJson',
),
);
}
public function serializeToJson(JsonSerializationVisitor $visitor, Media $media, array $type, Context $context)
{
$formats = array(MediaProviderInterface::FORMAT_REFERENCE);
$formats = array_merge($formats, array_keys($this->mediaPool->getFormatNamesByContext($media->getContext())));
$provider = $this->mediaPool->getProvider($media->getProviderName());
$properties = array();
foreach ($formats as $format) {
$properties[$format]['url'] = $provider->generatePublicUrl($media, $format);
$properties[$format]['properties'] = $provider->getHelperProperties($media, $format);
}
return $properties;
}
}
Service settings:
app.serializer.media:
class: AppBundle\Serializer\MediaHandler
arguments:
- '#sonata.media.pool'
tags:
- { name: jms_serializer.subscribing_handler }
And that's all!