How to inject entity depencies into a controller in symfony 5.2? - php

I'm a beginner in symfony and I would like to inject my post entity into a method of my controller.
However, I've the following error fired :
Unable to guess how to get a Doctrine instance from the request information for parameter "post".
Here is my code :
/**
* #Route("/post/article/new", name="new.html.twig")
* #param Request $request
* #param Posts $posts
* #return Response
*/
public function newArticle(Request $request, Posts $posts): Response
{
$post = $posts;
$article = new Articles();
$post->setAuthor(1);
$form = $this->createForm(PostsType::class, $post);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$this->em->persist($post);
$this->em->flush();
$article->setPost($post->getId());
$themes = $form->get('themes')->getData();
$article->setThemes(implode(',', $themes));
$this->em->persist($post);
$this->em->flush();
return $this->redirectToRoute('home.html.twig');
}
return $this->render('post/article.html.twig', [
'formArticle' => $form->createView()
]);
}

You need a reference to the Post in the route, like a slug for the id or another unique field.
#Route("/post/{id}/article/new", ...
Otherwise Symfony has no idea which Post to load.

Related

Symfony RedirectToRoute with array paramater

I'm working on an upload system based on Symfony 4 and PHPSpreadsheet.
My user uploads the excel file. I then create the products/categories... At the end I want to get all categories created. The following Doctrine query :
/**
* #param $user_id
* #return array
* #throws \Doctrine\ORM\NonUniqueResultException
*/
public function checkIfNew($user_id): ?array {
return $this->createQueryBuilder('c')
->andWhere('c.categorie_parent_id is NULL')
->andWhere('c.created_by = :val')
->setParameter('val', $user_id)
->getQuery()
->getResult()
;
}
gets all my categories where created_by is by the user ID, and where Parent is null.
What I want to do is to get an array of all those categories and then redirect the user to a rendered Twig template page where he can make the link.
But I don't really understand how to use the parameters...
In my Upload I've set this :
$isNew = $this->getDoctrine()
->getRepository(Categorie::class)
->checkIfNew($this->getUser()->getId());
if (!is_null($isNew)){
$this->addFlash('success', 'Catalogue crée avec succès');
return $this->redirectToRoute('admin.categorie.link', $this->getUser()->getId());
}
I don't understand how I can use the request to redirect the user correctly using the route :
/**
* #Route("/admin/categories/import", name="admin.categorie.link", methods={"GET|POST"})
* #param Categorie $categories
*/
public function linkImport()
{
// What TODO here ?
return $this->render('admin/categories/link.html.twig', compact('categories'));
}
Well I suppose I have to add $isNew as a parameter for my request ? But did I reuse the array after to use this in my twig, and display the element from this array inside my twig.
There is a small error:
If your route is "/admin/categories/import" and you want to transfer a value like "$this->getUser()->getId()" then this should be like
Route "/admin/categories/import/{userId}" and your return would be:
return $this->redirectToRoute('admin.categorie.link', ['userId' => $this->getUser()->getId()]);
and your controller could take the userId as var:
/**
* #Route("/admin/categories/import/{userId}", name="admin.categorie.link", methods={"GET|POST"})
*/
public function linkImport(int $userId, EntityManagerInterface $em) // var name must be the same as in the route
{
// What TODO here ?
$categories = $em->getRepository(Categories::class)->findByUserId($userId);
return $this->render('admin/categories/link.html.twig', ['categories' => $categories]);
}
Now you can access your categories in twig with {{categories}}

Why the URL header takes the wrong parameter?

