I have two Entities
- Kitchen
- KitchenSubImage
Each kitchen has a main image but also has many sub images (KitchenSubImage).
I have implemented both the entities and their form types. At this moment I have the form displayed and have implemented everything from How to Handle File Uploads with Symfony2 to handle the file upload.
The issue I have is that I have no idea how to handle both file uploads at once. It's made more complicated by the fact that the kitchen can have many sub images.
I also have the following error at the top of the form when I submit the form:
This value should be of type PWD\WebsiteBundle\Entity\KitchenSubImage.
Controller
<?php
namespace PWD\AdminBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use PWD\WebsiteBundle\Entity\Kitchen;
use PWD\AdminBundle\Form\Type\KitchenType;
use PWD\WebsiteBundle\Entity\KitchenSubImage;
use PWD\AdminBundle\Form\Type\KitchenSubImageType;
class KitchenController extends Controller
{
public function indexAction()
{
return 'index';
}
public function addAction(Request $request)
{
$kitchen = new Kitchen();
$image = new KitchenSubImage();
$kitchen->addSubImage($image);
$form = $this->createForm(new KitchenType(), $kitchen);
$form->handleRequest($request);
if ($form->isValid()) {
$kitchen->upload();
return $this->render('PWDWebsiteBundle:Pages:home.html.twig');
}
return $this->render('PWDAdminBundle:Pages:form-test.html.twig', array(
'form' => $form->createView(),
));
}
}
Kitchen Entity
<?php
namespace PWD\WebsiteBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="kitchen")
*/
class Kitchen
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
* #Assert\NotBlank()
*/
protected $name;
/**
* #ORM\Column(type="text")
* #Assert\NotBlank()
*/
protected $description;
/**
* #Assert\File(maxSize="6000000")
* #Assert\Image(
* minWidth = 800,
* maxWidth = 800,
* minHeight = 467,
* maxHeight = 467
* )
*/
protected $mainImage;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
protected $mainImagePath;
/**
* #Assert\Type(type="PWD\WebsiteBundle\Entity\KitchenSubImage")
* #ORM\OneToMany(targetEntity="KitchenSubImage", mappedBy="kitchen")
*/
protected $subImage;
public function __construct()
{
$this->subImage = new ArrayCollection();
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getMainImage()
{
return $this->mainImage;
}
public function setMainImage(UploadedFile $mainImage = null)
{
$this->mainImage = $mainImage;
}
public function getSubImage()
{
return $this->subImage;
}
public function setSubImage(KitchenSubImage $subImage = null)
{
$this->subImage = $subImage;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set mainImagePath
*
* #param string $mainImagePath
* #return Kitchen
*/
public function setMainImagePath($mainImagePath)
{
$this->mainImagePath = $mainImagePath;
return $this;
}
/**
* Get mainImagePath
*
* #return string
*/
public function getMainImagePath()
{
return $this->mainImagePath;
}
public function getAbsolutePath()
{
return null === $this->mainImagePath
? null
: $this->getUploadRootDir().'/'.$this->mainImagePath;
}
public function getWebPath()
{
return null === $this->mainImagePath
? null
: $this->getUploadDir().'/'.$this->mainImagePath;
}
public function getUploadRootDir()
{
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function getUploadDir()
{
return 'uploads/our-work';
}
public function upload()
{
if (null === $this->getMainImage()) {
return;
}
$this->getMainImage()->move(
$this->getUploadRootDir(),
$this->getMainImage()->getClientOriginalName()
);
$this->mainImagePath = $this->getMainImage()->getClientOriginalName();
$this->mainImage = null;
}
/**
* Add subImage
*
* #param \PWD\WebsiteBundle\Entity\KitchenSubImage $subImage
* #return Kitchen
*/
public function addSubImage(\PWD\WebsiteBundle\Entity\KitchenSubImage $subImage)
{
$this->subImage[] = $subImage;
$subImage->setKitchen($this); # used for persisting
return $this;
}
/**
* Remove subImage
*
* #param \PWD\WebsiteBundle\Entity\KitchenSubImage $subImage
*/
public function removeSubImage(\PWD\WebsiteBundle\Entity\KitchenSubImage $subImage)
{
$this->subImage->removeElement($subImage);
}
}
KitchenSubImage Entity
<?php
namespace PWD\WebsiteBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="kitchenImages")
*/
class KitchenSubImage
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Assert\Image(
* minWidth = 800,
* maxWidth = 800,
* minHeight = 467,
* maxHeight = 467
* )
*/
public $image;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $imagePath;
/**
* #ORM\ManyToOne(targetEntity="Kitchen", inversedBy="subImage")
* #ORM\JoinColumn(name="kitchen_id", referencedColumnName="id")
**/
protected $kitchen;
public function getImage()
{
return $this->image;
}
public function setImage(UploadedFile $image = null)
{
$this->image = $image;
}
public function getAbsolutePath()
{
return null === $this->imagePath
? null
: $this->getUploadRootDir().'/'.$this->imagePath;
}
public function getWebPath()
{
return null === $this->imagePath
? null
: $this->getUploadDir().'/'.$this->imagePath;
}
public function getUploadRootDir()
{
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function getUploadDir()
{
return 'uploads/our-work';
}
public function upload()
{
if (null === $this->getImage()) {
return;
}
$this->getImage()->move(
$this->getUploadRootDir(),
$this->getImage()->getClientOriginalName()
);
$this->mainImagePath = $this->getImage()->getClientOriginalName();
$this->mainImage = null;
}
/**
* Set imagePath
*
* #param string $imagePath
* #return KitchenSubImage
*/
public function setImagePath($imagePath)
{
$this->imagePath = $imagePath;
return $this;
}
/**
* Get imagePath
*
* #return string
*/
public function getImagePath()
{
return $this->imagePath;
}
/**
* Set kitchen
*
* #param \PWD\WebsiteBundle\Entity\Kitchen $kitchen
* #return KitchenSubImage
*/
public function setKitchen(\PWD\WebsiteBundle\Entity\Kitchen $kitchen = null)
{
$this->kitchen = $kitchen;
return $this;
}
/**
* Get kitchen
*
* #return \PWD\WebsiteBundle\Entity\Kitchen
*/
public function getKitchen()
{
return $this->kitchen;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
}
KitchenType:
<?php
namespace PWD\AdminBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class KitchenType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('description', 'textarea');
$builder->add('mainImage', 'file');
$builder->add('subImage', 'collection', array(
'type' => new KitchenSubImageType(),
'label' => false,
'allow_add' => true,
'by_reference' => false,
));
$builder->add('submit', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'PWD\WebsiteBundle\Entity\Kitchen',
'cascade_validation' => true,
));
}
public function getName()
{
return 'kitchen';
}
}
KitchenSubImageType:
<?php
namespace PWD\AdminBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class KitchenSubImageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', 'file', array('label' => 'Sub Images'));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'PWD\WebsiteBundle\Entity\KitchenSubImage',
));
}
public function getName()
{
return 'kitchensubimage';
}
}
Welcome back. Kind of wish that you had taken my previous suggestion and gone though the blog/tags example. You are still having big picture issues with collections.
In your kitchen entity, this is all wrong:
protected $subImage;
public function getSubImage()
{
return $this->subImage;
}
public function setSubImage(KitchenSubImage $subImage = null)
{
$this->subImage = $subImage;
}
It should be:
protected $subImages;
public function getSubImages()
{
return $this->subImages;
}
public function addSubImage(KitchenSubImage $subImage)
{
$this->subImages[] = $subImage;
$subImage->setKitchen($this);
}
See how a collection aka relation works in Doctrine?. Just like the bolg/tags example shows. As the form component processes the subImages collection, it will call addSubImage for each posted KitchenSubImage.
The above change may or may not fix everything. Kind of doubt it. If not:
Please tell me that you got the Kitchen form working before you added the sub image collection? You are able to load/store/retrieve the main image? If not, comment out $builder->add('subImage', 'collection', and focus on the kitchen entity.
Once kitchen is working, add subImages back into the form but comment out the allow_add and report what happens.
===================================================
With respect to how the sub images are processed, I can understand some of the confusion. I have not implemented a collection of images my self. Might be some gotchas.
I do know that your need to call upload on each sub image. upload is actually a somewhat misleading name. The file is already on your serve sitting in a tmp directory somewhere. Upload just moves it to a permanent location and stores the path in your entity.
Start by trying this:
if ($form->isValid()) {
$kitchen->upload();
foreach($kitchen->getSubImages() as $subImage)
{
$subImage->upload();
}
// really should redirect here but okay for now
return $this->render('PWDWebsiteBundle:Pages:home.html.twig');
}
It might be better to loop on subImages in kitchen::upload but try it in the controller for now.
Related
Following this doc https://symfony.com/doc/current/form/data_transformers.html#harder-example-transforming-an-issue-number-into-an-issue-entity, we learn how to fill a field (which is an Entity) without using a choice field type or similar.
It seems to work, using the primary key of the Entity concerned (el famoso "id").
However, i want to use another field and store it in a table, but i have an error (see screeshot at bottom)
Entities :
<?php
namespace App\Entity;
use App\Repository\LandRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=LandRepository::class)
* #ORM\Table(name="land",indexes={#ORM\Index(columns={"uid"})})
* #ORM\HasLifecycleCallbacks()
*/
class Land
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $libelle;
/**
* #ORM\Column(type="integer")
*/
private $uid;
public function getId(): ?int
{
return $this->id;
}
public function getLibelle(): ?string
{
return $this->libelle;
}
public function setLibelle(string $libelle): self
{
$this->libelle = $libelle;
return $this;
}
public function getUid(): ?int
{
return $this->uid;
}
public function setUid(int $uid): self
{
$this->uid = $uid;
return $this;
}
}
namespace App\Entity;
use App\Repository\RideRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RideRepository::class)
* #ORM\HasLifecycleCallbacks()
*/
class Ride
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $libelle;
/**
* #ORM\ManyToOne(targetEntity=Land::class)
* #ORM\JoinColumn(name="areaparent", referencedColumnName="uid")
*/
private $areaparent;
public function getId(): ?int
{
return $this->id;
}
public function getLibelle(): ?string
{
return $this->libelle;
}
public function setLibelle(string $libelle): self
{
$this->libelle = $libelle;
return $this;
}
public function getAreaparent(): ?Land
{
return $this->areaparent;
}
public function setAreaparent(?Land $areaparent): self
{
$this->areaparent = $areaparent;
return $this;
}
}
Expected final result :
"Land" table
"Ride" table
So you can see that in the "ride" table that the "areaparent" column is relative to the "land.uid" column.
"Land" form is classic
"Ride" form have a minor change : The "areaparent" is not a choice field, it's a text type which will call my DataTransformer
Extract of my form "RideType"
<?php
namespace App\Form;
use App\Entity\Ride;
use App\Entity\Land;
use Symfony\Component\Form\AbstractType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Repository\LandRepository;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use App\Form\DataTransformer\UIDTransformer;
class RideType extends AbstractType
{
private $transformer;
public function __construct(UIDTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('libelle')
->add('areaparent', TextType::class, [
'invalid_message' => 'That is not a valid areaparent number',
])
;
$builder->get('areaparent')->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ride::class,
]);
}
}
Extract of my DataTransformer :
<?php
namespace App\Form\DataTransformer;
use App\Entity\Land;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class UIDTransformer implements DataTransformerInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Transforms an object (Land) to a string (areaparent).
*
* #param Land|null $Land
* #return string
*/
public function transform($land)
{
if (null === $land) {
return '';
}
return $land->getUid();
}
/**
* Transforms a string (police) to an object (Land).
*
* #param string $areaparent
* #return Land|null
* #throws TransformationFailedException if object (Land) is not found.
*/
public function reverseTransform($areaparent)
{
// no areaparent? It's optional, so that's ok
if (!$areaparent) {
return;
}
$land = $this->entityManager->getRepository(Land::class)->findByUid($areaparent);
if (count($land) == 0) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf('Land with areaparent "%s" does not exist!',$areaparent));
}
return $land[0];
}
}
Error
I try to debug the symfony/doctrine code, and if i correctly understand, the error appears because the field "uid" is not considered as an "identifier".
See some dump.
Have you got some ideas ?
Thanks !
This case is a case study, I'm trying to resolve this issue in order to explain how to organize entities and create forms to my students.
I have this singular relation between 3 of my entities :
Protagonist <--(OneToMany)--> EventRegistration <--(ManyToOne)--> Event
Which could not be transformed as a many to many relation because there are some columns inside the EventRegistration table :
Protagonist :
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProtagonistRepository")
*/
class Protagonist
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="string", length=100, nullable=true)
*/
private $japaneseName;
/**
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $picture;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $background;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $updated_at;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="protagonists")
* #ORM\JoinColumn(nullable=false)
*/
private $category;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Tag", mappedBy="protagonists")
*/
private $tags;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Registration", mappedBy="protagonist")
*/
private $registrations;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
private $isAlive;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Event", mappedBy="protagonists")
*/
private $events;
/**
* #ORM\OneToMany(targetEntity="App\Entity\EventRegistration", mappedBy="protagonist")
*/
private $eventRegistrations;
public function __construct()
{
$this->tags = new ArrayCollection();
$this->registrations = new ArrayCollection();
$this->events = new ArrayCollection();
$this->eventRegistrations = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getJapaneseName(): ?string
{
return $this->japaneseName;
}
public function setJapaneseName(?string $japaneseName): self
{
$this->japaneseName = $japaneseName;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getPicture(): ?string
{
return $this->picture;
}
public function setPicture(?string $picture): self
{
$this->picture = $picture;
return $this;
}
public function getBackground(): ?string
{
return $this->background;
}
public function setBackground(?string $background): self
{
$this->background = $background;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updated_at;
}
public function setUpdatedAt(?\DateTimeInterface $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
/**
* #return Collection|Tag[]
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
$tag->addProtagonist($this);
}
return $this;
}
public function removeTag(Tag $tag): self
{
if ($this->tags->contains($tag)) {
$this->tags->removeElement($tag);
$tag->removeProtagonist($this);
}
return $this;
}
/**
* #return Collection|Registration[]
*/
public function getRegistrations(): Collection
{
return $this->registrations;
}
public function addRegistration(Registration $registration): self
{
if (!$this->registrations->contains($registration)) {
$this->registrations[] = $registration;
$registration->setProtagonist($this);
}
return $this;
}
public function removeRegistration(Registration $registration): self
{
if ($this->registrations->contains($registration)) {
$this->registrations->removeElement($registration);
// set the owning side to null (unless already changed)
if ($registration->getProtagonist() === $this) {
$registration->setProtagonist(null);
}
}
return $this;
}
public function getIsAlive(): ?bool
{
return $this->isAlive;
}
public function setIsAlive(?bool $isAlive): self
{
$this->isAlive = $isAlive;
return $this;
}
/**
* #return Collection|Event[]
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addProtagonist($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->contains($event)) {
$this->events->removeElement($event);
$event->removeProtagonist($this);
}
return $this;
}
/**
* #return Collection|EventRegistration[]
*/
public function getEventRegistrations(): Collection
{
return $this->eventRegistrations;
}
public function addEventRegistration(EventRegistration $eventRegistration): self
{
if (!$this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations[] = $eventRegistration;
$eventRegistration->setProtagonist($this);
}
return $this;
}
public function removeEventRegistration(EventRegistration $eventRegistration): self
{
if ($this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations->removeElement($eventRegistration);
// set the owning side to null (unless already changed)
if ($eventRegistration->getProtagonist() === $this) {
$eventRegistration->setProtagonist(null);
}
}
return $this;
}
}
EventRegistration :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\EventRegistrationRepository")
*/
class EventRegistration
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetimetz")
*/
private $registrationDate;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Protagonist", inversedBy="eventRegistrations")
*/
private $protagonist;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Event", inversedBy="eventRegistrations")
*/
private $event;
public function getId(): ?int
{
return $this->id;
}
public function getRegistrationDate(): ?\DateTimeInterface
{
return $this->registrationDate;
}
public function setRegistrationDate(\DateTimeInterface $registrationDate): self
{
$this->registrationDate = $registrationDate;
return $this;
}
public function getProtagonist(): ?Protagonist
{
return $this->protagonist;
}
public function setProtagonist(?Protagonist $protagonist): self
{
$this->protagonist = $protagonist;
return $this;
}
public function getEvent(): ?Event
{
return $this->event;
}
public function setEvent(?Event $event): self
{
$this->event = $event;
return $this;
}
}
Event :
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\EventRepository")
*/
class Event
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="datetimetz", nullable=true)
*/
private $start_date;
/**
* #ORM\Column(type="datetimetz", nullable=true)
*/
private $end_date;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Protagonist", inversedBy="events")
*/
private $protagonists;
/**
* #ORM\OneToMany(targetEntity="App\Entity\EventRegistration", mappedBy="event")
*/
private $eventRegistrations;
public function __construct()
{
$this->protagonists = new ArrayCollection();
$this->eventRegistrations = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getStartDate(): ?\DateTimeInterface
{
return $this->start_date;
}
public function setStartDate(?\DateTimeInterface $start_date): self
{
$this->start_date = $start_date;
return $this;
}
public function getEndDate(): ?\DateTimeInterface
{
return $this->end_date;
}
public function setEndDate(?\DateTimeInterface $end_date): self
{
$this->end_date = $end_date;
return $this;
}
/**
* #return Collection|Protagonist[]
*/
public function getProtagonists(): Collection
{
return $this->protagonists;
}
public function addProtagonist(Protagonist $protagonist): self
{
if (!$this->protagonists->contains($protagonist)) {
$this->protagonists[] = $protagonist;
}
return $this;
}
public function removeProtagonist(Protagonist $protagonist): self
{
if ($this->protagonists->contains($protagonist)) {
$this->protagonists->removeElement($protagonist);
}
return $this;
}
/**
* #return Collection|EventRegistration[]
*/
public function getEventRegistrations(): Collection
{
return $this->eventRegistrations;
}
public function addEventRegistration(EventRegistration $eventRegistration): self
{
if (!$this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations[] = $eventRegistration;
$eventRegistration->setEvent($this);
}
return $this;
}
public function removeEventRegistration(EventRegistration $eventRegistration): self
{
if ($this->eventRegistrations->contains($eventRegistration)) {
$this->eventRegistrations->removeElement($eventRegistration);
// set the owning side to null (unless already changed)
if ($eventRegistration->getEvent() === $this) {
$eventRegistration->setEvent(null);
}
}
return $this;
}
}
I can access a collection of eventRegistrations with my Protagonist and Event entity, and I can access the protagonist and event with my EventRegistration entity.
The issue crops up when I try to create a checkbox with all the events available for the protagonist : I don't have any attribute that allows me to make a collection of those events :
ProtagonistType
<?php
namespace App\Form;
use App\Entity\Category;
use App\Entity\Event;
use App\Entity\EventRegistration;
use App\Entity\Protagonist;
use App\Entity\Tag;
use App\Repository\EventRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class ProtagonistType
* #package App\Form
*/
class ProtagonistType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('japaneseName', TextType::class, [
'required' => false
])
->add('description', TextareaType::class)
->add('picture', TextType::class, [
'required' => false
])
->add('background', TextType::class, [
'required' => false
])
->add('isAlive', CheckboxType::class)
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'title',
'expanded' => true,
'multiple' => false
])
->add('tags', EntityType::class, [
'class' => Tag::class,
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
'by_reference' => false,
])
**->add('eventRegistrations', CollectionType::class, [
'entry_type' => EventRegistrationType::class
])**
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Protagonist::class,
]);
}
}
EventRegistrationType :
<?php
namespace App\Form;
use App\Entity\Event;
use App\Entity\EventRegistration;
use App\Repository\EventRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EventRegistrationType
* #package App\Form
*/
class EventRegistrationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('event', EntityType::class, [
'class' => Event::class,
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
'by_reference' => false,
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => EventRegistration::class,
]);
}
}
The only effective solution I found is to create a ManyToMany relation between Protagonist and Event, then set another Registration table which is on a ManyToOne relation with Protagonist in order to get the protagonists registrations.
Still I'd like to make this many to many relation with extra fields works, I'm all ears for any solution you'd get to solve this issue.
Thank you!
Some theory
Entities are your models, business objects that have identities, data, behavior.
They are the heart, building blocks of your business model.
When we design entities - in the first place we should consider them as objects, that have their own shape and responsibilities instead of them being just containers for data stored in a database. Also, we should care about proper relations between entities.
Ideally, entities should always be valid. If so - they can be persisted at any time.
Persistence is a separate concern.
In general case, it's even not necessary for entities to be persisted into a database. They could just be persisted in memory, file system, key-value storage etc.
Forms is also a separate concern that is closer to working with user interfaces.
Forms help us to render user interface, translate requests from users to some structures of known shape that is easier to work with, than with raw data from requests, and validate this submitted data.
These structures are just containers for data retrieved from requests, they should not have any behavior.
These structures can be invalid at certain times.
What about the issue described?
So making entities playing roles of these forms underlying data structures might be not the best idea ever.
It's just clearly mixing of concerns and rigid coupling between different layers.
That is why you're having these issues.
So instead of using EventRegistration class as a data_class for EventRegistrationType and Protagonist - for ProtagonistType - consider creating separate data structures. Propagate submitted data to entities only when it's successfully validated.
Some useful links to read more on the topic (though the author calls underlying structures - commands somewhy):
Decoupling (Symfony2) Forms from Entities
Form, Command, and Model Validation
Other useful topics:
The Clean Architecture
Domain-Driven Design in PHP
I was following these guides: File upload tutorial and Collection form type guide. Everything was ok but "edit" action. Collection of images is shown correctly in "edit" action but when I try to submit the form (without adding any new images) i get an error.
Call to a member function guessExtension() on null
Which means that images in a collection got null values, instead of UploadedFile.
I've searched a lot and read many of sof questions about handling collection of images with doctrine but still no clue.
Questions like:
Symfony2, Edit Form, Upload Picture
howto handle edit forms with FileType inputs in symfony2
Multiple (oneToMany) Entities form generation with symfony2 and file upload
So the question is how can u handle edit images? Removal is needed too.
Controller edit action
public function editAction(Request $request, Stamp $stamp)
{
if (!$stamp) {
throw $this->createNotFoundException('No stamp found');
}
$form = $this->createForm(AdminStampForm::class, $stamp);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var Stamp $stamp */
$stamp = $form->getData();
foreach ($stamp->getImages() as $image) {
$fileName = md5(uniqid()) . '.' . $image->getName()->guessExtension();
/** #var $image StampImage */
$image->getName()->move(
$this->getParameter('stamps_images_directory'),
$fileName
);
$image->setName($fileName)->setFileName($fileName);
}
$em = $this->getDoctrine()->getManager();
$em->persist($stamp);
$em->flush();
$this->addFlash('success', 'Successfully edited a stamp!');
return $this->redirectToRoute('admin_stamps_list');
}
return [
'form' => $form->createView(),
];
}
Entity
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\JoinTable;
use Doctrine\ORM\Mapping\ManyToMany;
/**
* Class Stamp
* #package AppBundle\Entity
*
*
* #ORM\Entity(repositoryClass="AppBundle\Repository\StampRepository")
* #ORM\Table(name="stamp")
* #ORM\HasLifecycleCallbacks()
*/
class Stamp
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
// * #ORM\OneToMany(targetEntity="AppBundle\Entity\StampImage", mappedBy="id", cascade={"persist", "remove"})
/**
* #ManyToMany(targetEntity="AppBundle\Entity\StampImage", cascade={"persist"})
* #JoinTable(name="stamps_images",
* joinColumns={#JoinColumn(name="stamp_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="image_id", referencedColumnName="id", unique=true)}
* ) */
private $images;
/**
* #ORM\Column(type="datetime")
*/
private $createdAt;
/**
* #ORM\Column(type="datetime")
*/
private $updatedAt;
public function __construct()
{
$this->images = new ArrayCollection();
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
public function addImage(StampImage $image)
{
$this->images->add($image);
}
public function removeImage(StampImage $image)
{
$this->images->removeElement($image);
}
/**
* #return mixed
*/
public function getImages()
{
return $this->images;
}
/**
* #param mixed $images
*
* #return $this
*/
public function setImages($images)
{
$this->images = $images;
return $this;
}
/**
* #return mixed
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* #param mixed $createdAt
*
* #return Stamp
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* #return mixed
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* #param mixed $updatedAt
*
* #return Stamp
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateTimestamps()
{
$this->setUpdatedAt(new \DateTime('now'));
if (null == $this->getCreatedAt()) {
$this->setCreatedAt(new \DateTime());
}
}
}
StampImage entity
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity as UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class StampImage
* #package AppBundle\Entity
*
* #ORM\Entity()
* #ORM\Table(name="stamp_image")
* #UniqueEntity(fields={"name"}, message="This file name is already used.")
*/
class StampImage
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
private $name;
/**
* #ORM\Column(type="string")
*/
private $fileName;
/**
* #return mixed
*/
public function getFileName()
{
return $this->fileName;
}
/**
* #param mixed $fileName
*
* #return StampImage
*/
public function setFileName($fileName)
{
$this->fileName = $fileName;
return $this;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*
* #return StampImage
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
}
Main entity form
<?php
namespace AppBundle\Form;
use AppBundle\Entity\Stamp;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AdminStampForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('images', CollectionType::class, [
'entry_type' => AdminStampImageForm::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Stamp::class,
]);
}
public function getName()
{
return 'app_bundle_admin_stamp_form';
}
}
Image form type
<?php
namespace AppBundle\Form;
use AppBundle\Entity\StampImage;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AdminStampImageForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', FileType::class, [
'image_name' => 'fileName',
'label' => false,
'attr' => [
],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => StampImage::class,
'required' => false
]);
}
public function getBlockPrefix()
{
return 'app_bundle_admin_stamp_image_form';
}
}
FileType extension
- extends 'form_div_layout.html.twig'
- block file_widget
- spaceless
- if image_url is not null
%img{:src => "#{image_url}"}
%div{:style => "display: none;"}
#{block('form_widget')}
- else
#{block('form_widget')}
1) Multiupload (but not OneToMany).
2) For editing an uploaded image:
# AppBundle/Entity/Stamp
/**
* #ORM\Column(type="string", length=255)
*/
private $image;
# AppBundle/Form/StampForm
->add('imageFile', FileType::class, [ //the $image property of Stamp entity class will store the path to the file, and this imageFile field will get the uploaded file
'data_class' => null, //important!
])
#AppBundle/Controller/StampController
/**
* #Route("/{id}/edit", name="stamp_edit")
* #Method({"GET", "POST"})
*/
public function editAction(Request $request, Stamp $stamp) {
$editForm = $this->createForm(StampType::class, $stamp, [
'action'=>$this->generateUrl('stamp_edit',['id'=>$stamp->getId()]),
'method'=>'POST'
]);
$editForm->handleRequest($request);
if($editForm->isSubmitted() && $form->isValid()) {
$imageFile = $editForm->get('imageFile')->getData();
if (null != $imageFile) { //this means that for the current record that needs to be edited, the user has chosen a different image
//1. remove the old image
$oldImg = $this->getDoctrine()->getRepository('AppBundle:Stamp')->find($stamp);
$this->get('app.file_remove')->removeFile($oldImg->getImage());
//2. upload the new image
$img = $this->get('app.file_upload')->upload($imageFile);
//3. update the db, replacing the path to the old file with the path to the new uploaded file
$stamp->setImage($img);
$this->getDoctrine()->getManager()->flush();
//4. add a success flash, and anything else you need, and redirect to a route
} else { //if the user has chosen to edit a different field (but not the image one)
$this->getDoctrine()->getManager()->flush();
//add flash message, and redirect to a route
}
}
return $this->render(...);
}
#AppBundle/Services/FileRemove
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
class FileRemove {
private $targetDir;
public function __construct($targetDir) {
$this->targetDir = $targetDir;
}
public function removeFile($path) {
$fs = new Filesystem();
$file = $this->targetDir . '/' . $path;
try{
if($fs->exists($file)){
$fs->remove($file);
return true;
}
return false;
} catch(IOExceptionInterface $e){
//log error for $e->getPath();
}
}
}
#app/config/services.yml
app.file_remove:
class: AppBundle/Services/FileRemove
arguments: ['%stamp_dir%']
#app/config/config.yml
parameters:
stamp_dir: '%kernel.root_dir%/../web/uploads/stamps' //assuming this is how you've set up the upload directory
#AppBundle/Services/FileUpload
use Symfony\Component\HttpFoundation\File\UploadedFile;
class FileUpload{
private $targetDir;
public function __construct($targetDir) {
$this->targetDir = $targetDir;
}
public function upload(UploadedFile $file) {
$file_name = empty($file->getClientOriginalName()) ? md5(uniqid()).'.'.$file->guessExtension() : $file->getClientOriginalName();
$file->move($this->targetDir, $file_name);
return $file_name;
}
}
#app/config/services.yml
app.file_upload:
class: AppBundle\Services\FileUpload
arguments: ['%stamp_dir%']
Sorry for typos, and please let me know if this works for your case, as I adapted my case to yours.
Check you have uploaded image vie form or not before foreach.
if ($form->isSubmitted() && $form->isValid()) {
/** #var Stamp $stamp */
$stamp = $form->getData();
if(!empty($stamp->getImages()) && count($stamp->getImages()) > 0){ // Check uploaded image
foreach ($stamp->getImages() as $image) {
$fileName = md5(uniqid()) . '.' . $image->guessExtension();
/** #var $image StampImage */
$image->getName()->move(
$this->getParameter('stamps_images_directory'),
$fileName
);
$image->setName($fileName)->setFileName($fileName);
}
}
$em = $this->getDoctrine()->getManager();
$em->persist($stamp);
$em->flush();
$this->addFlash('success', 'Successfully edited a stamp!');
return $this->redirectToRoute('admin_stamps_list');
}
Update #1
Replace
$fileName = md5(uniqid()) . '.' . $image->getName()->guessExtension();
with
$fileName = md5(uniqid()) . '.' . $image->guessExtension();
I'm creating a ticket system for practice purposes.
Right now I'm having a problem with uploading files.
The idea is that a ticket can have multiple attachments, so I created a many-to-one relationship between the ticket and the upload tables.
class Ticket {
// snip
/**
* #ORM\OneToMany(targetEntity="Ticket", mappedBy="ticket")
*/
protected $uploads;
// snip
}
The upload entity class contains the upload functionality, which I took from this tutorial:
<?php
namespace Sytzeandreae\TicketsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity(repositoryClass="Sytzeandreae\TicketsBundle\Repository\UploadRepository")
* #ORM\Table(name="upload")
* #ORM\HasLifecycleCallbacks
*/
class Upload
{
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
private $temp;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Ticket", inversedBy="upload")
* #ORM\JoinColumn(name="ticket_id", referencedColumnName="id")
*/
protected $ticket;
/**
* #ORM\Column(type="string")
*/
protected $title;
/**
* #ORM\Column(type="string")
*/
protected $src;
public function getAbsolutePath()
{
return null === $this->src
? null
: $this->getUploadRootDir().'/'.$this->src;
}
public function getWebPath()
{
return null === $this->src
? null
: $this->getUploadDir().'/'.$this->src;
}
public function getUploadRootDir()
{
// The absolute directory path where uplaoded documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function getUploadDir()
{
// Get rid of the __DIR__ so it doesn/t screw up when displaying uploaded doc/img in the view
return 'uploads/documents';
}
/**
* Sets file
*
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
if (isset($this->path)) {
// store the old name to delete after the update
$this->temp = $this->path;
$this->path = null;
} else {
$this->path = 'initial';
}
}
/**
* Get file
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set src
*
* #param string $src
*/
public function setSrc($src)
{
$this->src = $src;
}
/**
* Get src
*
* #return string
*/
public function getSrc()
{
return $this->src;
}
/**
* Set ticket
*
* #param Sytzeandreae\TicketsBundle\Entity\Ticket $ticket
*/
public function setTicket(\Sytzeandreae\TicketsBundle\Entity\Ticket $ticket)
{
$this->ticket = $ticket;
}
/**
* Get ticket
*
* #return Sytzeandreae\TicketsBundle\Entity\Ticket
*/
public function getTicket()
{
return $this->ticket;
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
// do whatever you want to generate a unique name
$filename = sha1(uniqid(mt_rand(), true));
$this->src = $filename.'.'.$this->getFile()->guessExtension();
}
}
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// move takes the target directory and then the target filename to move to
$this->getFile()->move(
$this->getUploadRootDir(),
$this->src
);
// check if we have an old image
if (isset($this->temp)) {
// delete the old image
unlink($this->getUploadRootDir().'/'.$this->temp);
// clear the temp image path
$this->temp = null;
}
// clean up the file property as you won't need it anymore
$this->file = null;
}
/**
* #ORM\PostRemove
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
}
The form is build as follows:
class TicketType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('priority')
->add('uploads', new UploadType())
}
Where UploadType looks like this:
class UploadType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options) {
$builder->add('file', 'file', array(
'label' => 'Attachments',
'required' => FALSE,
'attr' => array (
'accept' => 'image/*',
'multiple' => 'multiple'
)
));
}
This part seems to work fine, I do get presented a form which contains a file uploader.
Once I put this line in the Ticket entity's constuctor:
$this->uploads = new \Doctrine\Common\Collections\ArrayCollection();
It throws me the following error:
Neither property "file" nor method "getFile()" nor method "isFile()"
exists in class "Doctrine\Common\Collections\ArrayCollection"
If I leave this line out, and upload a file, it throws me the following error:
"Sytzeandreae\TicketsBundle\Entity\Ticket". Maybe you should create
the method "setUploads()"?
So next thing I did was creating this method, try an upload again and now it throws me:
Class Symfony\Component\HttpFoundation\File\UploadedFile is not a
valid entity or mapped super class.
This is where I am really stuck. I fail to see what, at what stage, I did wrong and am hoping for some help :)
Thanks!
You can either add a collection field-type of UploadType() to your form. Notice that you can't use the multiple option with your current Upload entity ... but it's the quickest solution.
Or adapt your Ticket entity to be able to handle multiple files in an ArrayCollection and looping over them.
I have a user profile that allows a user to upload and save a profile picture.
I have a UserProfile entity and a Document entity:
Entity/UserProfile.php
namespace Acme\AppBundle\Entity\Profile;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="ISUserProfile")
*/
class UserProfile extends GenericProfile
{
/**
* #ORM\OneToOne(cascade={"persist", "remove"}, targetEntity="Acme\AppBundle\Entity\Document")
* #ORM\JoinColumn(name="picture_id", referencedColumnName="id", onDelete="set null")
*/
protected $picture;
/**
* Set picture
*
* #param Acme\AppBundle\Entity\Document $picture
*/
public function setPicture($picture)
{
$this->picture = $picture;
}
/**
* Get picture
*
* #return Acme\AppBundle\Entity\Document
*/
public function getPicture()
{
return $this->picture;
}
}
Entity/Document.php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $path;
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
public function getAbsolutePath()
{
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
}
public function getWebPath()
{
return null === $this->path ? null : $this->getUploadDir().'/'.$this->path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
return 'uploads/documents';
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
$this->path = sha1(uniqid(mt_rand(), true)).'.'.$this->file->guessExtension();
}
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
$this->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
}
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set path
*
* #param string $path
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Get path
*
* #return string
*/
public function getPath()
{
return $this->path;
}
/**
* Set file
*
* #param string $file
*/
public function setFile($file)
{
$this->file = $file;
}
/**
* Get file
*
* #return string
*/
public function getFile()
{
return $this->file;
}
}
My user profile form type adds a document form type to include the file uploader on the user profile page:
Form/UserProfileType.php
namespace Acme\AppBundle\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserProfileType extends GeneralContactType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
/*
if(!($pic=$builder->getData()->getPicture()) || $pic->getWebPath()==''){
$builder->add('picture', new DocumentType());
}
*/
$builder
->add('picture', new DocumentType());
//and add some other stuff like name, phone number, etc
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\AppBundle\Entity\Profile\UserProfile',
'intention' => 'user_picture',
'cascade_validation' => true,
));
}
public function getName()
{
return 'user_profile_form';
}
}
Form/DocumentType.php
namespace Acme\AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DocumentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\AppBundle\Entity\Document',
));
}
public function getName()
{
return 'document_form';
}
}
In my controller I have an update profile action:
Controller/ProfileController.php
namespace Acme\AppBundle\Controller;
class AccountManagementController extends BaseController
{
/**
* #param Symfony\Component\HttpFoundation\Request
* #return Symfony\Component\HttpFoundation\Response
*/
public function userProfileAction(Request $request) {
$user = $this->getCurrentUser();
$entityManager = $this->getDoctrine()->getEntityManager();
if($user && !($userProfile = $user->getUserProfile())){
$userProfile = new UserProfile();
$userProfile->setUser($user);
}
$uploadedFile = $request->files->get('user_profile_form');
if ($uploadedFile['picture']['file'] != NULL) {
$userProfile->setPicture(NULL);
}
$userProfileForm = $this->createForm(new UserProfileType(), $userProfile);
if ($request->getMethod() == 'POST') {
$userProfileForm->bindRequest($request);
if ($userProfileForm->isValid()) {
$entityManager->persist($userProfile);
$entityManager->flush();
$this->get('session')->setFlash('notice', 'Your user profile was successfully updated.');
return $this->redirect($this->get('router')->generate($request->get('_route')));
} else {
$this->get('session')->setFlash('error', 'There was an error while updating your user profile.');
}
}
$bindings = array(
'user_profile_form' => $userProfileForm->createView(),
);
return $this->render('user-profile-template.html.twig', $bindings);
}
}
Now, this code works... but it's ugly as hell. why do I have to check the request object for an uploaded file and set the picture to null so Symfony realises that it needs to persist a new Document entity?
Surely having a simple user profile page with the option to upload an image as a profile picture should be simpler than this?
What am I missing??
(I don´t think you expect some feedback now, after some months, but maybe it helps other people with the same issue)
I have a similar setup and goal, found this post and after copying some of the code I found out that I don´t need the "$uploadedFile"-snippet that you refer to as "ugly". I override the fos registration form and all I need to add is a DocumentType.php and a RegistrationFormType.php (like your user UserProfileType).
I can´t tell why your version needs the ugly bit of code. (or maybe I will run into the same issue when I´m coding an Update form?).