Magento 2 grid structure - grid

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.

Related

Zf3 populate select Element with data from Database

I know this topic was discussed 2 years ago. But I stuck on difficulty that I would like to solve.
I would like to create a zf3 form which contains more than two Select Element.
I would like to populate them with data coming from different repositories (options values of each Select item come from distinct repository).
First all, I tried to pass the service manager (from where I can access to my repositories) in the constructor of my form but I heard that this solution is not suitable at all.
So how can I include multiples repositories in my form to populate my Select Elements?
Short answer:
Create a class that extends the Select
Create a factory for such class
Add this custom element in your module configuration (module.config.php)
Use this class as type for your form elements
Retrieve the form through the form manager
Example, for controllers, adapt the controller's factory
Detailed answer:
Create a class that extends the Select, like BrandSelect
namespace MyModule\Form\Element;
use Laminas\Form\Element\Select;
class BrandSelect extends Select {
protected $repository;
public function __construct($repository, $name = null, $options = []) {
parent::__construct($name, $options);
$this->repository = $repository;
}
/**
* Initialize the element
*
* #return void
*/
public function init() {
$valueOptions = [];
foreach ($this->repository->fetchBrands() as $brand) {
$valueOptions[$brand->getBrandId()] = $brand->getName();
}
asort($valueOptions);
$this->setValueOptions($valueOptions);
}
}
Create a factory for such class
namespace MyModule\Form\Element;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
use MyModule\Db\Repository;
class BrandSelectFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, $options = null): BrandSelect {
$repository = $container->get(Repository::class);
return new BrandSelect($repository);
}
}
Add this custom element in your module configuration (module.config.php)
namespace MyModule;
return [
// ..
// Other configs
// ..
'form_elements' => [
'factories' => [
Form\Element\BrandSelect::class => Form\Element\BrandSelectFactory::class
]
]
];
Use this class as type for your form elements.
It is really important to add all elements in the init() method, otherwise it will not work. I also added the InputFilterProviderInterface.
In this case, form doens't require any other element its constructor. If needed, you must create a factory for the form and pass all params you need. The form factory must be added in module.config.php configuration, always under the form_elements key (as did for the BrandSelect):
namespace MyModule\Form;
use Laminas\Form\Form;
use Laminas\InputFilter\InputFilterProviderInterface;
class BrandForm extends Form implements InputFilterProviderInterface {
public function __construct($name = null, $options = []) {
parent::__construct($name, $options);
}
// IT IS REALLY IMPORTANT TO ADD ELEMENTS IN INIT METHOD!
public function init() {
parent::init();
$this->add([
'name' => 'brand_id',
'type' => Element\BrandSelect::class,
'options' => [
'label' => 'Brands',
]
]);
}
public function getInputFilterSpecification() {
$inputFilter[] = [
'name' => 'brand_id',
'required' => true,
'filters' => [
['name' => 'Int']
]
];
return $inputFilter;
}
}
Retrieve the form through the form manager
Form must be retrieved using correct manager, which isn't the service manager, but the FormElementManager.
For example, if you need the form inside BrandController:
<?php
namespace MyModule\Controller;
use Laminas\Form\FormElementManager;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
class BrandController extends AbstractActionController {
private $formManager;
public function __construct(FormElementManager $formManager) {
$this->formManager = $formManager;
}
public function addBrandAction() {
$form = $this->formManager->get(\MyModule\Form\BrandForm::class);
// Do stuff
return new ViewModel([
'form' => $form
]);
}
}
Finally, you'll have to adapt the controller's factory:
namespace MyModule\Controller;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
class BrandControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, $options = null): BrandController {
$formManager = $container->get('FormElementManager');
return new BrandController($formManager);
}
}
which must be configured under controllers key in module.config.php:
namespace MyModule;
return [
// ..
// Other configs
// ..
'controllers' => [
'factories' => [
Controller\BrandController::class => Controller\BrandControllerFactory::class
],
],
'form_elements' => [
'factories' => [
Form\Element\BrandSelect::class => Form\Element\BrandSelectFactory::class
]
]
];

ManyToMany new value must be an array or an instance of \Traversable, "NULL" given

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.

easyadmin entity field's dynamic custom choices

