I have a form where I can fill in my euros, my entity only knows cents and is a integer.
So I want to create (not sure if i'm using the right method) form transformer.
What I do:
class EuroTransformer implements DataTransformerInterface
{
public function transform($euro)
{
return $euro * 100;
}
public function reverseTransform($euro)
{
return $euro / 100;
}
}
form:
->add('price', 'money', array(
'attr' => array(
'style' => 'width: 70px;'
)
))
->addModelTransformer($euroTransformer)
But i'm getting the next message:
The form's view data is expected to be an instance of class Entity\InvoiceRule, but is a(n) integer. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) integer to an instance of Entity\InvoiceRule.
And yes I have already a data_class in my default options.
How to solve my problem?
using symfony2 2.2
Sf2 MoneyType handles this case !
->add('price', 'money', array(
'divisor' => 100,
'attr' => array(
'style' => 'width: 70px;'
)
))
You need to return an object in your reverseTransform method:
/**
* #param int $cents
*
* #return InvoiceRule
*/
public function reverseTransform($cents)
{
$euro = new InvoiceRule();
$euro->setValue($cents / 100);
return $euro;
}
And your transform method must transform an object into a number:
/**
* #param InvoiceRule $euro
*
* #return int
*/
public function transform($euro)
{
return $euro->getValue() * 100;
}
See the documentation for more examples.
Related
I have a ManyToMany relation in my Symfony 4.2.6 application and I would like for it to be possible to have this to be null.
So my first entity SpecialOffers is as follows :
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SpecialOfferRepository")
*/
class SpecialOffer
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Neighbourhood", inversedBy="specialOffers")
*/
private $neighbourhood;
public function __construct()
{
$this->neighbourhood = new ArrayCollection();
}
/**
* #return Collection|Neighbourhood[]
*/
public function getNeighbourhood(): Collection
{
return $this->neighbourhood;
}
public function addNeighbourhood(Neighbourhood $neighbourhood): self
{
if (!$this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood[] = $neighbourhood;
}
return $this;
}
public function removeNeighbourhood(Neighbourhood $neighbourhood): self
{
if ($this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood->removeElement($neighbourhood);
}
return $this;
}
}
It is related to the neighbourhood class :
/**
* #ORM\Entity(repositoryClass="App\Repository\NeighbourhoodRepository")
*/
class Neighbourhood implements ResourceInterface
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\SpecialOffer", mappedBy="neighbourhood")
*/
private $specialOffers;
public function __construct()
{
$this->specialOffers = new ArrayCollection();
}
/**
* #return Collection|SpecialOffer[]
*/
public function getSpecialOffers(): Collection
{
return $this->specialOffers;
}
public function addSpecialOffer(SpecialOffer $specialOffer): self
{
if (!$this->specialOffers->contains($specialOffer)) {
$this->specialOffers[] = $specialOffer;
$specialOffer->addNeighbourhood($this);
}
return $this;
}
public function removeSpecialOffer(SpecialOffer $specialOffer): self
{
if ($this->specialOffers->contains($specialOffer)) {
$this->specialOffers->removeElement($specialOffer);
$specialOffer->removeNeighbourhood($this);
}
return $this;
}
}
And finally the form is
class SpecialOfferType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'placeholder' => 'form.neighbourhood.all'
]
);
}
}
But when I don't select a specific neighbourhood for the Special offer in my form I get the following error :
Could not determine access type for property "neighbourhood" in class "App\Entity\SpecialOffer": The property "neighbourhood" in class "App\Entity\SpecialOffer" can be defined with the methods "addNeighbourhood()", "removeNeighbourhood()" but the new value must be an array or an instance of \Traversable, "NULL" given.
Is there anyway I can make it so that my special offer either contains and array of neighbourhoods or just null ?
I feel like I'm overlooking something really obvious, any help would be greatly appreciated
Test =>
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'multiple' => true,
'placeholder' => 'form.neighbourhood.all'
]
);
Since your fields on the entities are both many-to-many, thus expecting an array (or similar) and the form field is of EntityType, which will return one Entity of the expected type or null, I feel like there is some form of asymmetry.
I would consider using the CollectionType from the start or at least setting the multiple option on the form to true, so that the return value is an array.
Another option would be to add a DataTransformer to the form field, which turns null into an empty array and one entity into an array of one entity, and vice-versa.
I am going to create a form with one column ID Verification.
There are several options based on the system, same as Enum.
The Target page should be :
Here I have the property in Entity as :
/**
* #var array<string>
*
* #ORM\Column(name="id_verification_requirements", type="simple_array", nullable=true)
*/
private $idVerificationRequirements;
I am not sure how to build the form element and store the related data.
Do I need to save the specific data in database as constants table?
It seems that PHP does not support Enum.
At the same time I have to handle a frequency type in the form as well.
I have got some suggestions to create a new frequency type to handle the data like:
Frequency:
class Frequency{
private $count;
// $unit must be defined in FrequencyUnit
private $unit;
function __construct($c, $u)
{
$this->count = $c;
$this->unit = $u;
}}
Then FrequencyUnit:
abstract class FrequencyUnit
{
const PER_DAY = "PER DAY";
const PER_WEEK = "PER WEEK";
const PER_MONTH = "PER_MONTH";
const PER_YEAR = "PER_YEAR";
}
How to generate this in the form like a drop-down list or there is other better way to deal with this kind of thing?
One more question is that is there any way to display only a label in the form, just like sub-title?
Thank you very much for you help.
++++++++++++++++++++++++++++++++++++++++++++++++++
Currently I have build the form type as below :
->add('frequencyCount', IntegerType::class,array('label' => 'Max Load Frequency'))
->add('frequencyUnit', ChoiceType::class,array(
'choices' => array(
'Per Day' => 'Per Day',
'Per Week' => 'Per Week',
'Per Month' => 'Per Month',
'Per Year' => 'Per Year'
),
'expanded' => false, 'multiple' => false
))
->add('idVerificationRequirements', ChoiceType::class, array(
'label' => 'ID Verification Requirements',
'choices' => array(
'Front' => 1,
'Back' => 2,
'Both' => 3
),
'expanded' => true, 'multiple' => true
)
)
For Frequency I have add below get and set function to handle the form data.
public function setFrequencyCount($count)
{
$this->loadFreq->setCount($count);
return $this;
}
public function getFrequencyCount()
{
return $this->loadFreq->getCount();
}
public function setFrequencyUnit($unit)
{
$this->loadFreq->setUnit($unit);
return $this;
}
public function getFrequencyUnit()
{
return $this->loadFreq->getUnit();
}
It seems work.
I am not sure if this should be the solution or maybe some better way.
For now, I just wonder if there is some way to manage the choices with ChoiceType. I don't want to list as array when building the form. Could I get the data from other class?
Thanks a lot.
I suggest creating a FrequencyUnit Entity for example like this:
<?php
// src/AppBundle/Entity/Unit.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="unit")
*/
class Unit
{
/**
* #ORM\Id
* #ORM\Column(name="unit_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $unit_id;
/**
* #ORM\Column(name="value", type="string")
*/
protected $value;
/**
* Set value
*
* #param string $val
*
* #return Unit
*/
public function setValue($val)
{
$this->value = $val;
return $this;
}
/**
* Get value
*
* #return string
*/
public function getValue()
{
return $this->value;
}
}
Then in your form use EntityType like so:
...
->add('unit', EntityType::class, array(
'class' => 'AppBundle:Unit',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u');
},
'choice_label' => 'value',
))
...
You would need to still add the values to the Entity. Maybe in your Controller?
$unit = new Unit();
$unit->setValue('PER DAY');
...
Hopefully that helps. It's hard to give you a good thorough answer, as because there are many ways to do this.
I have started looking into Magento 2 grid. I have developed one simple module but I didn't understand the structure of grid.
In Magento 1.9.X, the way was clear for adding grid but in Magento 2 there is structure is different. How do I add a grid in Magento 2?
In Magento 2, you can create a grid by XML (see here)
However, you can create a grid by PHP like Magento 1: Extending your grid class to "Magento\Backend\Block\Widget\Grid\Extended"
<?php
namespace Yourpackage\Yourmodule\Block\Adminhtml\Sample;
class Grid extends \Magento\Backend\Block\Widget\Grid\Extended
{
protected $_yourmodelFactory;
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Backend\Helper\Data $backendHelper,
\Yourpackage\Yourmodule\Model\YourmodelFactory $yourmodelFactory,
array $data = []
) {
parent::__construct($context, $backendHelper, $data);
$this->_yourmodelFactory = $yourmodelFactory;
}
protected function _construct()
{
parent::_construct();
$this->setId('sample_grid');
$this->setDefaultSort('id');
$this->setDefaultDir('DESC');
$this->setSaveParametersInSession(true);
}
protected function _prepareCollection()
{
$collection = $this->_yourmodelFactory->create()->getCollection();
$this->setCollection($collection);
return parent::_prepareCollection();
}
protected function _prepareColumns()
{
$this->addColumn(
'id',
[
'header' => __('ID'),
'align' => 'right',
'width' => '50px',
'index' => 'id',
]
);
// Some columns
return parent::_prepareColumns();
}
}
You can see more at: /vendor/magento/module-cms/Block/Adminhtml/Page/Grid.php.
1:Create controller Index.php
<?php
namespace Ced\Abhinay\Controller\Adminhtml\Account;
class Index extends \Magento\Backend\App\Action {
/**
* #var bool|\Magento\Framework\View\Result\PageFactory
*/
protected $resultPageFactory = false;
/**
* Index constructor.
* #param \Magento\Backend\App\Action\Context $context
* #param \Magento\Framework\View\Result\PageFactory $resultPageFactory
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory
)
{
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->prepend((__('Ced Abhinay')));
return $resultPage;
}
}
2:After that create layout file for this
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin- 2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Ced\Abhinay\Block\Adminhtml\Account\ListGrid" name="ced_custom_grid"/>
</referenceContainer>
</body>
</page>
3.After that create file ListGrid.php
<?php
namespace Ced\Abhinay\Block\Adminhtml\Account;
class ListGrid extends \Magento\Backend\Block\Widget\Grid\Container {
/**
* Class ListGrid extends parent constructor \Magento\Backend\Block \Widget\Grid
*/
protected function _construct()
{
$this->_controller = 'account_index';
$this->_blockGroup = 'Ced_Abhinay';
$this->_addButtonLabel = __('Ced Test');
parent::_construct();
}
}
4:Now finally create your Grid.php
<?php
namespace Ced\Abhinay\Block\Adminhtml\Account\Grid;
class Grid extends \Magento\Backend\Block\Widget\Grid\Extended {
/** #var \Ced\Abhinay\Model\ListModel */
protected $listModelData;
/**
* Grid constructor.
* #param \Magento\Backend\Block\Template\Context $context
* #param \Magento\Backend\Helper\Data $backendHelper
* #param \Ced\Abhinay\Model\ListModel $listModelData
* #param array $data
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Backend\Helper\Data $backendHelper,
\Ced\Abhinay\Model\ListModel $listModelData,
array $data = []
) {
parent::__construct($context, $backendHelper, $data);
$this->listModelData = $listModelData;
}
protected function _construct()
{
parent::_construct();
$this->setId('list_grid');
$this->setDefaultSort('list_id');
$this->setDefaultDir('DESC');
$this->isAjax('true');
}
protected function _prepareCollection()
{
$collection = $this->listModelData->getCollection();
$this->setCollection($collection);
return parent::_prepareCollection();
}
protected function _prepareColumns()
{
$this->addColumn(
'post_id',
[
'header' => __('ID'),
'sortable' => true,
'index' => 'post_id',
'type' => 'number',
'header_css_class' => 'col-id',
'column_css_class' => 'col-id'
]
);
$this->addColumn(
'title',
[
'header' => __('Name'),
'index' => 'name',
'header_css_class' => 'col-name',
'column_css_class' => 'col-name'
]
);
$this->addColumn(
'position',
[
'header' => __('Position'),
'name' => 'position',
'width' => 60,
'type' => 'number',
'validate_class' => 'validate-number',
'index' => 'position',
'editable' => true,
]
);
return parent::_prepareColumns();
}
}
The best practice is to create all grids via UI components (xml).
Look into Magento_Catalog module and find product_form.xml.
Now the preferred way of adding grid inside adminhtml is with ui components
Reason why it's the best way now is because you can use a lot of magento 2 backend functionality when adding it as ui component.
However there are multiple ways to add this.
Not to repeat the answer and tons of code in stackoverflow i found a mageplaza explination, that explains the creation of the grid.
https://www.mageplaza.com/magento-2-module-development/create-admin-grid-magento-2.html
You can also refer to magento 2 documentation to take a look on additional components you can use in you ui component:
https://devdocs.magento.com/guides/v2.0/ui-components/ui-component.html
There are multiple existing components you can you in grid and you can create your own. Aldo complex they do offer big amount of flexibility when setuped. And when you do create a couple you will understand how they function and will be able to work with them with ease.
I have an entity named HoursSpecial with a foreign key relationship to an entity called HoursArea. Each HoursSpecial belongs to an HoursArea. When I create a new HoursSpecial via my HoursSpecialType, I want the form field to automatically populate the HoursArea field.
I know what you're thinking, just do something like this in my controller's method:
$form->add('area', 'hidden', array('data'=>$area));
That would be fine except I need to make a DataTransformer to switch between the area's ID and the actual area entity. So I have to declare my HoursArea field within my HoursSpecialType with the transformer:
$builder
...
->add('area', 'hidden')
;
$builder->get('area')->addModelTransformer(new HoursAreaToIntTransformer($this->manager));
Now, I can't simply feed my HoursArea entity into the form. Is there an effective way to make this happen?
I've thumbed through Symfony's documentation on How to Dynamically Modify Forms Using Form Events, but I can't make heads or tails of how I would pass in that HoursArea entity dynamically from outside of the form builder. Maybe I'm just missing something?
UPDATE
Following the recommendation of the answer (Recommendation #1) below from #Ryan, I have created the custom type HiddenHoursAreaType:
// AppBundle\Form\Type\HideenHoursAreaType.php
class HiddenHoursAreaType extends AbstractType
{
//need to instantiate HoursAreaToIntTransformer
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new HoursAreaToIntTransformer($this->manager);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'compound' => true //this should be FALSE as there are no children
));
}
/**
*
* the return value of the getParent function indicates that you're extending the choice field type.
* This means that, by default, you inherit all of the logic and rendering of that field type.
*/
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'app_hoursArea';
}
I have added my transformer into the custom type class. Here is the transformer class:
// AppBundle\Form\DataTransformer;
class HoursAreaToIntTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (HoursArea) to a string (number).
*
* #param Issue|null $issue
* #return string
*/
public function transform($area)
{
if (null === $area) {
return '';
}
return $area->getId();
}
/**
* Transforms a string (number) to an object (HoursArea).
*
* #param string $areaId
* #return HoursArea|null
* #throws TransformationFailedException if object (HoursArea) is not found.
*/
public function reverseTransform($areaId)
{
// no area number? It's optional, so that's ok
if (!$areaId) {
return;
}
$area = $this->manager
->getRepository('AppBundle:HoursArea')
// query for the issue with this id
->find($areaId)
;
if (null === $area) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An area with number "%s" does not exist!',
$areaId
));
}
return $area;
}
}
Within my controller I create the form with the custom type field:
$form = $this->createForm(new HoursSpecialType($this->getDoctrine()->getManager()), $entity, array(
'action' => $this->generateUrl('hoursspecial_postcreate'),
'method' => 'POST',
));
$form->add('eventDate', 'hidden', array('data'=>$dateString));
$form->add('area', new \AppBundle\Form\Type\HiddenHoursAreaType($this->getDoctrine()->getManager()), array(
'data'=>$area,
'invalid_message'=>'Area field not converted proerly'
));
$form->add('submit', 'submit', array('label' => 'Create'));
Thanks to the transformer and the custom type, the form now correctly converts the HoursArea entity to an integer for population in the hidden field.
The problem now is that upon form submission, the integer is not converted back into an HoursArea object. I know this because I get the 'invalid_message' upon submission.
Final Update
The reason the HoursArea id wasn't being inserted properly had something to do with the
'compound' => true
setting I had in my custom type. I assume it was looking for child fields and wasn't finding any...which it shouldn't have because there were none!
You could create a custom type for it and add the addModelTransformer() call in the buildForm() of your custom type, but still pass the data in explicitly. So your $form->add('area', 'hidden', array('data'=>$area)) would become $form->add('area', new HiddenHoursAreaType(), array('data'=>$area)) where HiddenHoursAreaType::getParent() would be the hidden type.
You could set the data in a POST_SET_DATA listener.
You could get the $options['data'] value in buildForm() and explicitly pass in the HoursArea ID.
/** #var HoursSpecial $hoursSpecial Prepopulated in controller */
$hoursSpecial = $options['data']
$builder->add('area', 'hidden', ['data' => $hoursSpecial->getHoursArea()->getId()])
I'm trying to set a form type "sonata_type_immutable_array" as follows:
->add('metadatos', 'sonata_type_immutable_array', array(
'keys' => array(
array('Test', 'text', array('required' => false)),
array('URL', 'url', array('required' => false)),
)
))
And saving like this:
public function setMetadatos(\Portal\EntradasBundle\Entity\EntradaMeta $metadatos = null)
{
$this->metadatos = $metadatos;
return $this;
}
But always get the error:
Catchable Fatal Error: Argument 1 passed to Portal\EntradasBundle\Entity\Entrada::setMetadatos() must be an instance of Portal\EntradasBundle\Entity\EntradaMeta, array given
I dont know how to set a datatransformer (ArrayToModelTransformer) to reach this.
Anyone can help me plz. Thanks in advance!
A data transformer is quite simple,
Look at this:
http://symfony.com/doc/current/cookbook/form/data_transformers.html
A data transformer is used like this:
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct($om)
{
$this->om = $om;
}
[..]
$yourTransformer = new YourDataTransformer($this->om);
And then ->addModelTransformer($yourTransformer))
It's used to get the id of an object , and/or get the object from an id.