I'm using VichUploaderBundle in my application to upload the files.
Problem is that even if I followed the docs, the file isn't uploaded (even if seems to be all right, as profiles sais), and no rows was make in the entity table.
Here is my vich_uploader.yaml:
vich_uploader:
db_driver: orm
mappings:
media:
uri_prefix: /media
upload_destination: '%kernel.project_dir%/public/media'
inject_on_load: false
delete_on_update: true
delete_on_remove: true
Here is my Media Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #Vich\Uploadable
*/
class Media
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="media", fileNameProperty="imageName", size="imageSize")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="integer")
*
* #var integer
*/
private $imageSize;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $imageFile
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
if (null !== $imageFile) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTimeImmutable();
}
}
public function getImageFile(): ?File
{
return $this->imageFile;
}
public function setImageName(?string $imageName): void
{
$this->imageName = $imageName;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageSize(?int $imageSize): void
{
$this->imageSize = $imageSize;
}
public function getImageSize(): ?int
{
return $this->imageSize;
}
}
I just need to store the file in the entity, there is no relations between files and other entities.
Here is my MediaType
<?php
namespace App\Form;
use App\Entity\Media;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
class MediaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('imageFile', VichImageType::class, [
'required' => false,
'allow_delete' => true,
'download_uri' => true
])
->add('submit', SubmitType::class, [
'attr' => [
'class' => 'btn btn-success'
],
'label' => 'Upload File'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Media::class,
]);
}
}
The Form is handled by this Twig template:
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel">
<div class="panel-body">
{{ form_start(form) }}
{{ form_widget(form.imageFile) }}
</div>
<div class="panel-footer">
{{ form_widget(form.submit) }}
{{ form_end(form) }}
</div>
</div>
</div>
</div>
</div>
Related
I want to render a filtered collection as a checkbox list.
But i have trouble to get the collection shown. i get "Catchable Fatal Error: Object of class Doctrine\ORM\PersistentCollection could not be converted to string".
Below is my formtype:
class PropertyfilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('view', EntityType::class, [
'class' => Propsearch::class,
'choice_label' => 'propsearchviews',
'expanded' => true,
'multiple' => true
]);
}
This is my many-to-many entity
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
*/
class Propsearch
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var Propsearchview[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Propview", cascade={"persist"})
* #ORM\JoinTable(name="propsearch_propview")
* #ORM\OrderBy({"title": "ASC"})
* #Assert\Count(max="4", maxMessage="Can only select 4 views")
*/
private $propsearchviews;
/**
* #var Propsearchfacility[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Propfacility", cascade={"persist"})
* #ORM\JoinTable(name="propsearch_propfacility")
* #ORM\OrderBy({"title": "ASC"})
* #Assert\Count(max="4", maxMessage="Can only select 4 facilities")
*/
private $propsearchfacilities;
public function getId(): ?int
{
return $this->id;
}
public function __construct()
{
$this->propsearchviews = new ArrayCollection();
$this->propsearchfacilities = new ArrayCollection();
}
/**
* #return Collection|Propsearchview[]
*/
public function getPropsearchviews(): Collection
{
return $this->propsearchviews;
}
public function addPropsearchview(Propsearchview $propsearchview): self
{
if (!$this->propsearchviews->contains($propsearchview)) {
$this->propsearchviews[] = $propsearchview;
}
return $this;
}
public function removePropsearchview(Propsearchview $propsearchview): self
{
if ($this->propsearchviews->contains($propsearchview)) {
$this->propsearchviews->removeElement($propsearchview);
}
return $this;
}
/**
* #return Collection|Propsearchfacility[]
*/
public function getPropsearchfacilities(): Collection
{
return $this->propsearchfacilities;
}
public function addPropsearchfacility(Propsearchfacility $propsearchfacility): self
{
if (!$this->propsearchfacilities->contains($propsearchfacility)) {
$this->propsearchfacilities[] = $propsearchacility;
}
return $this;
}
public function removePropsearchfacility(Propsearchfacility $propsearchfacility): self
{
if ($this->propsearchfacilities->contains($propsearchfacility)) {
$this->propsearchfacilities->removeElement($propsearchfacility);
}
return $this;
}
}
This is my original view entity.
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table(name="propview")
*
* Defines the properties of the Tag entity to represent the post tags.
*
* See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
*
* #author Yonel Ceruto <yonelceruto#gmail.com>
*/
class Propview
{
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string", length=191)
*/
private $title;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function __toString(): string
{
return $this->title;
}
}
So i want to show the collection of views as a checkbox list that has been added to the propsearch table in the form.
Thanks in advance!
Edit 2
Okay so i have the propsearchviews which has an colleciton from propviewtype. including the dataclass from propsearch.
I changed my propertyfiltertype to the following:
<?php
namespace App\Form;
use App\Entity\Propsearch;
class PropertyfilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('propsearchviews', CollectionType::class, [
'entry_type' => PropviewType::class,
'by_reference' => false,
]);
}
the propviewtype itself
namespace App\Form\Type;
use App\Entity\Propview;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class PropviewType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('propview', EntityType::class, [
'class' => Propview::class,
'choice_label' => 'title',
]);
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Propview::class,
));
}
}
and my html.twig file
<div class="col-12 col-md-4 mb-2">
{% for field in propertybuyform.propsearchviews %}
<div class="col-xs-4">
{{ form_widget(field) }}
{{ form_label(field) }}
</div>
{% endfor %}
</div>
You should use embedded form functionality to achieve this. Please refer to https://symfony.com/doc/current/form/form_collections.html to get idea of how it could be implemented.
Briefly describing your case - you should create PropsearchType which would render propsearchviews property as CollectionType, where 'entry_type' would be another custom form type that you should create - PropviewType, that should render your Propviews as checboxes.
I'm developing a king of simple CMS, with Symfony 4.1.
Regarding my question, we have 2 entities:
Post Entity:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post extends BaseEntity
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="text")
*/
private $content;
/**
* #ORM\Column(type="boolean")
*/
private $status;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Category", inversedBy="posts")
*/
private $categories;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Picture", mappedBy="post", orphanRemoval=true, cascade={"persist"})
*/
private $pictures;
/**
* #Assert\All({#Assert\Image(mimeTypes="image/jpeg")})
*
*/
private $pictureFiles;
/**
* Post constructor.
*/
public function __construct()
{
$this->categories = new ArrayCollection();
$this->pictures = new ArrayCollection();
}
/**
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* #return null|string
*/
public function getContent(): ?string
{
return $this->content;
}
/**
* #param string $content
* #return Post
*/
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
/**
* #return bool|null
*/
public function getStatus(): ?bool
{
return $this->status;
}
/**
* #param bool $status
* #return Post
*/
public function setStatus(bool $status): self
{
$this->status = $status;
return $this;
}
/**
* #return Collection|Category[]
*/
public function getCategories(): Collection
{
return $this->categories;
}
/**
* #param Category $category
* #return Post
*/
public function addCategory(Category $category): self
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
}
return $this;
}
/**
* #param Category $category
* #return Post
*/
public function removeCategory(Category $category): self
{
if ($this->categories->contains($category)) {
$this->categories->removeElement($category);
}
return $this;
}
/**
* #return Collection|Picture[]
*/
public function getPictures(): Collection
{
return $this->pictures;
}
/**
* #param Picture $picture
* #return Post
*/
public function addPicture(Picture $picture): self
{
if (!$this->pictures->contains($picture)) {
$this->pictures[] = $picture;
$picture->setPost($this);
}
return $this;
}
/**
* #param Picture $picture
* #return Post
*/
public function removePicture(Picture $picture): self
{
if ($this->pictures->contains($picture)) {
$this->pictures->removeElement($picture);
if ($picture->getPost() === $this) {
$picture->setPost(null);
}
}
return $this;
}
/**
* #return mixed
*/
public function getPictureFiles()
{
return $this->pictureFiles;
}
/**
* #param $pictureFiles
* #return Post
*/
public function setPictureFiles($pictureFiles): self
{
foreach ($pictureFiles as $pictureFile) {
/** #var Picture $picture */
$picture = new Picture();
$picture->setImageFile($pictureFile);
$this->addPicture($picture);
}
$this->pictureFiles = $pictureFiles;
return $this;
}
}
Picture Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
/**
* #ORM\Entity(repositoryClass="App\Repository\PictureRepository")
*/
class Picture
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var File|null
* #Assert\Image(mimeTypes="image/jpeg")
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Post", inversedBy="pictures")
* #ORM\JoinColumn(nullable=false)
*/
private $post;
/**
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* #return File|null
*/
public function getImageFile(): ? File
{
return $this->imageFile;
}
/**
* #param File|null $imageFile
* #return Picture
*/
public function setImageFile(? File $imageFile): self
{
$this->imageFile = $imageFile;
return $this;
}
/**
* #return string|null
*/
public function getFilename(): ?string
{
return $this->filename;
}
/**
* #param string $filename
* #return Picture
*/
public function setFilename(string $filename): self
{
$this->filename = $filename;
return $this;
}
/**
* #return Post|null
*/
public function getPost(): ?Post
{
return $this->post;
}
/**
* #param Post|null $post
* #return Picture
*/
public function setPost(?Post $post): self
{
$this->post = $post;
return $this;
}
}
So for adding a Post, I have a PostType:
<?php
namespace App\Form;
use App\Entity\Category;
use App\Entity\Post;
use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class PostType
* #package App\Form
*/
class PostType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('content', CKEditorType::class)
->add('categories', EntityType::class,
[
'class' => Category::class,
'required' => true,
'choice_label' => 'name',
'multiple' => true,
]
)
->add('pictureFiles', FileType::class,
[
'required' => false,
'multiple' => true,
'label' => 'Add files...',
'attr' =>
[
'action' => '%kernel.project_dir%/public/media/posts'
]
]
)
->add('status')
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}
The view corresponding to that form:
{% form_theme form '/admin/form/switch_btn_layout.html.twig' %}
{{ form_start(form) }}
{{ form_errors(form) }}
<div class="form-row">
<div class="col-md-6">
{{ form_row(form.name) }}
{{ form_row(form.categories) }}
{{ form_row(form.status) }}
</div>
<div class="col-md-6 dropzone" id="postDropzone">
{{ form_row(form.pictureFiles, {'attr': {'class': 'dropzone'}} ) }}
<div class="dropzone-previews" style="border: 1px solid red"></div>
</div>
</div>
<div class="form-group">
{{ form_row(form.content) }}
</div>
<div class="form-group">
{{ form_row(form.status) }}
</div>
{{ form_rest(form) }}
<button class="btn btn-success btn-lg btn-block" id="postSubmit">
{{ button_label|default('Save') }}
</button>
{{ form_end(form) }}
As you can see, the "input" for files as the dropzone css class.
Indeed, my project include the oneup_uploader bundle, for dropzone.
Here the configuration for oneup_uploader:
oneup_uploader:
mappings:
# This is a mapping example, remove it and create your own mappings.
post_image:
frontend: dropzone
namer: oneup_uploader.namer.uniqid
storage:
directory: '%kernel.project_dir%/public/media/posts'
And my script for Dropzone:
Dropzone.autoDiscover = false;
var postDropzone = new Dropzone('.dropzone', {
url: '%kernel.project_dir%/public/media/posts',
// url: 'file/post',
maxFiles: 10,
addRemoveLinks: true,
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 100,
});
postDropzone.on("addedfile", function (file) {
file.previewElement.addEventListener("click", function () {
postDropzone.removeFile(file);
})
});
The issue for me is:
no file is save in the folder
the Post entity is save in my DB, but nothing is save for Pictures.
I also tried to not use OneUploaderBundle, and use VichUploader: the saving part in DB is perfect, but I can't link it to dropzone.
Some help guys ?
Thanks a lot !
Might be useful for new visitors.
You can use a library that extends Symfony Form and adds a new type DropzneType.
1.Install the library
composer require emrdev/symfony-dropzone
This way you will have a new form type DropzoneType
2.Use the type in your form like this
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options)
{
// userFiles is OneToMany
$builder->add('userFiles', DropzoneType::class, [
'class' => File::class,
'maxFiles' => 6,
'uploadHandler'=>'uploadHandler', // route name
'removeHandler'=> 'removeHandler'// route name
]);
}
Change the uploadHandler and removeHandler options to your endpoints
3.Route uploadHandler/removeHandler might look something like this
/**
* #Route("/uploadhandler", name="uploadHandler")
*/
public function uploadhandler(Request $request, ImageUploader $uploader) {
$doc = $uploader->upload($request->files->get('file'));
$file = new File();
$file->setSrc($doc['src']);
...
$this->getDoctrine()->getManager()->persist($file);
$this->getDoctrine()->getManager()->flush();
return new JsonResponse($file);
}
/**
* #Route("/removeHandler/{id}", name="removeHandler")
*/
public function removeHandler(Request $request,File $file = null) {
$this->getDoctrine()->getManager()->remove($file);
$this->getDoctrine()->getManager()->flush();
return new JsonResponse(true);
}
note that uploadhandler should return a File object
you should pass upload url instead of upload directory
generate url in twig - {{ oneup_uploader_endpoint('post_image') }}
var postDropzone = new Dropzone('.dropzone', {
url: '{{ oneup_uploader_endpoint('post_image') }}',
// url: '%kernel.project_dir%/public/media/posts',
// url: 'file/post',
maxFiles: 10,
addRemoveLinks: true,
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 100,
});
I try to allowed the multiple upload with the bundle VichUploader. On the project, i have a class Theater which own a main image but also a number of secondary images (Collection of images). Actually each secondary images is a Ressource.
So a Theater have onetomany Resources and a Resource is connected to one theater.
But when i try to create i can access to my form but i have an error when i try to save which is :
Expected argument of type "AppBundle\Entity\Resources", "AppBundle\Entity\Theatre" given
Here this is my class Theater with only details for multiple upload :
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Theatre
*
* #ORM\Table(name="theatre")
* #ORM\Entity
* #Vich\Uploadable
*/
class Theatre
{
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Resources", mappedBy="theatre", cascade={"persist", "remove"}, orphanRemoval=true)
*/
private $images;
// ..
/**
* Constructor
*/
public function __construct()
{
$this->images = new \Doctrine\Common\Collections\ArrayCollection();
}
// MultiUpload
/**
* #return ArrayCollection
*/
public function getImages()
{
return $this->images;
}
/**
* #param ArrayCollection $pictures
*/
public function setImages($pictures)
{
$this->images = $pictures;
}
public function getAttachImages()
{
return null;
}
/**
* #param array $files
*
* #return array
*/
public function setAttachImages(array $files=array())
{
if (!$files) return [];
foreach ($files as $file) {
if (!$file) return [];
$this->attachImages($file);
}
return [];
}
/**
* #param UploadedFile|null $file
*/
public function attachImages(UploadedFile $file=null)
{
if (!$file) {
return;
}
$picture = new Resources();
$picture->setImage($file);
$this->addImage($picture);
}
/**
* Add image.
*
* #param \AppBundle\Entity\Resources $image
*
*
*/
public function addImage(\AppBundle\Entity\Resources $image)
{
$image->setTheatre($this);
//$this->images->add($image);
$this->images[] = $image;
//return $this;
}
/**
* Remove image.
*
* #param \AppBundle\Entity\Resources $image
*
*
*/
public function removeImage(\AppBundle\Entity\Resources $image)
{
$image->setTheatre(null);
$this->images->removeElement($image);
// return $this->images->removeElement($image);
}
Then my class Resources :
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Resources
*
* #ORM\Table(name="resources")
* #ORM\Entity
* #Vich\Uploadable
*/
class Resources
{
/**
* #var Theatre
* #ORM\ManyToOne(targetEntity="Theatre", inversedBy="images")
*/
private $theatre;
/**
* #Vich\UploadableField(mapping="uploads_image", fileNameProperty="url")
* #Assert\File(
* mimeTypes = {"image/png", "image/jpeg", "image/jpg"},
* mimeTypesMessage = "Please upload a valid valid IMAGE"
* )
*
*
* #var File $image
*/
protected $image;
/**
* #ORM\Column(type="string", length=255, name="url")
*
* #var array $url
*/
protected $url;
/**
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* #return Theatre
*/
public function setImage(File $image = null)
{
$this->image = $image;
}
public function getImage()
{
return $this->image;
}
// ..
/**
* Set theatre.
*
* #param \AppBundle\Entity\Theatre $theatre
*
* #return Resources
*/
public function setTheatre(\AppBundle\Entity\Theatre $theatre)
{
$this->theatre = $theatre;
return $this;
}
/**
* Get theatre.
*
* #return \AppBundle\Entity\Theatre
*/
public function getTheatre()
{
return $this->theatre;
}
/**
* Set url.
*
* #param string $url
*
* #return Resources
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get url.
*
* #return array
*/
public function getUrl()
{
return $this->url;
}
Then I add to the builder :
namespace AppBundle\Form\Collection;
use AppBundle\Entity\Theatre;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Vich\UploaderBundle\Form\Type\VichFileType;
class TheatreImages extends AbstractType{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('attachImages', FileType::class, ['multiple'=>true, 'required'=>false])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Theatre::class,
));
}
/**
* #return string
*/
public function getName()
{
return 'app_theatreImages';
}
}
I add my config with the bundle easyAdminBundle:
easy_admin:
entities:
Theatre:
class: AppBundle\Entity\Theatre
list:
title: 'Liste des théâtres'
fields:
- 'Name'
- 'adress'
- 'Metro station'
- { property: 'Main image', type: 'image', template: 'theatreFile.html.twig', base_path: '%app.path.theatre_images%' }
new:
title: 'Création théâtre'
fields:
- { type: 'section', label: 'Information du théâtre' }
- {property: 'name', label: 'Nom'}
- {property: 'number_of_seats', type: 'integer', label: 'Nombre de sièges'}
- {property: 'about', label: 'description'}
- { property: 'imageFile', type: 'vich_file', label: 'image', type_options: { required: false}}
- {property: 'images', type: 'collection', type_options: {entry_type: 'AppBundle\Form\Collection\TheatreImages', by_reference: false}}
- { type: 'section', label: 'Localisation du théâtre' }
- {property: 'adress', label: 'adresse'}
- {property: 'metro_station', label: 'Station de métro'}
- {property: 'location_coordinates', label: 'Coordonnées'}
edit:
title: "Édition théâtre"
fields:
- { type: 'section', label: 'Information du théâtre' }
- {property: 'name', label: 'Nom'}
- {property: 'number_of_seats', type: 'integer', label: 'Nombre de sièges'}
- {property: 'about', label: 'description'}
- { property: 'imageFile', type: 'vich_file', label: 'image', type_options: { required: false}}
- {property: 'images', type: 'collection', type_options: {entry_type: 'AppBundle\Form\Collection\TheatreImages', by_reference: false}}
- { type: 'section', label: 'Localisation du théâtre' }
- {property: 'adress', label: 'adresse'}
- {property: 'metro_station', label: 'Station de métro'}
- {property: 'location_coordinates', label: 'Coordonnées'}
Thanks in advance.
I have created a solution for a very useful bundle [VichUploader] which is missing the functionality of multiple uploads and it works on every Symfony version, I have created on [Symfony] 5.2.
It's on OneToMany relation and it works fine. So I have used CollectionType and [VichFileType] in my custom forms and little trick in my controller, Here the code and to see all the project, you can find it in my GitHub
[a link] https://github.com/malek-laatiri
Admission.php
class Admission
{
/**
* #ORM\OneToMany(targetEntity=Diplome::class, mappedBy="admission")
*/
private $diplomes;
/**
* #return Collection|Diplome[]
*/
public function getDiplomes(): Collection
{
return $this->diplomes;
}
public function addDiplome(Diplome $diplome): self
{
if (!$this->diplomes->contains($diplome)) {
$this->diplomes[] = $diplome;
$diplome->setAdmission($this);
}
return $this;
}
public function removeDiplome(Diplome $diplome): self
{
if ($this->diplomes->removeElement($diplome)) {
// set the owning side to null (unless already changed)
if ($diplome->getAdmission() === $this) {
$diplome->setAdmission(null);
}
}
return $this;
}
}
Diplome.php
<?php
namespace App\Entity;
use App\Repository\DiplomeRepository;
use Doctrine\ORM\Mapping as ORM;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity(repositoryClass=DiplomeRepository::class)
* #Vich\Uploadable
*/
class Diplome
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=Admission::class, inversedBy="diplomes",cascade={"persist","remove"})
*/
private $admission;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #Vich\UploadableField(mapping="product_image", fileNameProperty="name")
* #var File
*/
private $file;
public function getId(): ?int
{
return $this->id;
}
public function getAdmission(): ?Admission
{
return $this->admission;
}
public function setAdmission(?Admission $admission): self
{
$this->admission = $admission;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getFile()
{
return $this->file;
}
public function setFile( $file)
{
$this->file = $file;
return $this;
}
}
AdmissionType.php
<?php
namespace App\Form;
use App\Entity\Admission;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichFileType;
class Admission1Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('diplomes', CollectionType::class, [
'entry_type' => DiplomeType::class,
'allow_add' => true,
'allow_delete' => true,
'required' => false,
'label'=>false,
'by_reference' => false,
'disabled' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Admission::class,
]);
}
}
DiplomeType.php
<?php
namespace App\Form;
use App\Entity\Diplome;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichFileType;
class DiplomeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file',VichFileType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Diplome::class,
"allow_extra_fields" => true,
]);
}
}
_form.html.twig
<ul id="diplomes-fields-list"
data-prototype="{{ form_widget(form.diplomes.vars.prototype)|e }}"
data-widget-tags="{{ '<li></li>'|e }}"
data-widget-counter="{{ form.diplomes|length }}">
{% for emailField in form.diplomes %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
<button type="button"
class="add-another-collection"
data-list-selector="#diplomes-fields-list">Add another email
</button>
script.js
jQuery(document).ready(function () {
jQuery('.add-another-collection').click(function (e) {
var list = $("#diplomes-fields-list");
var counter = list.data('widget-counter') | list.children().length;
var newWidget = list.attr('data-prototype');
newWidget = newWidget.replace(/__name__/g, counter);
counter++;
list.data('widget-counter', counter);
var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget);
newElem.appendTo(list);
newElem.append('remove');
$('.remove-tag').click(function(e) {
e.preventDefault();
$(this).parent().remove();
});
});
});
Controller.php
$admission = new Admission();
$form = $this->createForm(Admission1Type::class, $admission);
$form->handleRequest($request);
$entityManager = $this->getDoctrine()->getManager();
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form->getData()->getNotes() as $dip){
$entityManager->persist($dip);
$admission->addNote($dip);
}
$entityManager->persist($admission);
$entityManager->flush();
Vihcuploader not support multiple file uploads well, you have to create a custom form type to handle it maybe this solution would help:
github user soluton
Let's imagine that we want to assign categories to a post, we use an EntityType to generate them based on the amount of categories we have, so we just add the next block of code to our form:
Controller:
->add('categories', EntityType::class, array(
'class' => Category::class,
'choice_label' => 'category_description',
'multiple' => true,
'expanded' => true,
'required' => false,
))
And then save them to the database when the form is submitted, ManyToMany, using ArrayCollection:
foreach($post_data['categories'] as $form_category)
{
$database_category = $database_manager->getRepository(Category::class)->find($form_category->getId());
$post->addCategory($database_category);
}
Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
...
/**
* #ORM\ManyToMany(targetEntity="Category", cascade={"persist"})
* #ORM\JoinTable(name="junction_table",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
private $categories;
public function __construct() {
$this->categories = new ArrayCollection();
}
...
public function getCategories()
{
return $this->categories;
}
public function addCategory(Category $category): self
{
$this->categories->add($category);
return $this;
}
}
Image:
Generated checkboxes with description and index
But what if i want to have those checkboxes preselected when i go to edit mode so the user knows which ones were selected last time, how would you approach that?
Also how would you remove one if it is deselected?
If your form fields are mapped to an entity, the data should be set automatically.
You don't even have to add the categories manually to the post.
Here is a full sample of working code that matches what you want to do:
//src/Entity/Group.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\GroupRepository")
* #ORM\Table(name="`group`")
*/
class Group
{
/**
* Group constructor.
*/
public function __construct()
{
$this->players = new ArrayCollection();
}
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Player", inversedBy="groups")
*/
private $players;
/**
* #return mixed
*/
public function getPlayers()
{
return $this->players;
}
/**
* #param mixed $players
*
* #return Group
*/
public function setPlayers($players)
{
$this->players = $players;
return $this;
}
/**
* #param Player $player
*
* #return Group
*/
public function addPlayer(Player $player)
{
$this->players->add($player);
return $this;
}
/**
* #param Player $player
*
* #return Group
*/
public function removePlayer(Player $player)
{
$this->players->removeElement($player);
return $this;
}
}
//src/Entity/Player.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\PlayerRepository")
*/
class Player
{
/**
* #return string
*/
public function __toString()
{
return $this->name;
}
/**
* Player constructor.
*/
public function __construct()
{
$this->groups = new ArrayCollection();
}
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Group", mappedBy="players")
*/
private $groups;
/**
* #return mixed
*/
public function getGroups()
{
return $this->groups;
}
/**
* #param mixed $groups
*
* #return Player
*/
public function setGroups($groups)
{
$this->groups = $groups;
return $this;
}
/**
* #param Group $group
*
* #return Player
*/
public function addGroup(Group $group)
{
$this->groups->add($group);
return $this;
}
/**
* #param Group $group
*
* #return Player
*/
public function removeGroup(Group $group)
{
$this->groups->removeElement($group);
return $this;
}
}
//src/Form/GroupType.php
namespace App\Form;
use App\Entity\Group;
use App\Entity\Player;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GroupType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'players',
EntityType::class,
[
'class' => Player::class,
'multiple' => true,
'expanded' => true,
'required' => false,
]
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => Group::class,
]
);
}
}
//src/Controller/GroupController.php
namespace App\Controller;
use App\Entity\Group;
use App\Form\GroupType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class GroupController extends Controller
{
/**
* #Route("/edit-group/{id}", name="group_edit")
* #param Request $request
* #param Group $group
*
* #return Response
*/
public function editGroup(Request $request, Group $group)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(GroupType::class, $group);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($group);
$em->flush();
}
return new Response(
$this->renderView(
'group/edit-group.html.twig',
array(
'form' => $form->createView(),
)
)
);
}
}
//templates/group/edit-group.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<h1>Edit group</h1>
</div>
{{ form_start(form) }}
{{ form_row(form.players) }}
<button type="submit" class="btn btn-success">Edit</button>
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
{% endblock %}
I have a problem with VichUploaderBundle, I followed all the instructions but I dont know how my Controller should handle this after finished form becouse image failed to uploaded and database did not write.
my Entity class:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity
* #Vich\Uploadable
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ..... other fields
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="product_image", fileNameProperty="imageName", size="imageSize")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="integer")
*
* #var integer
*/
private $imageSize;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* #return Product
*/
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
if ($image) {
var_dump($image);
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTimeImmutable();
}
return $this;
}
/**
* #return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
* #param string $imageName
*
* #return Product
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
return $this;
}
/**
* #return string|null
*/
public function getImageName()
{
return $this->imageName;
}
/**
* #param integer $imageSize
*
* #return Product
*/
public function setImageSize($imageSize)
{
$this->imagesize = $imageSize;
return $this;
}
/**
* #return integer|null
*/
public function getImageSize()
{
return $this->imageSize;
}
}
my FormType class:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
class AvatarType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('imageFile', FileType::class, array('label' => 'Brochure (PDF file)'))
;
}
public function getName()
{
return 'avatar_form';
}
}
config:
vich_uploader:
db_driver: orm # or mongodb or propel or phpcr
mappings:
product_image:
uri_prefix: /image/avatar
upload_destination: '%kernel.root_dir%/../web/image/avatar'
Twig template:
<div>
{{ form_start(form) }}
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form.imageFile) }}
<button type="submit" class="btn btn-default">Submit</button>
{{ form_end(form) }}
</div>
and finally Controller:
namespace AppBundle\Controller\User;
use AppBundle\Entity\Product;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use AppBundle\Controller\InitController;
use AppBundle\Form\AvatarType;
class UserController extends Controller implements InitController
{
/**
* #Route("/user/avatar", name="AvatarAction")
*/
public function AvatarAction(Request $request)
{
$form = $this->createForm('AppBundle\Form\AvatarType',null,array(
// To set the action use $this->generateUrl('route_identifier')
'action' => $this->generateUrl('AvatarAction'),
'method' => 'POST'
));
$form->handleRequest($request);
if ($request->isMethod('POST')) {
if($form->isValid()){
//$track = new Product();
//$em = $this->getDoctrine()->getManager();
//$em->persist($track);
//$em->flush();
}
return $this->render('default/avatar.html.twig',array(
'form' => $form->createView()
));
}
}
Thanks for all your advice!
Maybe this is not the solution, but your line :
->add('imageFile', FileType::class, array('label' => 'Brochure (PDF file)')):
Should be :
->add('imageFile', VichFileType::class, array('label' => 'Brochure (PDF file)'))
More info : https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/form/vich_file_type.md
What Symfony tells you as error ?
Have a nice day