So, after research a lot and get no results (maybe I'm a bad searcher) I coming from this topics: SonataAdmin Bundle File Upload Error and SonataMediaBundle - how to upload images? I can't find a solution for my problem. I have a Entity Company and each company can have multiple files: PDF, DOC, XLS and some other mime/types. I think to use VichUploaderBundle but again docs only covers example for one to one relationship so my question is, any can give me some examples or ways to get this done? I mean upload files and attach them to company?
EDIT1 working and testing
As I said before I'm trying to integrate SonataMediaBundle into another admin module I have but I can't get it to work. What I did until now?
Of course install and configure all bundles: SonataAdminBundle and SonataMediaBundle both are working fine
Modified \Application\Sonata\MediaBundle\Entity\Media.php class to add the needed functionality by adding a ManyToMany relationship
namespace Application\Sonata\MediaBundle\Entity;
use Sonata\MediaBundle\Entity\BaseMedia as BaseMedia;
use Doctrine\ORM\Mapping as ORM;
class Media extends BaseMedia {
/**
* #var integer $id
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="PL\OrderBundle\Entity\Order", inversedBy="medias")
* #ORM\JoinTable(name="order_has_media__media",
* joinColumns={#ORM\JoinColumn(name="media__media_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="order_no_order", referencedColumnName="no_order")}
* )
*/
protected $orders;
public function __construct() {
$this->orders = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer $id
*/
public function getId() {
return $this->id;
}
public function setOrders(\PL\OrderBundle\Entity\Order $order) {
$this->orders[] = $order;
}
public function getOrders() {
return $this->orders;
}
}
Adding the need fields in PL\OrderBundle\Entity\Order.php as follow:
namespace PL\OrderBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="tb_order")
*/
class Order {
/**
* #ORM\Id
* #ORM\Column(type="string", length=15, unique=true, nullable=false)
*/
protected $no_order;
/**
* #ORM\ManyToOne(targetEntity="PL\CompanyBundle\Entity\Company", inversedBy="id")
*/
protected $company;
/**
* #ORM\Column(type="string", length=15, unique=true)
*/
protected $business_case;
/**
* #ORM\Column(type="integer", length=1)
*/
protected $charge_status;
/**
* #ORM\Column(type="datetime")
*/
protected $eta;
/**
* #ORM\Column(type="datetime")
*/
protected $etd;
/**
* #ORM\Column(type="integer", length=1)
*/
protected $transport_media;
/**
* #ORM\Column(type="integer", length=1)
*/
protected $incoterm;
/**
* #ORM\Column(type="string", length=250)
*/
protected $comments;
/**
* #ORM\ManyToMany(targetEntity="Application\Sonata\MediaBundle\Entity\Media", mappedBy="orders")
*/
protected $medias;
public function __construct() {
$this->medias = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setNoOrder($no_order) {
$this->no_order = $no_order;
}
public function getNoOrder() {
return $this->no_order;
}
public function setCompany(\PL\CompanyBundle\Entity\Company $company) {
$this->company = $company;
}
public function getCompany() {
return $this->company;
}
public function setBusinessCase($business_case) {
$this->business_case = $business_case;
}
public function getBusinessCase() {
return $this->business_case;
}
public function setChargeStatus($charge_status) {
$this->charge_status = $charge_status;
}
public function getChargeStatus() {
return $this->charge_status;
}
public function setETA($eta) {
$this->eta = $eta;
}
public function getETA() {
return $this->eta;
}
public function setETD($etd) {
$this->etd = $etd;
}
public function getETD() {
return $this->etd;
}
public function setTransportMedia($transport_media) {
$this->transport_media = $transport_media;
}
public function getTransportMedia() {
return $this->transport_media;
}
public function setIncoterm($incoterm) {
$this->incoterm = $incoterm;
}
public function getIncoterm() {
return $this->incoterm;
}
public function setComments($comments) {
$this->comments = $comments;
}
public function getComments() {
return $this->comments;
}
public function setMedias(\Application\Sonata\MediaBundle\Entity\Media $media) {
$this->medias[] = $media;
}
public function addMedia(\Application\Sonata\MediaBundle\Entity\Media $media) {
$this->medias[] = $media;
}
public function getMedias() {
return $this->medias;
}
}
Changed the configureFormFields in OrderAdmin.php file as follow:
protected function configureFormFields(FormMapper $form) {
$form
->add('no_order', null, array('label' => 'No. Order'))
->add('company', 'entity', array('class' => 'PL\CompanyBundle\Entity\Company', 'label' => 'Cliente'))
->add('business_case', null, array('label' => 'BC'))
->add('charge_status', 'choice', array('choices' => array(
"empty_value" => "Seleccione una opción",
"0" => "Ninguno",
"1" => "Proceso de Fabricacion",
"2" => "Pickup en destino",
"3" => "A la espera de recojo por cliente",
"4" => "Carga en transito",
"5" => "Carga arribada",
"6" => "En proceso de aduana",
"7" => "Entregado a cliente",
"8" => "En bodega"
), "required" => true, 'label' => 'Estado de la carga'))
->add('eta', null, array('label' => 'ETD'))
->add('etd', null, array('label' => 'ETA'))
->add('transport_media', 'choice', array('choices' => array("empty_value" => "Seleccione una opción", "0" => "EXW", "1" => "Maritimo", "2" => "Aereo"), "required" => true, 'label' => 'Via de Transporte'))
->add('incoterm', 'choice', array('choices' => array(
"empty_value" => "Seleccione una opción",
"0" => "Ninguno",
"1" => "EWX",
"2" => "FOB",
"3" => "CIF",
"4" => "DDP"
), "required" => true, 'label' => 'Incoterm'))
->add('comments', null, array('label' => 'Comentarios'))
->add('medias', 'sonata_type_collection', array(
'label' => 'Documentos',
'type_options' => array('delete' => true)), array(
'edit' => 'inline', 'inline' => 'table', 'sortable' => 'position')
);
}
But this doesn't work since I can't upload any file and this is what I want upload many files from the same form and attach them to the order I'm creating. See the attached images for a visual I get when I access the create action:
What I'm missing?
For your solution to have multiple images for your company admin you have to organize your relation like there will be one junction entity which will point to sonata media entity in a ManyToOne relation and also to your products entity in a ManyToOne relation as well i have created this type of collection for one of needs for footer widgets which can have multiple images so you can map it for your products images as well in a similar way.
Footer Entity contains a property named as links which points to a junction entity FooterWidgetsHasMedia in OneToMany way,junction entity (FooterWidgetsHasMedia ) holds the relation to sonata media in addition i need multiple images for my each footer object and also for each image a in need a hover image too so my junction entity basically holds two properties that points to sonata media
FooterWidgets
/**
* #Assert\NotBlank()
* #ORM\OneToMany(targetEntity="Traffic\WidgetsBundle\Entity\FooterWidgetsHasMedia", mappedBy="footerWidget",cascade={"persist","remove"} )
*/
protected $links;
/**
* Remove widgetImages
*
* #param \Application\Sonata\MediaBundle\Entity\Media $widgetImages
*/
public function removeLinks(\Traffic\WidgetsBundle\Entity\FooterWidgetsHasMedia $links)
{
$this->links->removeElement($links);
}
/**
* Get widgetImages
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getLinks()
{
return $this->links;
}
/**
* {#inheritdoc}
*/
public function setLinks($links)
{
$this->links = new ArrayCollection();
foreach ($links as $footerWidget) {
$this->addLinks($footerWidget);
}
}
/**
* {#inheritdoc}
*/
public function addLinks(\Traffic\WidgetsBundle\Entity\FooterWidgetsHasMedia $links)
{
$links->setFooterWidget($this);
$this->links[] = $links;
}
Now my junction entity will points back to FooterWidgets and sonata media entity
FooterWidgetsHasMedia
Definitions of properties
/**
* #var \Application\Sonata\MediaBundle\Entity\Media
* #Assert\NotBlank()
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media", cascade={"persist"}, fetch="LAZY")
* #ORM\JoinColumn(name="media_id", referencedColumnName="id")
*/
protected $media;
/**
* #var \Application\Sonata\MediaBundle\Entity\Media
* #Assert\NotBlank()
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media", cascade={"persist"}, fetch="LAZY")
* #ORM\JoinColumn(name="media_hover_id", referencedColumnName="id")
*/
protected $mediaHover;
/**
* #var \Traffic\WidgetsBundle\Entity\FooterWidgets
* #Assert\NotBlank()
* #ORM\ManyToOne(targetEntity="Traffic\WidgetsBundle\Entity\FooterWidgets", cascade={"persist","remove"} ,inversedBy="links", fetch="LAZY" )
* #ORM\JoinColumn(name="footer_widget_id", referencedColumnName="id",nullable=true)
*/
protected $footerWidget;
/**
* #var integer
* #ORM\Column(name="position", type="integer")
*/
protected $position;
/**
* #var boolean
* #ORM\Column(name="enable", type="boolean")
*/
protected $enabled;
Generate getters and setters for above properties
Now you have to create new admin for your collection which references to junction entity FooterWidgetsHasMedia and configureFormFields will look something like below
FooterWidgetsHasMediaAdmin
protected function configureFormFields(FormMapper $formMapper)
{
$link_parameters = array();
if ($this->hasParentFieldDescription()) {
$link_parameters = $this->getParentFieldDescription()->getOption('link_parameters', array());
}
if ($this->hasRequest()) {
$context = $this->getRequest()->get('context', null);
if (null !== $context) {
$link_parameters['context'] = $context;
}
}
$formMapper
->add('media', 'sonata_type_model_list', array('required' => false), array(
'link_parameters' => $link_parameters
))
->add('mediaHover', 'sonata_type_model_list', array('required' => false), array(
'link_parameters' => $link_parameters
))
->add('enabled', null, array('required' => false))
->add('link', 'text', array('required' => false))
->add('position', 'hidden')
;
}
And your company admin will have a new field in configureFormFields
FooterWidgetsAdmin
->add('links', 'sonata_type_collection', array(
'cascade_validation' => false,
'type_options' => array('delete' => false),
), array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'link_parameters' => array('context' => 'widgets'),
'admin_code' => 'sonata.admin.footer_widgets_has_media' /*here provide service name for junction admin */
)
)
Register admin service for your new admin as
sonata.admin.footer_widgets_has_media:
class: Traffic\WidgetsBundle\Admin\FooterWidgetsHasMediaAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Widgets", label: "Footer Widgets Section Media" , show_in_dashboard: false }
arguments:
- ~
- Traffic\WidgetsBundle\Entity\FooterWidgetsHasMedia
- ~
calls:
- [ setTranslationDomain, [TrafficWidgetsBundle]]
Demo snap shot
You can find full code demo here Git Hub hope it makes sense
Related
I am currently using doctrine wrong as when I have a choiceType in my entity, I use this:
/**
* #ORM\Column(type="integer")
*/
private $type;
plus in my builder:
->add('type', ChoiceType::class, ['choices' => ['Pattern' => 0, 'Image' => 1]])
I would like now to have something cleaner and have another entity in my database that would be linked to this main entity. Here is what I did.
Created a "category" entity (and created in DB):
/**
* #ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
* #ORM\Table(name="vipbox_mep_category")
*/
class Category {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $category;
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param mixed $category
*/
public function setCategory($category): void
{
$this->category = $category;
}
}
linked my main entity to the second one (with the getter and setter):
/**
* #ORM\OneToOne(targetEntity="App\Entity\Category")
*/
private $category;
Added my category to my form:
->add('category', CategoryType::class, ['required' => true, 'label' => 'Category'])
Extended AbstractType form for my category:
class CategoryType extends AbstractType
{
/**
* #var RouterInterface $route
*/
private $router;
/**
* #param RouterInterface $router
*/
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'required' => true,
'label' => 'Category'
]);
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
}
Populated my Category table with text sample data (2 rows)
Tested to show my form, I does compute, but I have an empty choiceType:
I know I missed one/multiple things on my way, any clues ?
Assuming you have saved some data in your Category table to populate your drop-down list just change your step 3 from this:
->add('category', CategoryType::class, ['required' => true, 'label' => 'Category'])
to this:
->add('category', EntityType::class, [
'class' => Category::class,
'required' => true,
'label' => 'Category',
])
There are lots of other options available for EntityType, see the docs.
I'm using Zend Framework 3 with Doctrine and I'm trying to save an Entity "Cidade" related to another Entity "Estado" witch is already stored in the database. However, Doctrine is trying to persist Entity "Estado", and the only attribute I have from Estado is the primary key in a HTML combo.
My view forms are built under Zend forms and fieldsets, which means POST data is automatically converted to the target entities using ClassMethods hydrator.
The problem is that if I set the attribute $estado with cascade={"persist"} in Cidade Entity, Doctrine tries to persist Estado Entity missing all required attributes but the primary key ID, which comes from POST request (HTML combo). I also considered using cascade={"detach"} ir order to Doctrine ignore Estado Entity in the EntityManager. But I get this error:
A new entity was found through the relationship 'Application\Entity\Cidade#estado' that was not configured to cascade persist operations for entity: Application\Entity\Estado#000000007598ee720000000027904e61.
I found a similar doubt here and the only way I could find for this matter was first retrieving Estado Entity and setting it on Cidade Entity before saving. If this is the only way, can I tell my form structure won't work unless I retrieve all relationships before saving the dependant entities?
In other words, what is the best way of doing such thing in Doctrine (for example):
<?php
/*I'm simulating the creation of Estado Entity representing an
existing Estado in database, so "3" is the ID rendered in HTML combo*/
$estado = new Entity\Estado();
$estado->setId(3);
$cidade = new Entity\Cidade();
$cidade->setNome("City Test");
$cidade->setEstado($estado); //relationship here
$entityManager->persist($cidade);
$entityManager->flush();
How to do that without having to retrieve an Estado all the time I need to save a Cidade? Wouldn't affect performance?
My Cidade Entity:
<?php
namespace Application\Entity;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Cidade
* #package Application\Entity
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"detach"})
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
private $estado;
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true]
]);
}
return $this->inputFilter;
}
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return Estado
*/
public function getEstado()
{
return $this->estado;
}
/**
* #param Estado $estado
*/
public function setEstado($estado)
{
$this->estado = $estado;
}
}
My Estado Entity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
/**
* Class Estado
* #package Application\Entity
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var string
* #ORM\Column(length=3)
*/
private $sigla;
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return string
*/
public function getSigla()
{
return $this->sigla;
}
/**
* #param string $sigla
*/
public function setSigla($sigla)
{
$this->sigla = $sigla;
}
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true],
"sigla" => ["required" => true]
]);
}
return $this->inputFilter;
}
}
Both entities extend my superclass AbstractEntity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
* Class AbstractEntity
* #package Application\Entity
* #MappedSuperClass
*/
abstract class AbstractEntity implements InputFilterAwareInterface
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var InputFilterAwareInterface
*/
protected $inputFilter;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #param InputFilterInterface $inputFilter
* #return InputFilterAwareInterface
* #throws \Exception
*/
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \Exception("Método não utilizado");
}
}
My HTML inputs are rendered as it follows:
<input name="cidade[nome]" class="form-control" value="" type="text">
<select name="cidade[estado][id]" class="form-control">
<option value="3">Bahia</option>
<option value="2">Espírito Santo</option>
<option value="1">Minas Gerais</option>
<option value="9">Pará</option>
</select>
Each option above is an Estado Entity retrieved from database. My POST data comes as the following example:
[
"cidade" => [
"nome" => "Test",
"estado" => [
"id" => 3
]
]
]
On Zend Form's isValid() method, this POST data is automatically converted to the target Entities, which makes me crash on this Doctrine issue. How do I move on?
You should bind an object to your form and use the Doctrine Hydrator. In the form the field names should exactly match that of the Entity. So Entity#name is Form#name.
With Separation of Concerns I'm absolutely against placing the InputFilter for an Entity within the Entity itself. As such, I'll give you an example with everything separated, if you decide to mash it back together, that's up to you.
AbstractEntity for ID
/**
* #ORM\MappedSuperclass
*/
abstract class AbstractEntity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
// getter/setter
}
Cicade Entity
/**
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome; // Changed to 'protected' so can be used in child classes - if any
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"persist", "detach"}) // persist added
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
protected $estado;
// getters/setters
}
Estado Entity
/**
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome;
//getters/setters
}
So, above is the Entity setup for Many to One - Uni-direction relation.
You want to manage this, easily, with forms. So we need to create InputFilters for both.
Having InputFilters separately from the Entity allows us to nest them. This in turn allows us to create structured and nested forms.
For example, you could create a new Estado on-the-fly. If this were a bi-directional relation, you could create multiple Cicade Entity objects on-the-fly from/during the creation of Estado.
First: InputFilters. In the spirit of abstraction, which you started with your Entities, let's do that here as well:
AbstractDoctrineInputFilter
source AbstractDoctrineInputFilter & source AbstractDoctrineFormInputFilter
This gives a nice clean setup and a requirement to fulfill. I'm glossing over the more complex elements added in the source files, feel free to look those up though.
Both objects (Estado & Cicade) require an ObjectManager (they're Doctrine entities after all), so I'm assuming you might have more. The below should come in handy.
<?php
namespace Application\InputFilter;
use Doctrine\Common\Persistence\ObjectManager;
use Zend\InputFilter\InputFilter;
abstract class AbstractInputFilter extends InputFilter
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFormInputFilter constructor.
*
* #param array $options
*/
public function __construct(array $options)
{
// Check if ObjectManager|EntityManager for FormInputFilter is set
if (isset($options['object_manager']) && $options['object_manager'] instanceof ObjectManager) {
$this->setObjectManager($options['object_manager']);
}
}
/**
* Init function
*/
public function init()
{
$this->add(
[
'name' => 'id',
'required' => false, // Not required when adding - should also be in route when editing and bound in controller, so just additional
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]
);
// If CSRF validation has not been added, add it here
if ( ! $this->has('csrf')) {
$this->add(
[
'name' => 'csrf',
'required' => true,
'filters' => [],
'validators' => [
['name' => Csrf::class],
],
]
);
}
}
// getters/setters for ObjectManager
}
Estado InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init();
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
}
}
Cicade InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init(); // Adds the CSRF
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
$this->add(
[
'name' => 'estado',
'required' => true,
]
);
}
}
So. Now we have 2 InputFilters, based on an AbstractInputFilter.
EstadoInputFilter filters just the nome property. Add additional if you want ;)
CicadeInputFilter filters the nome property and has a required estado field.
The names match those of the Entity definition in the respective Entity classes.
Just to be complete, below is the CicadeForm, take what you need to create the EstadoForm.
class CicadeForm extends Form
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFieldset constructor.
*
* #param ObjectManager $objectManager
* #param string $name Lower case short class name
* #param array $options
*/
public function __construct(ObjectManager $objectManager, string $name, array $options = [])
{
parent::__construct($name, $options);
$this->setObjectManager($objectManager);
}
public function init()
{
$this->add(
[
'name' => 'nome',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Nome',
],
]
);
// #link: https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
$this->add(
[
'type' => ObjectSelect::class,
'required' => true,
'name' => 'estado',
'options' => [
'object_manager' => $this->getObjectManager(),
'target_class' => Estado::class,
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Estado'),
'label_attributes' => [
'title' => _('Estado'),
],
'label_generator' => function ($targetEntity) {
/** #var Estado $targetEntity */
return $targetEntity->getNome();
},
],
]
);
//Call parent initializer. Check in parent what it does.
parent::init();
}
/**
* #return ObjectManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager $objectManager
*
* #return AbstractDoctrineFieldset
*/
public function setObjectManager(ObjectManager $objectManager) : AbstractDoctrineFieldset
{
$this->objectManager = $objectManager;
return $this;
}
}
Config
Now that the classes are there, how to use them? Slap 'em together with module config!
In your module.config.php file, add this config:
'form_elements' => [
'factories' => [
CicadeForm::class => CicadeFormFactory::class,
EstadoForm::class => EstadoFormFactory::class,
// If you create separate Fieldset classes, this is where you register those
],
],
'input_filters' => [
'factories' => [
CicadeInputFilter::class => CicadeInputFilterFactory::class,
EstadoInputFilter::class => EstadoInputFilterFactory::class,
// If you register Fieldsets in form_elements, their InputFilter counterparts go here
],
],
From this config we read we need a Factory for both the Form and for the InputFilter of a set.
Below the CicadeInputFilterFactory
class CicadeInputFilterFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeInputFilter
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager|EntityManager $objectManager */
$objectManager = $this->setObjectManager($container->get(EntityManager::class));
return new CicadeInputFilter(
[
'object_manager' => objectManager,
]
);
}
}
Matching CicadeFormFactory
class CicadeFormFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeForm
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) : CicadeForm
{
$inputFilter = $container->get('InputFilterManager')->get(CicadeInputFilter::class);
// Here we creazte a new Form object. We set the InputFilter we created earlier and we set the DoctrineHydrator. This hydrator can work with Doctrine Entities and relations, so long as data is properly formatted when it comes in from front-end.
$form = $container->get(CicadeForm::class);
$form->setInputFilter($inputFilter);
$form->setHydrator(
new DoctrineObject($container->get(EntityManager::class))
);
$form->setObject(new Cicade());
return $form;
}
}
Massive preparation done, time to use it
Specific EditController to Edit an existing Cicade Entity
class EditController extends AbstractActionController // (Zend's AAC)
{
/**
* #var CicadeForm
*/
protected $cicadeForm;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
public function __construct(
ObjectManager $objectManager,
CicadeForm $cicadeForm
) {
$this->setObjectManager($objectManager);
$this->setCicadeForm($cicadeForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
if (is_null($id)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of id received from route
}
/** #var Cicade $entity */
$entity = $this->getObjectManager()->getRepository(Cicade::class)->find($id);
if (is_null($entity)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of not found entity
}
/** #var CicadeForm $form */
$form = $this->getCicadeForm();
$form->bind($entity); // <-- This here is magic. Because we overwrite the object from the Factory with an existing one. This pre-populates the form with value and allows us to modify existing one. Assumes we got an entity above.
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/** #var Cicade $cicade */
$cicade = $form->getObject();
$this->getObjectManager()->persist($cicade);
try {
$this->getObjectManager()->flush();
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
$this->redirect()->toRoute('cicade/view', ['id' => $entity->getId()]);
}
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
/**
* #return CicadeForm
*/
public function getCicadeForm() : CicadeForm
{
return $this->cicadeForm;
}
/**
* #param CicadeForm $cicadeForm
*
* #return EditController
*/
public function setCicadeForm(CicadeForm $cicadeForm) : EditController
{
$this->cicadeForm = $cicadeForm;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return EditController
*/
public function setObjectManager(ObjectManager $objectManager) : EditController
{
$this->objectManager = $objectManager;
return $this;
}
}
So, felt like giving a really expanded answer. Covers the whole thing really.
If you have any questions about the above, let me know ;-)
I have problem with fieldsets in ZF2, I show you my problem.
Here is my form (made by AngularJS, not by ZF2), where you can put a name, and select if you permit which page or which action (Pages are composed of actions).
The picture below partially show what I send to ZF2 :
Here is my data model of my CustomRole class :
class CustomRole
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\CustomRolePagePermission", mappedBy="customRole")
*/
protected $pagePermissions;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\CustomRoleActionPermission", mappedBy="customRole")
*/
protected $actionPermissions;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Users\GlobalRole")
*/
protected $globalRole;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Cie", inversedBy="customRoles")
* #ORM\JoinColumn(name="cie", referencedColumnName="id_cie")
*/
protected $cie;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\User", mappedBy="customRole")
*/
protected $users;
...
Here my class CustomRolePagePermission (which is near the same than CustomRoleActionPermission) :
class CustomRolePagePermission extends PagePermission
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\Entity\Users\CustomRole", inversedBy="pagePermissions")
* #ORM\JoinColumn(name="custom_role_id", referencedColumnName="id", nullable=false)
*/
protected $customRole;
...
And then the abstract class PagePermission :
abstract class PagePermission
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\Entity\Page")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id", nullable=false)
*/
protected $page;
/**
* #ORM\Column(type="boolean")
*/
protected $permission;
...
Now the Fieldset corresponding to CustomRole class (I have made it on each entities) :
class CustomRoleFieldset extends Fieldset implements InputFilterProviderInterface {
protected $serviceLocator;
public function __construct(EntityManager $entityManager) {
parent::__construct('role');
$this->setHydrator(new DoctrineHydrator($entityManager, 'App\Entity\Users\CustomRole'))
->setObject(new CustomRole());
$this->add(array('name' => 'name'));
$customRolePagePermissionFieldset = new CustomRolePagePermissionFieldset($entityManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'pagePermission',
'options' => array(
'target_element' => $customRolePagePermissionFieldset
),
));
$customRoleActionPermissionFieldset = new CustomRoleActionPermissionFieldset($entityManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'actionPermission',
'options' => array(
'target_element' => $customRoleActionPermissionFieldset
),
));
}
public function getInputFilterSpecification() {
return array(
'name' => array('required' => true),
'pagePermission' => array('required' => true),
'actionPermission' => array('required' => true),
);
}
}
...
Here my fieldset CustomRolePagePermissionFieldset :
class CustomRolePagePermissionFieldset extends Fieldset implements InputFilterProviderInterface {
protected $serviceManager;
public function __construct(EntityManager $entityManager) {
parent::__construct();
$this->setHydrator(new DoctrineHydrator($entityManager, 'App\Entity\Users\CustomRolePagePermission'))
->setObject(new CustomRolePagePermission());
$this->add(array('name' => 'permission'));
}
...
And then, my controller :
...
$customRoleForm = new CustomRoleForm($em);
$customRole = new CustomRole();
$formData = $request->getPost();
$customRoleForm->bind($customRole);
$customRoleForm->setData($formData);
if ($customRoleForm->isValid()) {
$customRole->setCie($cie);
$customRole->setGlobalRole($globalRole);
$em->persist($customRole);
$em->flush();
return $this->ok($customRole->getId());
}
...
Problem
When I send the form, the CustomRole is created, but pages and actions checked previously are not linked with the CustomRole created, as if I had never checked any checkbox.
I don't understand why it doesn't have effects, do you have any ideas ?
Thanks a lot in advance ! :)
Your collection should be "pagePermissions" for the hydrator to call setPagePermissions.
I've a class that represent a product with a property mapped as simple array that represents a sizes collection
<?php
namespace Middleware\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Product
*
* #ORM\Table(name="product", uniqueConstraints={...})
* #ORM\Entity(repositoryClass="...")
*/
class Product {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// more properties
// ...
/**
* #ORM\Column(type="simple_array", nullable=true)
*/
private $sizes;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set sizes
*
* #param array $sizes
* #return Product
*/
public function setSizes($sizes)
{
$this->sizes = $sizes;
return $this;
}
/**
* Get sizes
*
* #return array
*/
public function getSizes()
{
return $this->sizes;
}
}
My ProducAdmin class is like follows
<?php
namespace Middleware\MainBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class ProductAdmin extends Admin
{
/**
* #param DatagridMapper $datagridMapper
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('sizes') // I get a error ContextErrorException: Notice: Array to string conversion
;
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add('sizes') // I get a error ContextErrorException: Notice: Array to string conversion
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$sizes = array();
for ($index = 35; $index <= 48; $index++) {
$sizes[$index] = $index . "";
}
$formMapper
->add('sizes', 'choice', array('required' => false, 'expanded' => true, 'multiple' => true, 'choices' => $sizes)) // works fine
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('sizes') // I get a error ContextErrorException: Notice: Array to string conversion
;
}
}
How could I manage a field like sizes with Sonata Admin Bundle? Thanks very much
Although the result is not beautiful. I've solved the problem just adding 'array' type to 'sizes' field on configureShowFields and configureListFields methods. The result shows sizes field like [0 => 38] [1 => 40] [2 => 42] [3 => 43] [4 => 45] [5 => 46] and I would like it as similar to 38, 40, 42, 43, 45, 46
The change I've made:
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add('sizes', 'array') // I've just added array type
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('sizes', 'array') // I've just added array type
;
}
I would appreciate a better way. Thanks
Types availables on Sonata Admin Bundle Forms
You have to specifie that sizes is a collection , add 'sizes', 'collection'. This is working line SF2 form. Types can be found here, you'll found collections' attributes too:
http://symfony.com/fr/doc/current/reference/forms/types.html
The 'collection' tricks is working well for simple entity.
Exemple for my Tag entity :
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('name')
...
->add('tags', ' collection')
...
}
Then you need a toString method in your entity.
function __toString() {
return $this->getName();
}
Easy billy :)
I have a category table and product table. The categories are listed in product form in a drop down select box. When i edit a product then category saved in product table should be auto selected in category drop down at product edit form (selected="selected").
I have already tried empty_data and empty_value but they does not work.
I am searching for the solution since 2 days but could not find any tutorial or such example. I will be really thankful if someone can refer me a tutorial or can see below code to sort out the problem:
Controller
<?php
// src/Scsp/CmsBundle/Controller/ProductController.php
namespace Scsp\CmsBundle\Controller;
use Scsp\CmsBundle\Entity\CategoryEntity;
use Scsp\CmsBundle\Entity\ProductEntity;
use Scsp\CmsBundle\Form\ProductForm;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\HttpFoundation\Session\Session;
class ProductController extends Controller {
public function editAction($id, $request) {
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('ScspCmsBundle:ProductEntity')->find($id);
if (!$product){
throw $this->createNotFoundException('No product found for id ' . $id);
}
$productForm=new ProductForm($id);
$form = $this->createForm($productForm, $product, array(
'attr' => array('class' => 'form-horizontal')
));
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isValid()) {
$product->setCategoryid(3);
$em->persist($product);
$em->flush();
$this->session->getFlashBag()->add('success', $product->getProducts() . ' successfully edited!');
return $this->redirect($this->generateUrl('scsp_cms_products', array('action' => 'list', 'id' => 0)));
}
return $this->render('ScspCmsBundle:Default:form.html.twig', array(
'form' => $form->createView(),
'page_title' => 'Edit product',
'type' => 'products',
'id' => $id
));
}
}
Product Entity:
<?php
// src/Scsp/CmsBundle/Entity/ProductEntity.php
namespace Scsp\CmsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="Scsp\CmsBundle\Repository\ProductRepository")
* #ORM\Table(name="products")
*/
class ProductEntity {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $products;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $categoryid;
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set id
*
* #param string $id
* #return Id
*/
public function setId($id) {
$this->id = $id;
return $this;
}
/**
* Set products
*
* #param string $products
* #return ProductEntity
*/
public function setProducts($products)
{
$this->products = $products;
return $this;
}
/**
* Get products
*
* #return string
*/
public function getProducts()
{
return $this->products;
}
/**
* Set categoryid
*
* #param integer $categoryid
* #return ProductEntity
*/
public function setCategoryid($categoryid)
{
$this->categoryid = $categoryid;
return $this;
}
/**
* Get categoryid
*
* #return integer
*/
public function getCategoryid()
{
return $this->categoryid;
}
}
Category Entity:
<?php
// src/Scsp/CmsBundle/Entity/CategoryEntity.php
namespace Scsp\CmsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="Scsp\CmsBundle\Repository\CategoryRepository")
* #ORM\Table(name="categories")
*/
class CategoryEntity {
/**
* #ORM\OneToMany(targetEntity="ProductEntity", mappedBy="categorid")
*/
protected $products;
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $categories;
public function __construct(){
$this->products = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set id
*
* #param string $id
* #return Id
*/
public function setId($id) {
$this->id = $id;
return $this;
}
/**
* Set categories
*
* #param string $categories
* #return Categories
*/
public function setCategories($categories) {
$this->categories = $categories;
return $this;
}
/**
* Get categories
*
* #return string
*/
public function getCategories() {
return $this->categories;
}
public function __toString() {
return $this->getCategories();
}
/**
* Add products
*
* #param \Scsp\CmsBundle\Entity\ProductEntity $products
* #return CategoryEntity
*/
public function addProduct(\Scsp\CmsBundle\Entity\ProductEntity $products)
{
$this->products[] = $products;
return $this;
}
/**
* Remove products
*
* #param \Scsp\CmsBundle\Entity\ProductEntity $products
*/
public function removeProduct(\Scsp\CmsBundle\Entity\ProductEntity $products)
{
$this->products->removeElement($products);
}
/**
* Get products
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getProducts()
{
return $this->products;
}
}
Product Form:
<?php
// src/Scsp/CmsBundle/Form/ProductForm.php
namespace Scsp\CmsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use \Scsp\CmsBundle\Entity\ProductEntity;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
class ProductForm extends AbstractType
{
private $id;
public function __construct($id) {
$this->id=$id;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($this->id>0){
$builder->add('id', 'text',
array(
'label' => false,
'attr' => array(
'class' => 'form-control',
'readonly' => 'readonly'
)
)
);
}
$builder
->add('products', 'text',
array(
'label' => false,
'max_length' => 100,
'attr' => array(
'class' => 'form-control',
'autocomplete' => 'off'
)
)
)
->add('categoryid', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data'=>$builder->getData()->getCategoryId(),
'attr' => array(
)
)
)
->add('save', 'submit',array(
'attr' => array(
'formnovalidate' => 'formnovalidate',
'class' => 'btn btn-primary',
'label' => 'Save Changes'
)
)
)
->getForm();
}
public function getName()
{
return 'products';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => 'Scsp\CmsBundle\Entity\CategoryEntity'
));
}
}
categoryid
is not an entity, so you must to add a property called category in ProductEntity and stablish the relation ManyToOne there, the code:
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $categoryid;
must be replaced by:
/**
* #ORM\Column(name="categoryid", type="integer")
*/
protected $categoryid;
/**
* #var CategoryEntity
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $category;
//Here generate the get and the set customized
/**
* #return CategoryEntity
*/
public function getCategory()
{
return $this->category;
}
/**
* #param CategoryEntity $category
* #return $this
*/
public function setCategory($category)
{
//this is the owning side
//all changes will be detected
//in this side by doctrine
$category->addProduct($this);
$this->category= $category;
//The line below is important to set the categoryid
//field in the product table
$this->setCategoryid($category->getId());
return $this;
}
Now in the Product Form replace:
->add('categoryid', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data'=>$builder->getData()->getCategoryId(),
'attr' => array(
)
)
)
by:
->add('category', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'attr' => array(
)
)
)
That's all, enjoy it!
I see many things wrong in your form type, nonetheless, what are you looking for is the data option in the field definition:
->add('categoryid', 'entity',
array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data' => $builder->getData()->getCategoryId()
)
)
This data field must contain the select value property, which is the categoryId, at least when using an entity field type as normal. I don't know if this will work with you current setup!
I have a solution for Symfony 1.4.18.
The key is use jQuery.
You can view a detail explanation (including images) in my blog (in Spanish) here: http://symfonyandme.com/2013/10/09/como-usar-select-dependientes-en-symfony-1-4-con-tres-tablas/
And the same solution here: Symfony 1.4 with AJAX jQuery. How can I improve the AJAX's function that contains a select box which depends on another select box?
Then you would work for adjust this example for Symfony 2