I’m creating a module in Magento 2 for managing flash sale events.
My current goal is to list all the events in an admin grid
But nothing is displayed on the page.
./etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="MagentoFrameworkViewElementUiComponentDataProviderCollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="flash_sale_event_listing_data_source" xsi:type="string">
TrainingFlashSaleModelResourceModelFlashSaleEventGridCollection
</item>
</argument>
</arguments>
</type>
<virtualType name="TrainingFlashSaleModelResourceModelFlashSaleEventGridCollection"
type="MagentoFrameworkViewElementUiComponentDataProviderSearchResult">
<arguments>
<argument name="mainTable" xsi:type="string">flash_sale_event</argument>
<argument name="resourceModel" xsi:type="string">TrainingFlashSaleModelResourceModelFlashSaleEvent
</argument>
<argument name="model" xsi:type="string">MagentoFrameworkViewElementUiComponentDataProviderDocument
</argument>
</arguments>
</virtualType>
</config>
./etc/adminhtml/acl.xml
<?xml version="1.0"?>
<acl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<resources>
<resource id="Magento_Backend::admin">
<resource id="Training_FlashSale::flashsale" title="Flash Sale" sortOrder="10">
<resource id="Training_FlashSale::manage_flashsale" title="Manage Flash Sales"/>
</resource>
</resource>
</resources>
</acl>
./Model/FlashSaleEvent.php
<?php
namespace TrainingFlashSaleModel;
use MagentoFrameworkDataCollectionAbstractDb;
use MagentoFrameworkModelAbstractModel;
use MagentoFrameworkModelContext;
use MagentoFrameworkModelResourceModelAbstractResource;
use MagentoFrameworkRegistry;
use PsrLogLoggerInterface;
class FlashSaleEvent extends AbstractModel
{
/**
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* Constructor.
*
* @param Context $context
* @param Registry $registry
* @param AbstractResource|null $resource
* @param AbstractDb|null $resourceCollection
* @param array $data
*/
public function __construct(
Context $context,
Registry $registry,
MagentoFrameworkModelResourceModelAbstractResource $resource = null,
MagentoFrameworkDataCollectionAbstractDb $resourceCollection = null,
array $data = []
)
{
$this->logger = $context->getLogger();
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
/**
* Initialize resource model.
*/
protected function _construct(): void
{
$this->_init(ResourceModelFlashSaleEvent::class);
}
/**
* Check if the flash sale event is active.
*
* This method checks that the event's status is enabled and that the current time
* falls between the start and end dates.
*
* @return bool
*/
public function isActive(): bool
{
$status = $this->getData('status');
// Use the correct keys: start_time and end_time
$startTime = $this->getData('start_time');
$endTime = $this->getData('end_time');
try {
$currentTime = new DateTime();
$start = new DateTime($startTime);
$end = new DateTime($endTime);
} catch (Exception $e) {
// Log the error message instead of the object
$this->logger->critical('Date conversion failed: ' . $e->getMessage());
return false;
}
return ($status == 1 && $currentTime >= $start && $currentTime <= $end);
}
/**
* Retrieve the discount type.
*
* Example values: 'percentage' or 'fixed'
*
* @return string
*/
public function getDiscountType(): string
{
return (string)$this->getData('discount_type');
}
/**
* Retrieve the discount value.
*
* This value is used in conjunction with the discount type to calculate the sale price.
*
* @return float
*/
public function getDiscountValue(): float
{
return (float)$this->getData('discount_value');
}
/**
* Get associated product IDs for this flash sale event.
*
* Typically, this data might be loaded from the pivot table, and stored in the model
* (e.g., as an array or a comma-separated string). Here we assume an array is stored.
*
* @return array
*/
public function getAssociatedProductIds(): array
{
$productIds = $this->getData('associated_product_ids');
return is_array($productIds) ? $productIds : [];
}
/**
* (Optional) Compute the discounted price for a given original price.
*
* For a percentage discount, it subtracts the computed discount amount.
* For a fixed discount, it subtracts the fixed amount (not going below zero).
*
* @param float $originalPrice
* @return float
*/
public function computeDiscountedPrice(float $originalPrice): float
{
$discountValue = $this->getDiscountValue();
$discountType = $this->getDiscountType();
if ($discountType === 'percentage') {
$discountAmount = ($discountValue / 100) * $originalPrice;
return max(0, $originalPrice - $discountAmount);
} elseif ($discountType === 'fixed') {
return max(0, $originalPrice - $discountValue);
}
// Return original price if discount type is not recognized
return $originalPrice;
}
}
./Model/ResourceModel/FlashSaleEvent.php
<?php
namespace TrainingFlashSaleModelResourceModel;
use MagentoFrameworkModelResourceModelDbAbstractDb;
use MagentoFrameworkModelAbstractModel;
class FlashSaleEvent extends AbstractDb
{
/**
* Initialize resource model.
*
* This method links the model to the database table `flash_sale_event` with the primary key `event_id`.
*/
protected function _construct(): void
{
$logger = MagentoFrameworkAppObjectManager::getInstance()->get(PsrLogLoggerInterface::class);
$logger->debug('Constructing flashSaleEvent resource model.');
$this->_init('flash_sale_event', 'event_id');
}
/**
* After-save hook to update the pivot table with associated products.
*
* This method checks if the model contains an array of associated product IDs.
* It then clears any existing associations in the pivot table and inserts the new ones.
*
* @param AbstractModel $object
* @return $this
*/
protected function _afterSave(AbstractModel $object): static
{
$logger = MagentoFrameworkAppObjectManager::getInstance()->get(PsrLogLoggerInterface::class);
$logger->debug('entering after save.');
$productIds = $object->getData('associated_product_ids');
if (is_array($productIds)) {
$connection = $this->getConnection();
$pivotTable = $this->getTable('flash_sale_event_product');
// Remove existing associations for this event
$connection->delete($pivotTable, ['event_id = ?' => $object->getId()]);
// Insert each new association
foreach ($productIds as $productId) {
$connection->insert($pivotTable, [
'event_id' => $object->getId(),
'product_id' => $productId,
]);
}
}
$logger->debug('leaving after save.');
return parent::_afterSave($object);
}
}
./Model/ResourceModel/FlashSaleEvent/Collection.php
<?php
namespace TrainingFlashSaleModelResourceModelFlashSaleEvent;
use MagentoFrameworkModelResourceModelDbCollectionAbstractCollection;
use TrainingFlashSaleModelFlashSaleEvent as FlashSaleEventModel;
use TrainingFlashSaleModelResourceModelFlashSaleEvent as FlashSaleEventResource;
class Collection extends AbstractCollection
{
/**
* Initialize the collection by specifying the model and resource model.
*/
protected function _construct(): void
{
$this->_init(FlashSaleEventModel::class, FlashSaleEventResource::class);
}
/**
* Add filter to only include active flash sale events.
*
* Active events are those with:
* - status = 1,
* - a start time before or equal to the current timestamp,
* - an end time after or equal to the current timestamp.
*This method is useful for cron jobs or frontend blocks that need to display only currently active flash sale events
*
* @return $this
*/
public function addActiveFilter(): self
{
$currentTimestamp = date('Y-m-d H:i:s');
$this->getSelect()
->where('status = ?', 1)
->where('start_time <= ?', $currentTimestamp)
->where('end_time >= ?', $currentTimestamp);
return $this;
}
}
./view/adminhtml/layout/flashsale_flashsale_index.xml
<?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>
<uiComponent name="flash_sale_event_listing"/>
</body>
</page>
./view/adminhtml/ui_component/flash_sale_event_listing.xml
<?xml version="1.0"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<!-- JS configuration for the grid -->
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">flash_sale_event_listing.flash_sale_event_listing_data_source</item>
</item>
</argument>
<settings>
<spinner>event_columns</spinner>
<deps>
<dep>flash_sale_event_listing.flash_sale_event_listing_data_source</dep>
</deps>
</settings>
<!-- Data Source configuration -->
<dataSource name="flash_sale_event_listing_data_source" component="Magento_Ui/js/grid/provider">
<settings>
<updateUrl path="mui/index/render"/>
</settings>
<dataProvider class="TrainingFlashSaleUiDataProviderFlashSaleEventDataProvider"
name="flash_sale_event_listing_data_source">
<settings>
<requestFieldName>event_id</requestFieldName>
<primaryFieldName>event_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<!-- Listing toolbar for filters, mass actions and paging-->
<listingToolbar name="listing_top">
<bookmark name="bookmarks"/>
<columnsControls name="columns_controls"/>
<filters name="listing_filters"/>
<massaction name="listing_massaction"/>
<paging name="listing_paging"/>
</listingToolbar>
<!-- Columns definition -->
<columns name="event_columns">
<!-- Selection Checkbox -->
<selectionsColumn name="ids" sortOrder="0">
<settings>
<indexField>event_id</indexField>
</settings>
</selectionsColumn>
<!-- Event ID Column -->
<column name="event_id" sortOrder="10">
<settings>
<filter>textRange</filter>
<label translate="true">ID</label>
<sorting>asc</sorting>
</settings>
</column>
<!-- Event Name Column -->
<column name="name" sortOrder="20">
<settings>
<filter>text</filter>
<label translate="true">Event Name</label>
</settings>
</column>
<!-- Discount Type Column -->
<column name="discount_type" sortOrder="30">
<settings>
<filter>select</filter>
<dataType>text</dataType>
<label translate="true">Discount Type</label>
</settings>
</column>
<!-- Discount Value Column -->
<column name="discount_value" sortOrder="40">
<settings>
<filter>textRange</filter>
<dataType>number</dataType>
<label translate="true">Discount Value</label>
</settings>
</column>
<!-- Start Time Column -->
<column name="start_time" sortOrder="50">
<settings>
<filter>dateRange</filter>
<dataType>date</dataType>
<label translate="true">Start Time</label>
</settings>
</column>
<!-- End Time Column -->
<column name="end_time" sortOrder="60">
<settings>
<filter>dateRange</filter>
<dataType>date</dataType>
<label translate="true">End Time</label>
</settings>
</column>
<!-- Actions Column for Edit -->
<actionsColumn name="actions" sortOrder="100">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="indexField" xsi:type="string">event_id</item>
<item name="actions" xsi:type="array">
<item name="edit" xsi:type="array">
<item name="type" xsi:type="string">edit</item>
<item name="label" xsi:type="string" translate="true">Edit</item>
<item name="url" xsi:type="url" path="flashsale/event/edit"/>
</item>
</item>
</item>
</argument>
</actionsColumn>
</columns>
</listing>
./Ui/DataProvider/FlashSaleEventDataProvider.php
<?php
namespace TrainingFlashSaleUiDataProvider;
use MagentoUiDataProviderAbstractDataProvider;
use TrainingFlashSaleModelResourceModelFlashSaleEventCollection as FlashSaleEventCollection;
use PsrLogLoggerInterface;
class FlashSaleEventDataProvider extends AbstractDataProvider
{
/**
* @var FlashSaleEventCollection
*/
protected $collection;
/**
* @var LoggerInterface
*/
protected LoggerInterface $logger;
/**
* Constructor
*
* @param string $name
* @param string $primaryFieldName
* @param string $requestFieldName
* @param FlashSaleEventCollection $collection
* @param LoggerInterface $logger
* @param array $meta
* @param array $data
*/
public function __construct(
$name,
$primaryFieldName,
$requestFieldName,
FlashSaleEventCollection $collection,
LoggerInterface $logger,
array $meta = [],
array $data = []
)
{
$this->logger = $logger;
$this->logger->debug('FlashSaleEventDataProvider constructed.');
$this->collection = $collection;
$this->logger->debug('Main table: ' . $this->collection->getMainTable());
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
}
/**
* Override getData() to log the collection details.
*
* @return array
*/
public function getData(): array
{
$this->logger->debug('Main table: ' . $this->collection->getMainTable());
$this->logger->debug('FlashSaleEventDataProvider: getData() called.');
// Log collection size before loading (if not loaded yet)
if (!$this->collection->isLoaded()) {
$this->logger->debug('Collection not loaded yet. Size before load: ' . $this->collection->getSize());
}
// Force loading the collection
$this->collection->load();
$itemsCount = $this->collection->getSize();
$this->logger->debug('After load, collection size: ' . $itemsCount);
$sql = $this->collection->getSelect()->__toString();
$this->logger->debug('SQL Query: ' . $sql);
$data = [
'totalRecords' => $itemsCount,
'items' => array_values($this->collection->toArray()['items'])
];
$this->logger->debug('Data fetched: ' . json_encode($data));
return $data;
}
}
./Controller/Adminhtml/FlashSale/Index.php
<?php
namespace TrainingFlashSaleControllerAdminhtmlFlashSale;
use MagentoBackendAppAction;
use MagentoFrameworkAppResponseInterface;
use MagentoFrameworkControllerResultInterface;
use MagentoFrameworkViewResultPage;
use MagentoFrameworkViewResultPageFactory;
use PsrLogLoggerInterface;
class Index extends Action
{
const ADMIN_RESOURCE = 'Training_FlashSale::flashsale';
/**
* @var PageFactory
*/
protected PageFactory $resultPageFactory;
private LoggerInterface $logger;
public function __construct(
ActionContext $context,
PageFactory $resultPageFactory,
LoggerInterface $logger
)
{
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
$this->logger = $logger;
}
public function execute(): Page|ResultInterface|ResponseInterface
{
$this->logger->debug("Starting FlashSale Event");
$resultPage = $this->resultPageFactory->create();
$resultPage->setActiveMenu('Training_FlashSale::flashsale');
$resultPage->getConfig()->getTitle()->prepend(__('Manage Flash Sales'));
return $resultPage;
}
}