Installed easyadminbundle with symfony 4, configured for an entity name Delivery and it has a field associated to another entity name WeeklyMenu:
easy_amin.yaml:
Delivery:
...
form:
fields:
- { property: 'delivered'}
- { property: 'weeklyMenu', type: 'choice', type_options: { choices: null }}
I need a dynamically filtered results of weeklyMenu entity here, so I can get a list of the next days menus and so on. It's set to null now but have to get a filtered result here.
I've read about overriding the AdminController which I stucked with it. I believe that I have to override easyadmin's query builder that listing an associated entity's result.
i've figured out, here is the solution if someone looking for:
namespace App\Controller;
use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilder;
class AdminController extends EasyAdminController {
public function createDeliveryEntityFormBuilder($entity, $view) {
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$fields = $formBuilder->all();
/**
* #var $fieldId string
* #var $field FormBuilder
*/
foreach ($fields as $fieldId => $field) {
if ($fieldId == 'weeklyMenu') {
$options = [
'attr' => ['size' => 1,],
'required' => true,
'multiple' => false,
'expanded' => false,
'class' => 'App\Entity\WeeklyMenu',
];
$options['query_builder'] = function (EntityRepository $er) {
$qb = $er->createQueryBuilder('e');
return $qb->where($qb->expr()->gt('e.date', ':today'))
->setParameter('today', new \DateTime("today"))
->andWhere($qb->expr()->eq('e.delivery', ':true'))
->setParameter('true', 1)
->orderBy('e.date', 'DESC');
};
$formBuilder->add($fieldId, EntityType::class, $options);
}
}
return $formBuilder;
}
}
so the easyAdmin check if a formbuilder exists with the entity's name i.e. create<ENTITYNAME>FormBuilder(); and you can override here with your own logic.
Another approach to this would be to create new FormTypeConfigurator and overwrite choices and/or labels. And tag it as:
App\Form\Type\Configurator\UserTypeConfigurator:
tags: ['easyadmin.form.type.configurator']
and the configurator looks like this:
<?php
declare(strict_types = 1);
namespace App\Form\Type\Configurator;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TypeConfiguratorInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormConfigInterface;
final class UserTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {#inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
{
if ($parentConfig->getData() instanceof User) {
$options['choices'] = User::getUserStatusAvailableChoices();
}
return $options;
}
/**
* {#inheritdoc}
*/
public function supports($type, array $options, array $metadata)
{
return in_array($type, ['choice', ChoiceType::class], true);
}
}

Change Link Name on Generated Link Field

I'm creating a custom theme in Drupal 8 and on the node.html.twig template I'm using {{content.field_type}} to display the link of the category which the current page is in to allow the user to link back to the list of pages in that category. Using this, the page renders:
Sub Cat Name
What do I need to do change this to:
My Custom Link
It's possible to change render arrays using preprocess functions, but in your case it's not a good idea. Link you are talking about is a result of rendering of field formatter. So, you just need another field formatter for your 'Type' field, instead of current 'Label' formatter.
Creating new formatter is quite easy(especially if you use EntityReferenceLabelFormatter as an example). Suppose you have a module called entity_reference_link_formatter. Then in the directory of this module create src/Plugin/Field/FieldFormatter folder and put there the following EntityReferenceLinkFormatter.php file:
<?php
/**
* #file
* Contains Drupal\entity_reference_link_formatter\Plugin\Field\FieldFormatter\EntityReferenceLinkFormatter
*/
namespace Drupal\entity_reference_link_formatter\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'entity reference link' formatter.
*
* #FieldFormatter(
* id = "entity_reference_link",
* label = #Translation("Link"),
* description = #Translation("Display the link to the referenced entity."),
* field_types = {
* "entity_reference"
* }
* )
*/
class EntityReferenceLinkFormatter extends EntityReferenceFormatterBase {
/**
* {#inheritdoc}
*/
public static function defaultSettings() {
return [
'text' => 'View',
] + parent::defaultSettings();
}
/**
* {#inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements['text'] = [
'#title' => t('Text of the link to the referenced entity'),
'#type' => 'textfield',
'#required' => true,
'#default_value' => $this->getSetting('text'),
];
return $elements;
}
/**
* {#inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = t('Link text: #text', ['#text' => $this->getSetting('text')]);
return $summary;
}
/**
* {#inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
if (!$entity->isNew()) {
try {
$uri = $entity->urlInfo();
$elements[$delta] = [
'#type' => 'link',
'#title' => t('!text', ['!text' => $this->getSetting('text')]),
'#url' => $uri,
'#options' => $uri->getOptions(),
];
if (!empty($items[$delta]->_attributes)) {
$elements[$delta]['#options'] += array('attributes' => array());
$elements[$delta]['#options']['attributes'] += $items[$delta]->_attributes;
// Unset field item attributes since they have been included in the
// formatter output and shouldn't be rendered in the field template.
unset($items[$delta]->_attributes);
}
}
catch (UndefinedLinkTemplateException $e) {
// This exception is thrown by \Drupal\Core\Entity\Entity::urlInfo()
// and it means that the entity type doesn't have a link template nor
// a valid "uri_callback", so don't bother trying to output a link for
// the rest of the referenced entities.
}
}
$elements[$delta]['#cache']['tags'] = $entity->getCacheTags();
}
return $elements;
}
}
After enabling this module (or after clearing the cache if this module was enabled earlier), you will have 'Link' formatter for all your 'Entity reference' fields, allowing you to customize link text just in the formatter settings.

Symfony2 Form euro to cents

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.

Resources