On a CRUD comment system that I put on posts, I have no problem modifying/deleting said comment by retrieving the post of the id and that of the comment. Here is the method used (which is also used to create a comment):
/**
* #Route("{id}/create", name="createComment")
* #Route("{id}/{comment}/modif", name="modifComment", defaults={"comment"=1}, methods="GET|POST")
*/
public function modification(FilmRepository $film, Comment $comment = null, Request $req, EntityManagerInterface $em, $id)
{
if(!$comment) {
$comment = new Comment();
}
$film = $em->getRepository(Film::class)->findOneBy(array('id' => $id));
$user = $this->getUser();
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($req);
dump($film);
dump($comment);
if($form->isSubmitted() && $form->isValid()) {
$comment->setAuthor($user);
$comment->setFilm($film);
$comment->setCreatedAt(new \DateTime());
$em->persist($comment);
$em->flush();
$this->addFlash('success', 'L\'action a bien été effectuée');
return $this->redirectToRoute('home');
}
return $this->render('comment/modif.html.twig', [
"comment" => $comment,
"form" => $form->createView()
]);
}
The problem comes when I try to create a new comment. When I am directed to the form, it considers that the post id corresponds to the comment id (for example, if I am on post 1 and want to add a comment it takes me to the comment form 1). However I specified in my twig request (contrary to modify) that I only took the film.id parameter:
{# Modif comment, with two parameters, functionnal#}
Modif
{# Add comment, with one parameter, unfunctionnal#}
Add
I used the same code as for the CRUD of my posts, and yet he when I want to create a new post returns me an empty form :
/**
* #Route("/admin/create", name="createFilm")
* #Route("/admin/{id}", name="modifFilm", methods="GET|POST")
*/
public function modification(Film $film = null, Request $req, EntityManagerInterface $em)
{
if(!$film) {
$film = new Film();
}
$form = $this->createForm(FilmType::class, $film);
$form->handleRequest($req);
if($form->isSubmitted() && $form->isValid()) {
$em->persist($film);
$em->flush();
$this->addFlash('success', 'L\'action a bien été effectuée');
return $this->redirectToRoute('admin');
}
return $this->render('admin/modif.html.twig', [
"film" => $film,
"form" => $form->createView(),
"admin" => true
]);
}
So the problem comes from the url which takes the id of the film and interprets it as the id of the comment, but I don't understand what is causing this?
In your public function, you have Comment $comment.
You are giving two argument to your Route: id and comment
The Paramconverter will try to find the correct Comment with what you gave him (an id and a comment). It will not check where the value comes from in your twig file.
Indeed, if your argument id comes from a film.id, the Paramconverter will give you the wrong comment.
What you should do is send the comment.id to the argument id.
You can change your Route this way :
#Route("{film}/{id}/modif
For your Twig :
Modif

Pass several parameters correctly to your controller

I have already inquired here and there but nothing more or less corresponds to my problem.
I have a page with information about a movie, which I access with an id parameter:
See comments
The film table having a relation with the table how, I display all the comments specific to the movie thanks to an ArrayCollection :
$filmRepo = $repo->find($id);
$comments = $filmRepo->getComments();
I created a CommentController in which I wrote this method whose goal would be to recover the id of the movie AND the id of the comment in order to be able to make CRUD operations on it:
/**
* #Route("{id}/{comment}/create", name="createComment")
* #Route("{id}/{comment}/modif", name="modifComment", defaults={"comment"=1}, methods="GET|POST")
*/
public function modification(Comment $comment = null, Film $film, Request $req, EntityManagerInterface $em)
{
if(!$comment) {
$comment = new Comment();
}
$user = $this->getUser();
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($req);
if($form->isSubmitted() && $form->isValid()) {
$comment->setAuthor($user);
$comment->setFilm($film);
$em->persist($comment);
$em->flush();
$this->addFlash('success', 'L\'action a bien été effectuée');
return $this->redirectToRoute('home');
}
return $this->render('comment/modif.html.twig', [
"comment" => $comment,
"form" => $form->createView()
]);
}
But no matter which comment I select, it takes the default comment, that is to say the one with id 1. So something is wrong with my request. However I pass the two parameters in the twig template:
Modif
The problem comes from a syntax error in the twig template. Instead of :
Modif
Rather do :
Modif

FOSRestBundle, generate URL to created resource

I'm in the process of creating a REST API with Symfony and the FOSRestBundle and am pretty much new to the both.
Now I wonder how to generate an URL to a resource I just created. Routes are setup like this:
# app/config/routing.yml
characters:
type: rest
prefix: /api
resource: "#Optc/Resources/config/routing/characters_routing.yml"
NelmioApiDocBundle:
prefix: /api/doc
resource: "#NelmioApiDocBundle/Resources/config/routing.yml"
# Resources/Optc/Resources/config/routing/characters_routing.yml
characters:
type: rest
resource: Optc\Controller\CharactersController
The part of the Characters controller that creates the resource:
$character = new Character();
$form = $this->createForm(new CharacterType(), $character);
$form->bind($data);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($character);
$em->flush();
$response->headers->set('Location', $this->generateUrl('get_characters', array('id' => $user->getId()), true));
$view = $this->view($character, 200);
return $this->handleView($view);
}
Update: Full controller code:
<?php
namespace Optc\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\FOSRestController;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Optc\Entity\Character;
use Optc\Form\CharacterType;
use Optc\HttpFoundation\File\Base64File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Characters Controller
*/
class CharactersController extends FOSRestController
{
/**
* Get the list of characters.
*
* #param string $page integer with the page number (requires param_fetcher_listener: force)
*
* #return array data
*
* #QueryParam(name="page", requirements="\d+", default="1", description="Page number of the overview.")
* #ApiDoc()
*/
public function getCharactersAction($page)
{
$characters = $this
->getDoctrine()
->getRepository('Optc:Character')
->findAll();
$view = $this->view($characters, 200);
return $this->handleView($view);
}
public function getCharacterAction($id)
{
$character = $this
->getDoctrine()
->getRepository('Optc:Character')
->findOneById($id);
if (!$character) {
throw new HttpException(404, sprintf('Character with id %d not found!', $id));
}
$view = $this->view($character, 200);
return $this->handleView($view);
}
/**
* Create a new character.
*
* #param Request $request
*
* #return View view instance
*
* #ApiDoc()
*/
public function postCharacterAction(Request $request)
{
$data = $request->request->all();
// If the request contains image date, first convert it from its base64 enconding to a real file
if ($request->request->has('image') && $request->request->get('id')) {
$imagePath = realpath($this->get('kernel')->getRootDir() . '/../web'.$this->container->getParameter('upload_path_characters')).'/'.$request->request->get('id');
$file = Base64File::create($imagePath, $request->request->get('image'));
$data['image'] = $file;
}
$character = new Character();
$form = $this->createForm(new CharacterType(), $character);
$form->bind($data);
var_dump($form->isValid());
var_dump($form->getErrorsAsString());
var_dump($this->generateUrl('get_characters', array('id' => $character->getId()), true));
die();
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($character);
$em->flush();
$response->headers->set('Location', $this->generateUrl('acme_demo_user_get', array('id' => $user->getId()), true));
$view = $this->view($character, 200);
return $this->handleView($view);
}
else {
}
}
}
The thing that isn't quite working like I expected is the generateUrl part to set the Location header. It spits out http://optc.local/api/characters?id=2. This will of course only list all resources instead. But what I want is http://optc.local/api/characters/2.
How would I do that? Seems like I'm missing something simple.
(By the way, the PHP part about returning the Location header is from http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/, so I expected this to be the "right" way.)
you should check app/console debug:router in terminal to see what name symfony has named the route
in my case it used a minus instead of an underscore
i.e get-character
You must use get_character route instead of get_characters route,
I suggest to you whether implement ClassResourceInterface or use RouteResource annotation, that can use method name as getAction, cgetAction(this is only a suggestion)

Symfony form not saving entity with ManyToMany relation

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.

Categories